简易JVM的实现思路:C++实现

编译器前端:

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;则不是数组变量
            Object obArray[InitHeapSize];
}Heap;

typedef Object{
    bool isArray;
    int gcMark;
    Byte var[1];
}
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.添加库API,当JVM实现了原生数组后,就可以写一个ArrayList库。

------

所有的 GCObject 都有一个相同的数据头,叫作 CommonHeader ,在 lobject.h 里 43 行 以宏形式定义出来的。使用宏是源于使用上的某种便利。C 语言不支持结构的继承。

#define CommonHeader    GCObject *next; lu_byte tt; lu_byte marked

从这里我们可以看到:所有的 GCObject 都用一个单向链表串了起来。每个对象都以 tt 来识别其类型。marked 域用于标记清除的工作。

标记清除算法是一种简单的 GC 算法。每次 GC 过程,先以若干根节点开始,逐个把直接以及间接和它们相关的节点都做上标记。对于 Lua ,这个过程很容易实现。因为所有 GObject 都在同一个链表上,当标记完成后,遍历这个链表,把未被标记的节点一一删除即可。

Lua 在实际实现时,其实不只用一条链表维系所有 GCObject 。这是因为 string 类型有其特殊性。所有的 string 放在一张大的 hash 表中。它需要保证系统中不会有值相同的 string 被创建两份。顾 string 是被单独管理的,而不串在 GCObject 的链表中。

 https://blog.codingnow.com/2011/03/lua_gc_1.html


参考列表:

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

5.《自制编译器》

6.http://blog.jamesdbloom.com/JVMInternals.html

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值