程序员第一个(玩具)JVM

程序员第一个(玩具)JVM

我们都知道Java程序要运行在JVM之上,我们除了面试时会了解下JVM的面试题,之外可能很少会去想JVM是如何工作的。在这篇文章中,我会尝试写一个玩具JVM来展示其背后的核心原理,希望激发你进一步学习的兴趣。

一个简单的目标

java 复制代码
package me.kagami.myjvm;

public class Add {
    public static int add(int a, int b) {
        return a + b;
    }
}

首先使用javac Add.java编译后得到Add.class文件。该文件是JVM可以执行的二进制文件。接下来要做的事情就是正确地实现一个能够执行Add.class文件的JVM。

CLASS LOADER

JVM的工作之一是类加载,那么我们需要了解class文件的内容。

如果我们用hexdump插件以16进制视图打开Add.class文件,我们可以看到class文件的组织形式,但我们现在还完看不懂这个文件。没关系,本文将手把手介绍怎么阅读class文件。

如果查看Java规格说明,那么我们可以学习到classFile的结构。可以看到classFile文件总是以4字节的magic数开头(CAFEBABE)然后是2+2的版本信息 ,以此类推。而cp_info,field_info,method_info,attribute_info会比较复杂,本文具体以cp_info详细说明,只要会看cp_info后,其他三个都一样。

ini 复制代码
ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

我们以上文的Add.class举例

css 复制代码
magic[CA FE BA BE] minor_version[00 00] major_version[00 34] constant_pool_count[00 15] 0A 00 03 00 12 07

常量池计数是00 150x15换成十进制是21,规格说明里有这么一句话:

The constant_pool table is indexed from 1 to constant_pool_count - 1.

说明常量的个数应该是21-1为20个。那么说明constant_pool_count后面有20个常量信息,那么我们来看看常量池是怎么排列的吧。

根据Java规则说明,cp_info的结构如下:

ini 复制代码
cp_info {
    u1 tag;
    u1 info[];
}

忽略info前面的u1,因为规格说明里有这么一句话,说明tag后面是可能有多个字节的:

Each tag byte must be followed by two or more bytes giving information about the specific constant.

我们以上文的Add.class举例,constant_pool_count后第一个tag是 0A

css 复制代码
CA FE BA BE 00 00 00 34 constant_pool_count[00 15] 0A 00 03 00 12 07

根据Java规格说明,我们查看tag表类别

Constant Kind Tag
CONSTANT_Class 7
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_String 8
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_NameAndType 12
CONSTANT_Utf8 1
CONSTANT_MethodHandle 15
CONSTANT_MethodType 16
CONSTANT_Dynamic 17
CONSTANT_InvokeDynamic 18
CONSTANT_Module 19
CONSTANT_Package 20

0A换成十进制是10,所以第一个常量应该是CONSTANT_Methodref类型,那么我们再根据Java规格说明查看CONSTANT_Methodref类型的格式为:

ini 复制代码
CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}

所以我们继续看Add.class文件,class_index和name_and_type_index都是常量池的引用,也就是说,class_index指向的是常量池的第3常量,name_and_type_index指向的是第18(0x12为十进制18)常量。

css 复制代码
CA FE BA BE 00 00 00 34 constant_pool_count[00 15] CONSTANT_Methodref_info[0A class_index(00 03) name_and_type_index(00 12)] 07

常量池的看法我想你应该能看懂了,那么我现在直接给出常量池的全部解析后的结果,我们直接看第3和第18常量是什么吧。以下是按照tag表分出来的20个常量:

