用Java實(shí)現(xiàn)JVM(一):剛好夠運(yùn)行 HelloWorld

1. 前言

沒(méi)錯(cuò)這又是一篇介紹 JVM 的文章,這類文章網(wǎng)上已經(jīng)很多,不同角度、不同深度、不同廣度,也都不乏優(yōu)秀的。為什么還要來(lái)一篇?首先對(duì)于我來(lái)說(shuō),我正在學(xué)習(xí) Java,了解JVM的實(shí)現(xiàn)對(duì)學(xué)習(xí)Java當(dāng)然很有必要,但我已經(jīng)做了多年C++開(kāi)發(fā),就算我用C++實(shí)現(xiàn)一個(gè)JVM,我還是個(gè)C++碼農(nóng),而用 Java實(shí)現(xiàn),即能學(xué)習(xí) Java 語(yǔ)法,又能理解 JVM,一舉兩得。其次,作為讀者,hotspot或者其他成熟JVM實(shí)現(xiàn)的源碼讀起來(lái)并不輕松,特別是對(duì)沒(méi)有C/C++經(jīng)驗(yàn)的人來(lái)說(shuō),如果只是想快速了解JVM的工作原理,并且希望運(yùn)行和調(diào)試一下JVM的代碼來(lái)加深理解,那么這篇文章可能更合適。

我將用Java實(shí)現(xiàn)一個(gè)JAVA虛擬機(jī)(源碼在這下載,加 Star 亦可),一開(kāi)始它會(huì)非常簡(jiǎn)單,實(shí)際上簡(jiǎn)單得只夠運(yùn)行HelloWorld。雖然簡(jiǎn)單,但是我盡量讓其符合 JVM 標(biāo)準(zhǔn),目前主要參考依據(jù)是《Java虛擬機(jī)規(guī)范 (Java SE 7 中文版)》。

2. 準(zhǔn)備

先寫一個(gè)HelloWorld,代碼如下:

package org.caoym;

public class HelloWorld {
    public static void main(String[] args){
        System.out.println("Hello World");
    }
}

我期望所實(shí)現(xiàn)的虛擬機(jī)(姑且命名為JJvm吧),可以通過(guò)以下命令運(yùn)行:

$ java org.caoym.jjvm.JJvm org.caoym.HelloWorld
Hello World

接下來(lái)我們開(kāi)始實(shí)現(xiàn)JJvm,下面是其入口代碼,后面將逐步介紹:

public void run(String[] args) throws Exception {
    Env env = new Env(this);
    //加載初始類
    JvmClass clazz = findClass(initialClass);
    //找到入口方法
    JvmMethod method = clazz.getMethod(
            "main",
            "([Ljava/lang/String;)V",
            (int)(AccessFlags.JVM_ACC_STATIC|AccessFlags.JVM_ACC_PUBLIC));
    //執(zhí)行入口方法
    method.call(env, clazz, (Object[]) args);
}

3. 加載初始類

我們將包含 main 入口的類稱為初始類,JJvm 首先需要根據(jù)org.caoym.HelloWorld類名,找到.class 文件,然后加載并解析、校驗(yàn)字節(jié)碼,這些步驟正是 ClassLoader(類加載器)做的事情。HelloWorld.class內(nèi)容大致如下:

cafe babe 0000 0034 0022 0a00 0600 1409
0015 0016 0800 170a 0018 0019 0700 1a07
001b 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 164c 6f72 672f 6361
6f79 6d2f 4865 6c6c 6f57 6f72 6c64 3b01
0004 6d61 696e 0100 1628 5b4c 6a61 7661
...

沒(méi)錯(cuò)是緊湊的二進(jìn)制格式,需要按規(guī)范解析,不過(guò)我并不打算自己寫解析程序,可以直接用com.sun.tools.classfile.ClassFile,這也是用JAVA寫好處。下面是HelloWorld.class解析后的內(nèi)容(通過(guò)javap -v HelloWorld.class輸出):

public class org.caoym.HelloWorld
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #23            // Hello World
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // org/caoym/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lorg/caoym/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               Hello World
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               org/caoym/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
  public org.caoym.HelloWorld();
    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   Lorg/caoym/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 5: 0
        line 6: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}

可以看到HelloWorld.class 文件中主要包含幾部分:

  1. 常量池(Constant pool)

    常量池中記錄了當(dāng)前類中用到的常量,包括方法名、類名、字符串常量等,如:#3 = String #23, #3為此常量的索引,字節(jié)碼執(zhí)行時(shí)通過(guò)此索引獲取此常量,String為常量類型, 還可以是Methodref (方法引用)、Fieldref(屬性引用)等。

  2. 方法定義

    此處定義了方法的訪問(wèn)方式(如 PUBLIC、STATIC)、字節(jié)碼等,關(guān)于字節(jié)碼的執(zhí)行方式將在后面介紹。

