編譯器前端:
1.詞法分析:由Scanner.cc將char流轉換成一個個token給Parser.cc消費,Scanner轉換的過程中可以甄別出非法的token類型,將其打上tag=error的標籤。
2.語法分析:Parser.cc收到一個個的token進行語法進行語法分析,將其轉換成AST : List<Statmt>。設置兩個緩衝區存儲token流,Que<Token>tokenbuffer1,Que<Token>tokenbuffer2,大小不超過10.
getToken時,先判斷tokenbuffer2中有沒有token,如果有,則優先拿tokenbuffer2中的token。當tokenbuffer1大小超過10時,從tokenbuffer1的前端pop出一個值。這樣保證緩存區中的大小爲10;
ungetToken時,tokenbuffer1中肯定是有token的,如果沒有,則報錯。
爲什麼要設置兩個buffer呢?一個buffer不就可以了嗎?
不可以,設置兩個緩衝區的目的是爲了同步。當我unget兩個token的時候,Scanner的指針已經在兩個token之後的位置了,但是此時Scanner指針是不能回退的,設置buffer2的目的是爲了保存unget的結果,同時可以讓Scanner一直往後移動,不回頭。
Scanner::Token Parser::getToken() {
if (tokenBuffer2.size() == 0) {
auto token = scanner.nextToken();
tokenBuffer1.push_back(token);
if (tokenBuffer1.size() > 10)
tokenBuffer1.pop_front();
} else {
auto token = tokenBuffer2.front();
tokenBuffer1.push_back(token);
tokenBuffer2.pop_front();
}
return tokenBuffer1.back();
}
Scanner::Token Parser::ungetToken() {
assert(tokenBuffer1.size() > 0);
auto token = tokenBuffer1.back();
tokenBuffer2.push_front(token);
tokenBuffer1.pop_back();
return tokenBuffer1.back();
}
3.採用Visitor模式遍歷AST,進行變量的引用消解,將變量的引用定位的局部/全局變量表的具體位置,並進行類型的檢查,比如將int型賦值給String變量,即保證“=”兩邊的類型要一致。當然,也可以不用Visitor模式,直接給每個Statmt打上標籤,就可以判斷出Statmt是屬於哪個Stat類型,比如說賦值Statmt,函數調用Statmt。
4.將語法分析好的AST樹交給後端處理。
編譯器後端:
1.後端接到AST後,將節點中的信息分配到虛擬機的不同的區域。
#include <stdio.h>
#define STACK_MAX 256
typedef char BYTE;
//無操作數指令
typedef enum{
ireturn
}singleOp;
typedef enum{
iput
} doubleOp;
typedef struct {
union{
singleOp sinOp;
struct{
doubleOp douOp;
int arg1;
};
};
}Instruction;
typedef struct {
int returnVar;//返回值
BYTE hasReturnVar;//標誌位
int localVar[STACK_MAX];
int operandStack[STACK_MAX];
int* ConstantPoolRef;
int returnAdr;//方法返回地址
}Frame;
//方法區裏面有多個ClassData
typedef struct {
int ConstantPool[STACK_MAX];
Instruction OPList[STACK_MAX];
}ClassData;
typedef struct {
int gcMark;
ClassData *ClassRef;
BYTE commonVarTable[1];//存放普通類型對象
BYTE arrayVarTable[1];//數組類型對象,存放數組的長度,如果h長度爲0;則不是數組變量
}Heap;
typedef struct {
int start;
int end;
}ObjSite;
typedef struct {
int stackSize;
Heap *heap;
Instruction insList[STACK_MAX];
ObjSite objSiteTable[STACK_MAX];
int numObjects;
int maxObjects;
int PC;
Frame FrameStack[STACK_MAX];
} VM;
jvm內存佈局 (圖片轉自:http://blog.jamesdbloom.com/JVMInternals.html)
2.解析opcode指令。
從Main類的main函數開始執行。
3.堆內存分配,如何實現堆中分配內存?
直接動態擴展數組:
BYTE commonVarTable[1];//存放普通類型對象
BYTE arrayVarTable[1];//數組類型對象,存放數組的長度,如果h長度爲0;則不是數組變量
4.GC回收
採用標記-整理算法。
4.1什麼時候觸發GC?
當堆內存已滿或者下一次分配對象時堆剩餘空間無法滿足分配時,就觸發GC。
4.2 怎樣標記存活的對象?即怎樣區分哪些對象是垃圾?什麼樣的代碼會觸發JVM的GC?
遍歷Frame中的對象,與這些對象有關聯的對象都是不能回收的。
比如,我在一個函數裏面申明瞭一個局部變量對象,那麼函數結束時,是要將該局部變量清理掉的。那不可能沒次彈出一個Frame,JVM就去釋放掉對應的內存,這樣太費時間了,並不是每個函數都申明瞭局部變量對象,那麼JVM可以等到堆內存空間滿時再用GCRoot算法,找出要釋放的堆中要釋放的對象。在JDK6之後,採用棧對象,將函數創建的局部對象變量分配在棧中。
1.public class A {
A a = new A();
A b = new B();
a = null; -----1
a = b; ------2
}
}
當給一個對象引用賦值爲null或者指向其他對象,堆中的那個對象就無法訪問。
2.當在函數裏面創建了局部對象變量,函數結束時,導致無法訪問。
4.3C++內存泄漏的場景分析,即C++什麼時候用delete?
在C++中new和delete一定要成對使用。參考:https://blog.csdn.net/xxpresent/article/details/53024555
測試文件,test.java
class C{//類的大寫檢查
int = 0;
C(int i ){
i ++;
}
}
Class B{
int i = 0;
C c = new C(3);
Int fun(int i, int j, int k){
int m = 3; //函數局部變量
Return i + j + k + m; //前端處理表達式
}
}
class A{
const m = 3;
Const B n = new B();//默認構造函數
int i = 0;
char a = ‘a’;
B p; //並沒有在堆中開闢內存
B b = new B(); //成員變量
b.fun(1,2,3);
b = null; //GC
C c = new C();
int arr[3] = {1,2,3};
for(int i = 0; i < 3;i++){
i = i + 1;
j = i + j; //前端類型檢查
for(int k = 0; k < 3; k++){
print(‘*’);
}
}
}
參考列表:
1.參考用C++寫的JVM,實現了類加載和前端。
https://github.com/raylrnd?after=Y3Vyc29yOnYyOpK5MjAxNi0xMC0yN1QxMjowNjozNiswODowMM4EPdGu&tab=stars
2.可以參考https://github.com/raylrnd/wgtcc寫個前端(C++)。
3.參考https://github.com/airtrack/luna,他仿照lua編譯器寫的,實現類StringPool。
4.https://github.com/Xiang1993/jack-compiler/blob/master/jackc/
5.《自制編譯器》