css 复制代码
CA FE BA BE 00 00 00 34 constant_pool_count[00 15] [0A 00 03 00 12] [0700 13] [07 00 14] CONSTANT_Utf8_info [01 (00 06) 3C 69 6E 69 74 3E] [01 00 03 28 29 56] [01 00 04 43 6F 64 65] [01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65] [01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65] [01 00 04 74 68 69 73] [01 00 15 4C 6D 65 2F 6B 61 67 61 6D 69 2F 6D 79 6A 76 6D 2F 41 64 64 3B] [01 00 03 61 64 64] [01 00 05 28 49 49 29 49] [01 00 01 61] [01 00 01 49] [01 00 01 62] [01 00 0A 53 6F 75 72 63 65 46 69 6C 65] [01 00 08 41 64 64 2E 6A 61 76 61] [0C 00 04 00 05] [01 00 13 6D 65 2F 6B 61 67 61 6D 69 2F 6D 79 6A 76 6D 2F 41 64 64] [01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74] 00 21 00 02 00 03 00 00 00 00 00 02 00 01 00 
....

从中可以看出第3常量是[07 00 14]其中tag为CONSTANT_Class,其结构为:

ini 复制代码
CONSTANT_Class_info {
    u1 tag;
    u2 name_index;
}

说明name_index也是一个常量的引用,0x14指向的是第20常量,它是一个CONSTANT_Utf8_info常量,这种常量是utf8表示的字符串,结构是:

ini 复制代码
CONSTANT_Utf8_info {
    u1 tag;
    u2 length;
    u1 bytes[length];
}

所以第20常量[01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74]的长度是0x0010,也就是16个字符,从dump列可以用看出这16个字符是:java/lang/Object,同理可得第18常量为:0x04<init>0x05()V

那么第1个常量连起来就是java/lang/Object<init>()V,但这表示是什么意思呢 ?我们翻阅Java规格说明针对CONSTANT_Methodref_info找到了这么一句话:

If the name of the method in a CONSTANT_Methodref_info structure begins with a '<' ('\u003c'), then the name must be the special name <init>, representing an instance initialization method (§2.9.1). The return type of such a method must be void.

原来表示的是初始化方法,继续看文档我们又找到了一句话看完之后就更明确了:

A method is an instance initialization method if all of the following are true:

  • It is defined in a class (not an interface).
  • It has the special name <init>.
  • It is void (§4.3.3).

综上所述,第1个常量应该表示的是初始化方法的字符引用。

好了,cp_info我们都能看懂了,那么剩下的应该都不足为惧了,我们看看还剩下什么:

ini 复制代码
  	......
  	u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];

这里我先给出所有剩余字段的解析,可以看出剩下的字节码中,大部分是和method_info相关的,而对method_info的解析也是我们实现JVM的关键一步,所以单独拿出来说一下。(因为method_info里有非常关键的Code结构)

css 复制代码
常量池结尾 access_flags[00 21] this_class[00 02] super_class[00 03] interfaces_count[00 00] fields_count[00 00] methods_count[00 02] method_info[(00 01) (00 04) (00 05) (00 01) Code(00 06) (00 00 00 2F) (00 01) (00 01) (00 00 00 05) (2A B7 00 01 B1) (00 00) attributes_count(00 02) LineNumberTable(00 07) (00 00 00 06) (00 01) (00 00) (00 03) LocalVariableTable(00 08) (00 00 00 0C) (00 01) (00 00) (00 05) (00 09) (00 0A) (00 00) method_info(00 09) (00 0B) (00 0C) (00 01) Code(00 06) (00 00 00 38) (00 02) (00 02) (00 00 00 04) (1A 1B 60 AC) (00 00) attributes_count(00 02) LineNumberTable(00 07) (00 00 00 06) (00 01) (00 00) (00 05) LocalVariableTable(00 08) (00 00 00 16) (00 02) (00 00) (00 04) (00 0D) (00 0E) (00 00) (00 00) (00 04) (00 0F) (00 0E) (00 01)] 
attributes_count[00 01] [SourceFile(00 10) (00 00 00 02) (00 11)]

通过methods_count我们知道这个类有两个方法。要看懂method_info我们需要先了解method_info的结构,在Java规格说明中有:

ini 复制代码
method_info {
    u2             access_flags;
    u2             name_index;
    u2             descriptor_index;
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

然后我们先看第一个方法:

scss 复制代码
(00 01) (00 04) (00 05) (00 01) Code(00 06) (00 00 00 2F) (00 01) (00 01) (00 
00 00 05) (2A B7 00 01 B1) (00 00) attributes_count(00 02) LineNumberTable(00 07) (00 00 
00 06) (00 01) (00 00) (00 03) LocalVariableTable(00 08) (00 00 00 0C) (00 01) 
(00 00) (00 05) (00 09) (00 0A) (00 00)

说明这个方法的access_flags是public,name_index指向的是第4个常量指向了一个字符串<init>,descriptor_index的意思是方法描述第5个常量指向字符串()V。attributes_count是1。

我在最开始看Java规格说明的时候对attribute这个东西比较模糊,为什么attribute在field_info有?在method_info也有?在ClassFile中也有?感觉到处都是attribute,我现在的理解是attribute是对当前结构进行说明的东西,各种信息都可以在attributes里体现,举个例子:ClassFile中有个attribute叫SourceFile说明class的源文件是哪个。

源文件?好像与运行时没有太大的关系,但还是通过attribute附加到ClassFile中(可能是dubug工具需要SourceFile信息)。Java规格说明定义了28种attribute,其中大部分attribute都是可选的。

我们继续看Add.class关键的地方来了,第一个方法有一个attribute指向了0x06常量,这个常量对应的字符串是Code,所以这个attribute是一个Code attribute他的结构是。

ini 复制代码
Code_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 max_stack;
    u2 max_locals;
    u4 code_length;
    u1 code[code_length];
    u2 exception_table_length;
    {   u2 start_pc;
        u2 end_pc;
        u2 handler_pc;
        u2 catch_type;
    } exception_table[exception_table_length];
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

很长,但是包括了很多重要的信息,比如max_stack是执行这个方法所需的最大栈长度,max_locals是执行这个方法所需要的本地变量数,以及最关键的code_length与code信息,code里包括了字节码的逻辑,也就是说你写的代码的逻辑就放在code里!第一个方法的code指令是:

scss 复制代码
2A (B7 00 01) B1

在Java规格说明第六章中已经详细说明了每个指令码的码值,我们按图索骥。其中0x2Aaload_0 = 42 (0x2a)0xB7invokespecial = 183 (0xb7)因为它的格式带了两个参数,所以00 01都是它的入参,这条指令的目的是为了执行Add类的构造方法。

diff 复制代码
//invokespecial 指令的结构
Format
invokespecial
indexbyte1
indexbyte2

0xB1return = 177 (0xb1)表示void方法返回。

继续看,第一个方法exception_table_length为0,没有异常处理,然后它的attributes_count为2说明有两个attribute分别是LineNumberTable和LocalVariableTable,这两个东西我们本文不关注,你只要知道他们是在debug时用的就行了。

我们本次写的JVM是以一个非常简单的JVM,不考虑实现面向对象的功能,所以我们就不继续关注构造方法了。我们这次需要关注的是第二个方法,add方法,这个方法是我们本次实现JVM的重点。add方法的method_info描述如下

scss 复制代码
(00 09) (00 0B) (00 0C) 
(00 01) Code(00 06) (00 00 00 38) (00 02) (00 02) (00 00 00 04) 
(1A 1B 60 AC) (00 00) attributes_count(00 02) LineNumberTable(00 07) (00 00 00 06) (00 01) 
(00 00) (00 05) LocalVariableTable(00 08) (00 00 00 16) (00 02) (00 00) (00 04) 
(00 0D) (00 0E) (00 00) (00 00) (00 04) (00 0F) (00 0E) (00 01)

这个方法的access_flags是0x0009表示这个方法是public(0x00001) + static(0x00008)。name_index指向常量add,其他的基本与第一个方法差不多,我们只关注add方法的code片段:

1A 1B 60 AC

翻译过来就是:

ini 复制代码
1A = iload_0 
1B = iload_1 
60 = iadd 
AC = ireturn 

如果你懂一点汇编的话就能理解这4条操作码其实实现了一个加法。JVM是基于栈来运行字节码的,iload_0iload_1意思是把当前帧的本地0变量和1变量入栈,然后调用iadd将栈的前两个元素相加后把结果再压入栈,最后使用ireturn将栈顶元素返回。

文章写到这里所有的困难已经扫清了,我们的目标也明确了:我们需要按照规则解析class文件,然后调用add方法,解析add方法的字节码,并且得到返回值。

我们现在应该能够隐约感觉到解析class文件是一个体力活,既然是体力活,那么肯定有人已经做过了。确实有这么一个开源库实现了解析class文件,它就是:asm库。这个库可谓是非常专业解析java字节码的库了,官网上说openjdk中也用到了这个库。

xml 复制代码
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>9.5</version>
</dependency>

它的使用也非常简单

java 复制代码
//从文件流中加载,返回的ClassNode就包括了Class文件的所有解析后结果
ClassNode classNode = loadInitialClass(classLoader.getResourceAsStream("me/kagami/myjvm/Add.class"));
//通过名称找到add方法
MethodNode addMethod = MyJvm.findPublicStaticMethod(classNode, "add");

我们关注的重点Code指令存放在instructions中

这里有个问题是,为什么Class文件中指令明明是1A 1B但asm解析出来是21呢?我翻看Java规格说明原来iload有一系列指令

ini 复制代码
iload = 21 (0x15)

iload_0 = 26 (0x1a)
iload_1 = 27 (0x1b)
iload_2 = 28 (0x1c)
iload_3 = 29 (0x1d)

这里肯定是asm替我们做了转换了,翻看源码果然如此,位于org.objectweb.asm.ClassReader类的2237行左右

java 复制代码
case Constants.ALOAD_3:
    opcode -= Constants.ILOAD_0;
	//会把iload_x的x存放在第二个参数中
    methodVisitor.visitVarInsn(Opcodes.ILOAD + (opcode >> 2), opcode & 0x3);
    currentOffset += 1;
    break;

题外话:javap命令

直接看16进制的字节码也太困难了,如果工作上有需求要看字节码我们不可能对着文档一个一个看,太慢了。那么有什么好的方法直接可以看解析后的结果呢?是有的,就是JDK自带的javap命令。我们只要输入

arduino 复制代码
javap -v Add.class

就可以看到以下输出,常量池,代码区,都非常清晰。

yaml 复制代码
Classfile /C:/Users/Tian/Desktop/Add.class
  Last modified 2023-8-8; size 362 bytes
  MD5 checksum 3737aee44531207ae6270a32364fbc5d
  Compiled from "Add.java"
public class me.kagami.myjvm.Add
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#18         // java/lang/Object."<init>":()V
   #2 = Class              #19            // me/kagami/myjvm/Add
   #3 = Class              #20            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lme/kagami/myjvm/Add;
  #11 = Utf8               add
  #12 = Utf8               (II)I
  #13 = Utf8               a
  #14 = Utf8               I
  #15 = Utf8               b
  #16 = Utf8               SourceFile
  #17 = Utf8               Add.java
  #18 = NameAndType        #4:#5          // "<init>":()V
  #19 = Utf8               me/kagami/myjvm/Add
  #20 = Utf8               java/lang/Object
{
  public me.kagami.myjvm.Add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lme/kagami/myjvm/Add;

  public static int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: ireturn
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0     a   I
            0       4     1     b   I
}
SourceFile: "Add.java"

JVM 栈帧

方法执行时会像JVM申请一个执行栈,栈我们应该都很熟悉了(栈溢出的异常都见过吧),它包括了操作数栈、本地变量表,代码块。此外还包含当前指令指针(记录执行字节码时已经进行了多少步)和一个指向包含该方法的类的指针,用于访问类的常量池以及其他细节。我打算这么实现栈帧

java 复制代码
public class Frame {
	//操作数栈
    private final Object[] operands; 
    //本地变量表
    private final Object[] locals;   
    //操作数栈指针
    private int stackPtr;            
	//指令列表
    private final InsnList instructions;
    //当前指令指针
    private AbstractInsnNode programCtr;
	//用于记录返回值
    private Object returnObj;
    //这里我们用到了,CodeAttribute中的stack=2, locals=2字段,用于声明操作栈和本地变量表大小
    public Frame(int maxStack, int maxLocals, InsnList instructions, Object[] args) {
        this.operands = new Object[maxStack];
        this.locals = new Object[maxLocals];
        this.programCtr = null;
        this.instructions = instructions;
        for (int i = 0; i < args.length; i++) {
            store(i, args[i]);
        }
    }
    //操作数栈的push
    public void push(Object value) {
        this.operands[this.stackPtr++] = value;
    }
 	//操作数栈的pop
    public <T> T pop(Class<T> clazz) {
        return clazz.cast(this.operands[--this.stackPtr]);
    }
	//本地变量表
    public void store(int var, Object value) {
        this.locals[var] = value;
    }
    //本地变量表
    public <T> T load(int var, Class<T> clazz) {
        return clazz.cast(this.locals[var]);
    }
    //省略....
}

我们现在得到了栈帧,并且已经初始化好,现在我们开始利用栈帧执行指令了:

java 复制代码
   public static void executeFrame(Frame frame) {
        InsnList instructions = frame.getInstructions();
		//利用循环一条一条执行指令
        while (frame.next()) {
            int opcode = frame.getProgramCtr().getOpcode();
            //查找指令的实现类
            OpCodeInterface opCodeInterface = OpCodeService.CODE_MAP.get(opcode);
            if (opCodeInterface == null) {
                System.out.println("异常,指令没有实现类" + opcode);
                return;
            }
            opCodeInterface.handle(frame);
        }
    }

要做的就是用frame的操作数栈和本地变量表,一条一条执行指令,我们这次的指令用到了

ini 复制代码
1A = iload_0 
1B = iload_1 
60 = iadd 
AC = ireturn 

其中iload_0iload_1 被统一成iload所以执行add方法只需要实现三个指令。

java 复制代码
public class OpCode21iloadHandler implements OpCodeInterface{
    @Override
    public int getOpCode() {
        return 21;
    }
    // iload的实现
    @Override
    public void handle(Frame frame) {
        //从本地变量表中取出指定第几个变量,并存放在操作数栈中
         System.out.println("BE OP:" + getOpCode() + " Stack:" + Arrays.toString(frame.getOperands()));
        frame.push(frame.getLocals()[((VarInsnNode) frame.getProgramCtr()).var]);
        System.out.println("AF OP:" + getOpCode() + " Stack:" + Arrays.toString(frame.getOperands()));
    }
}

public class OpCode96iaddHandler implements OpCodeInterface {
    @Override
    public int getOpCode() {
        return 96;
    }

    // iadd的实现
    @Override
    public void handle(Frame frame) {
        System.out.println("BE OP:" + getOpCode() + " Stack:" + Arrays.toString(frame.getOperands()));
        Integer v1 = frame.pop(Integer.class);
        Integer v2 = frame.pop(Integer.class);

        frame.push(v1 + v2);
         //从操作栈栈顶的两个数据pop后相加在push到栈里
        System.out.println("AF OP:" + getOpCode() + " Stack:" + Arrays.toString(frame.getOperands()));
    }
}

public class OpCode172ireturnHandler implements OpCodeInterface {
    @Override
    public int getOpCode() {
        return 172;
    }
	//ireturn的实现
    @Override
    public void handle(Frame frame) {
        System.out.println("BE OP:" + getOpCode() + " Stack:" + Arrays.toString(frame.getOperands()));
        Integer pop = frame.pop(Integer.class);
        //存放返回值
        frame.setReturnObj(pop);
        System.out.println("AF OP:" + getOpCode() + " Stack:" + Arrays.toString(frame.getOperands()));
    }
}

最后,我们就可以调用add()方法了

java 复制代码
public static void main(String[] args) throws IOException {
        ClassLoader classLoader = Main.class.getClassLoader();
    //加载Class文件
        ClassNode classNode = loadInitialClass(classLoader.getResourceAsStream("me/kagami/myjvm/Add.class"));
    //找到add()方法
        MethodNode addMethod = MyJvm.findPublicStaticMethod(classNode, "add");
    //这里我们直接调用add方法,给add()方法传入参 1和2
        MyJvm.run(classNode, addMethod, new Integer(1), new Integer(2));
    }

我们运行后可以得到以下输出,打印了每个指令执行时操作数栈的前后变化情况。它正常运转了!打印出了最终的结果:3!

它是一个非常简单并且简陋的JVM,但是它做到了JVM应该做的事:加载字节码,并且执行字节码(当然,真正的JVM需要做的东西还有很多)。

ini 复制代码
输出如下:
BE OP:21 Stack:[null, null]
AF OP:21 Stack:[1, null]
BE OP:21 Stack:[1, null]
AF OP:21 Stack:[1, 2]
BE OP:96 Stack:[1, 2]
AF OP:96 Stack:[3, null]
BE OP:172 Stack:[3, null]
AF OP:172 Stack:[null, null]
3

离真正的JVM还差了什么?

  1. 内存管理

    我们的玩具JVM还是建立JVM之上的,所以对内存的掌控还是依赖JVM的内存管理。但想摆脱JVM也不是很难,假如我们用的是C语言实现的JVM,我们在解析Class后,在运行时完全可以自己管理内存的分配释放,甚至实现自己的垃圾回收机制。

  2. 指令的实现

    还有很多指令没有实现,我们只实现了int类型的load,Java中还有long/float/double等等类型都有对应的load指令,还有goto/if等跳转指令,还有pop/dup/swap用于操作操作数栈,还有最关键的invoke相关的用于引用的处理。

  3. 面相对象的实现

    需要考虑对象模型的实现,如何存储对象和类,如何处理继承等,如何实现new指令。还有需要注意方法调用的指令,每种指令都有略微的不同。

    invokestatic:对一个类调用静态方法。
    invokespecial:直接调用实例方法,例如私有方法。
    invokeinterface:调用接口方法
    invokedynamic:调用动态计算的方法调用点。(这个我暂时也不清楚是干啥的)
    
  4. 垃圾回收

    上文也提到了实现垃圾回收,实现垃圾回收需要考虑怎么判断对象是否为垃圾,引用的计数等,这里是JVM实现者可以自主发挥最大的地方,我们熟知的垃圾回收方法都有好几种了。

  5. 异常机制的实现

    如何实现athrow指令,如何使用异常表将异常在帧中传播。

JVM需要实现的东西还有很多我这就不一一列举了。

总结

实现自己的JVM是一个有趣的过程,在实现的过程中终于提起兴趣翻看JAVA规格说明,我的实现很简陋,如果有更多兴趣的读者,可以参考《自己动手写Java虚拟机》这本书,这本书使用GO语言从无到有的实现了一个能打印出helloworld的JVM。

本文源代码:gitee.com/kagami1/sim...

相关推荐
simple_ssn38 分钟前
【蓝桥杯】压缩字符串
java·算法
舒克日记1 小时前
Java:189 基于SSM框架的在线电影评价系统
java·开发语言
2401_857610031 小时前
中文学习系统:成本效益分析与系统优化
java·数据库·学习·架构
nbsaas-boot1 小时前
如何更高效地使用乐观锁提升系统性能
java·服务器·数据库
转转技术团队1 小时前
【述职黑话】ToB交易业务解决方案之状态机
java·状态模式
darkdragonking1 小时前
解决POM依赖与maven仓库关联的问题
java·maven
m0_672449601 小时前
前后端分离(前端删除数据库数据)
java·数据库·mysql
飞的肖1 小时前
在 Java 项目中集成和使用 dl4j 实现通过扫描图片识别快递单信息
java·ai·图像识别·dl4j
Javatutouhouduan1 小时前
如何系统全面地自学Java语言?
java·后端·程序员·编程·架构师·自学·java八股文
正在绘制中1 小时前
Java重要面试名词整理(八):RabbitMQ
java·面试·java-rabbitmq