以下為類加載器的部分代碼實(shí)現(xiàn):

/**
 * 虛擬機(jī)的引導(dǎo)類加載器
 */
public class JvmClassLoader {
    // ... 此處省略部分代碼    
    public JvmClass loadClass(String className) throws ClassNotFoundException{
        String fileName = classPath + "/"+className.replace(".", "/")+".class";
        Path path = Paths.get(fileName);
        //如果文件存在,加載文件字節(jié)碼
        //否則嘗試通過(guò)虛擬機(jī)宿主加載指定類,并將加載后的類當(dāng)做 native 類
        if(Files.exists(path)){
             return JvmOpcodeClass.read(path);
        }else{
            return new JvmNativeClass(Class.forName(className.replace("/",".")));
        }
    }
}

類加載器可以加載兩種形式的類:JvmOpcodeClassJvmNativeClass,均繼承自JvmClass。其中JvmOpcodeClass 表示用戶定義的類,通過(guò)字節(jié)碼執(zhí)行,也就是這個(gè)例子中的HelloWorld;JvmNativeClass表示JVM 提供的原生類,可直接調(diào)用原生類執(zhí)行,比如 java.lang.System。這里把所有非項(xiàng)目?jī)?nèi)的類,都當(dāng)做原始類處理,以便簡(jiǎn)化虛擬機(jī)的實(shí)現(xiàn)。

4. 找到入口方法

JVM規(guī)定入口是static public void main(String[]),為了能夠查找指定類的方法,JvmOpcodeClassJvmNativeClass都需要提供getMethod方法, 當(dāng)然 main 方法肯定存在JvmOpcodeClass中:

public class JvmOpcodeClass implements JvmClass{

    private JvmOpcodeClass(ClassFile classFile) throws ConstantPoolException {
        this.classFile = classFile;
        for (Method method : classFile.methods) {
            String name = method.getName(classFile.constant_pool);
            String desc = method.descriptor.getValue(classFile.constant_pool);
            methods.put(name+":"+desc, new JvmOpcodeMethod(classFile, method));
        }
    }

    @Override
    public JvmMethod getMethod(String name, String desc, int flags) throws NoSuchMethodException {
        JvmOpcodeMethod method = methods.get(name+":"+desc);
        //... check method != null
        return method;
    }
}

5. 執(zhí)行非 Native(字節(jié)碼定義的)方法

下圖為以HelloWorldmain()方法的執(zhí)行過(guò)程:

下面將詳細(xì)說(shuō)明。

5.1. 虛擬機(jī)棧

每一個(gè)虛擬機(jī)線程都有自己私有的虛擬機(jī)棧(Java Virtual Machine Stack),用于存儲(chǔ)棧幀。每一次方法調(diào)用,即產(chǎn)生一個(gè)新的棧幀,并推入棧頂,函數(shù)返回后,此棧幀從棧頂推出。以下為 JJvm中虛擬機(jī)棧的部分代碼:

public class Stack {
    //創(chuàng)建新棧并推入棧頂,用于 native 方法調(diào)用
    public StackFrame newFrame() {
        StackFrame frame = new StackFrame(null, null, 0, 0);
        frames.push(frame, 1);
        return frame;
    }
        //創(chuàng)建新棧并推入棧頂,用于 opcode 方法調(diào)用
    public StackFrame newFrame(ConstantPool constantPool,
                               Opcode[] opcodes,
                               int variables,
                               int stackSize) {
        StackFrame frame = new StackFrame(constantPool, opcodes, variables, stackSize);
        frames.push(frame, 1);
        return frame;
    }
    public StackFrame currentFrame(){...} //獲取當(dāng)前正在執(zhí)行的棧幀
    public StackFrame popFrame(){...} //從棧頂退出一個(gè)棧幀
}

5.2. 棧幀

棧幀用于保存當(dāng)前函數(shù)調(diào)用的上下文信息,以下為 JJvm 中棧幀的部分代碼:

public class StackFrame {  
    private int pc=0;  //程序計(jì)數(shù)器
    public StackFrame(ConstantPool constantPool,
                      Opcode[] opcodes,
                      int variables,
                      int stackSize) {
        this.constantPool = constantPool;               //常量池
        this.opcodes = opcodes;                         //當(dāng)前方法的字節(jié)碼
        this.operandStack = new SlotsStack(stackSize);  //操作數(shù)棧
        this.localVariables = new Slots(variables);     //局部變量表
    }
    public Slots<Object> getLocalVariables() {...}      //局部變量表
    public SlotsStack<Object> getOperandStack() {...}   //操作數(shù)棧
    public ConstantPool getConstantPool() {...}         //常量池
    public void setPC(int pc) {...}                     //設(shè)置程序計(jì)數(shù)器
    //設(shè)置方法返回值,一旦設(shè)置,此幀需要被退出棧頂,并將返回值推入上一個(gè)棧幀的操作數(shù)棧
    public void setReturn(Object returnVal, String returnType) {...}  
    public Object getReturn() {...}                     //獲取當(dāng)前方法返回值
    public String getReturnType() {...}                 //獲取當(dāng)前方法返回值類型
    public boolean isReturned() {...}                   //判斷當(dāng)前方法是否已經(jīng)返回
    public int getPC() {...}                            //獲取程序計(jì)數(shù)器
    public int increasePC() {...}                       //遞增程序計(jì)數(shù)器
    public Opcode[] getOpcodes() {...}                  //當(dāng)前方法的字節(jié)碼
}

說(shuō)明:

  • 局部變量表

    保存當(dāng)前方法的局部變量、實(shí)例的this指針和方法的實(shí)參。函數(shù)執(zhí)行過(guò)程中,部分字節(jié)碼會(huì)操作或讀取局部變量表。局部變量表的長(zhǎng)度由編譯期決定。

  • 常量池

    引用當(dāng)前類的常量池。

  • 字節(jié)碼內(nèi)容

    以數(shù)組形式保存的當(dāng)期方法的字節(jié)碼。

  • 程序計(jì)數(shù)器

    記錄當(dāng)前真在執(zhí)行的字節(jié)碼的位置。

  • 操作數(shù)棧

    操作數(shù)棧用來(lái)準(zhǔn)備字節(jié)碼調(diào)用時(shí)的參數(shù)并接收其返回結(jié)果,操作數(shù)棧的長(zhǎng)度由編譯期決定。

5.3. 方法調(diào)用

方法調(diào)用的過(guò)程大致如下:

  1. 新建棧幀,并推入虛擬機(jī)棧。
  2. 將實(shí)例的this和當(dāng)前方法的實(shí)參設(shè)置到棧幀的局部變量表中。
  3. 解釋執(zhí)行方法的字節(jié)碼。

以下為 JJvm 中的部分代碼:

public class JvmOpcodeMethod implements JvmMethod {
    public void call(Env env, Object thiz, Object ...args) throws Exception {
        // 每次方法調(diào)用都產(chǎn)生一個(gè)新的棧幀,當(dāng)前方法返回后,將其棧幀設(shè)置為已返回,BytecodeInterpreter.run會(huì)在檢查到返回后,將棧幀推
        // 出棧,并將返回值(如果有)推入上一個(gè)棧幀的操作數(shù)棧
        StackFrame frame = env.getStack().newFrame(
                classFile.constant_pool,
                opcodes,
                codeAttribute.max_locals,
                codeAttribute.max_stack);

        // Java 虛擬機(jī)使用局部變量表來(lái)完成方法調(diào)用時(shí)的參數(shù)傳遞,當(dāng)一個(gè)方法被調(diào)用的時(shí)候,它的 參數(shù)將會(huì)傳遞至從 0 開(kāi)始的連續(xù)的局部變量表位置
        // 上。特別地,當(dāng)一個(gè)實(shí)例方法被調(diào)用的時(shí)候, 第 0 個(gè)局部變量一定是用來(lái)存儲(chǔ)被調(diào)用的實(shí)例方法所在的對(duì)象的引用(即 Java 語(yǔ)言中的“this”
        // 關(guān)鍵字)。后續(xù)的其他參數(shù)將會(huì)傳遞至從 1 開(kāi)始的連續(xù)的局部變量表位置上。
        Slots<Object> locals = frame.getLocalVariables();
        int pos = 0;
        if(!method.access_flags.is(AccessFlags.ACC_STATIC)){
            locals.set(0, thiz, 1);
            pos++;
        }
        for (Object arg : args) {
            locals.set(pos++, arg, 1);
        }
        //解釋執(zhí)行字節(jié)碼
        BytecodeInterpreter.run(env);
    }
}

5.4. 解釋執(zhí)行字節(jié)碼

字節(jié)碼的執(zhí)行過(guò)程如下:

  1. 獲取棧頂?shù)牡谝粋€(gè)棧幀。
  2. 獲取當(dāng)前棧的程序計(jì)數(shù)器(PC,其默認(rèn)值為0)指向的字節(jié)碼,程序計(jì)數(shù)器+1。
  3. 執(zhí)行上一步獲取的字節(jié)碼,推出操作數(shù)棧的元素,作為其參數(shù),執(zhí)行字節(jié)碼。
  4. 字節(jié)碼返回的值(如果有),重新推入操作數(shù)棧。
  5. 如果操作數(shù)為return等,則設(shè)置棧幀為已返回狀態(tài)。
  6. 如果操作數(shù)為invokevirtual等嵌套調(diào)用其他方法,則創(chuàng)建新的棧幀,并回到第一步。
  7. 如果棧幀已設(shè)置為返回,則將返回值推入上一個(gè)棧幀的操作數(shù)棧,并推出當(dāng)前棧。
  8. 重復(fù)執(zhí)行1~7,直到虛擬機(jī)棧為空。

以下為JJvm中解釋執(zhí)行字節(jié)碼的部分代碼:

public class BytecodeInterpreter {
    
    //執(zhí)行字節(jié)碼
    public static void run(Env env) throws Exception {
        //只需要最外層調(diào)用執(zhí)行棧上操作
        if(env.getStack().isRunning()) return;
        
        StackFrame frame;
        Stack stack = env.getStack();
        stack.setRunning(true);

        while ((frame = stack.currentFrame()) != null){
            //如果棧幀被設(shè)置為返回,則將其返回值推入上一個(gè)棧幀的操作數(shù)棧
            if(frame.isReturned()){
                //原先此處有 bug,多謝 @樂(lè)浩beyond 指出
                StackFrame oldFrame = frame;
                stack.popFrame();
                frame = stack.currentFrame();
                //如果有返回值,則將返回值推入上一個(gè)棧幀的操作數(shù)棧。
                if(frame != null && !"void".equals(oldFrame.getReturnType())){
                    frame.getOperandStack().push(oldFrame.getReturn());
                }
                continue;
            }
            Opcode[] codes = frame.getOpcodes();
            int pc = frame.increasePC();
            codes[pc].call(env, frame);
        }
    }
    // opcode 的實(shí)現(xiàn)
    static {
        //return: 從當(dāng)前方法返回 void。
        OPCODES[Constants.RETURN] = (Env env, StackFrame frame, byte[] operands)->{
            frame.setReturn(null, "void");
        };

        //getstatic: 獲取對(duì)象的靜態(tài)字段值
        OPCODES[Constants.GETSTATIC] = (Env env, StackFrame frame, byte[] operands)->{
            int arg = (operands[0]<<4)|operands[1];
            ConstantPool.CONSTANT_Fieldref_info info
                    = (ConstantPool.CONSTANT_Fieldref_info)frame.getConstantPool().get(arg);
            //靜態(tài)字段所在的類
            JvmClass clazz = env.getVm().findClass(info.getClassName());
            //靜態(tài)字段的值
            Object value = clazz.getField(
                    info.getNameAndTypeInfo().getName(),
                    info.getNameAndTypeInfo().getType(),
                    AccessFlags.ACC_STATIC
                    );

            frame.getOperandStack().push(value, 1);
        };

        //ldc: 將 int,float 或 String 型常量值從常量池中推送至棧頂
        OPCODES[Constants.LDC] = (Env env, StackFrame frame, byte[] operands)->{
            int arg = operands[0];
            ConstantPool.CPInfo info = frame.getConstantPool().get(arg);
            frame.getOperandStack().push(asObject(info), 1);
        };

        //invokevirtual: 調(diào)用實(shí)例方法
        OPCODES[Constants.INVOKEVIRTUAL] = (Env env, StackFrame frame, byte[] operands)->{
            int arg = (operands[0]<<4)|operands[1];

            ConstantPool.CONSTANT_Methodref_info info
                    = (ConstantPool.CONSTANT_Methodref_info)frame.getConstantPool().get(arg);

            String className = info.getClassName();
            String name = info.getNameAndTypeInfo().getName();
            String type = info.getNameAndTypeInfo().getType();

            JvmClass clazz  = env.getVm().findClass(className);
            JvmMethod method = clazz.getMethod(name, type, 0);

            //從操作數(shù)棧中推出方法的參數(shù)
            Object args[] = frame.getOperandStack().dumpAll();
            method.call(env, args[0], Arrays.copyOfRange(args,1, args.length));
        };
        // ... 以下省略
    }
}

6. 執(zhí)行 Native 方法

Native方法的調(diào)用要更簡(jiǎn)單一些,只需調(diào)用已存在的實(shí)現(xiàn)即可,代碼如下:

public class JvmNativeMethod implements JvmMethod {

    private Method method;
    @Override
    public void call(Env env, Object thiz, Object... args) throws Exception {
        StackFrame frame = env.getStack().newFrame();
        Object res = method.invoke(thiz, args);
        //設(shè)置為已返回
        frame.setReturn(res, method.getReturnType().getName());
    }
}

7. 結(jié)束

到目前為止,我們的“剛好夠運(yùn)行 HelloWorld”的 JVM 已經(jīng)完成,完整代碼可在這里下載。當(dāng)然這個(gè)JVM 并不完整,缺少很多內(nèi)容,如類和實(shí)例的初始化、多線程問(wèn)題、反射、GC 等等。我爭(zhēng)取逐步完善JJvm,并奉上更多文章。

下一篇:用Java實(shí)現(xiàn)JVM(二):支持接口、類和對(duì)象

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容