自制 Java 虚拟机






4.92/5 (82投票s)
功能性的 Java 虚拟机 - 可以运行大多数指令的 Java 应用程序。
引言
早在 2004 年,我为了完成计算机科学与工程本科学业,不得不选择一个毕业论文课题。我选择了进程迁移。我们的老师 Mahmud Shahriar Hossain 同意指导我的工作。我的搭档是 Md. Helal Uddin。作为论文的一部分,我需要实现一个 Java 虚拟机。我从那时起就想写一篇关于它的文章。但最终并没有实现。今天(3 月 2 日)是我的生日,我想开始动笔。这个虚拟机也用在我新的项目 Morpheus 中——这是 Silverlight 1.1 的一个原型。从上面的链接下载的研讨会演示文稿展示了 JVM 的工作原理。您也可以从上面的链接查看 JVM 的源代码。请注意,大多数实现的决定可能与其他商业 JVM 实现不符。每当 JVM 规范没有明确说明时,就会采取最简单的方法来节省时间。
Java 虚拟机组件
 
 
类文件结构
Java 虚拟机需要一个由 Java 类集合组成的应用程序。任何类文件的开头都有一个定义的结构,如 JavaClassFileFormat。
struct JavaClassFileFormat { 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_ex *fields; //[fields_count]; u2 methods_count; method_info_ex* methods; //[methods_count]; u2 attributes_count; attribute_info** attributes; //[attributes_count]; };
以下是格式中使用的结构。它们代表类文件中的常量池(类文件中使用的常量值)、字段、方法和属性。稍后我将详细描述它们。
struct cp_info { u1 tag; u1* info; }; struct field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info* attributes; //[attributes_count]; }; struct method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info* attributes; //[attributes_count]; }; struct attribute_info { u2 attribute_name_index; u4 attribute_length; u1* info;//[attribute_length]; };
我们首先将类文件以原始字节形式加载到内存中,然后使用 JavaClass 对象来解析原始字节并识别类文件中的字段、方法、异常表等。JavaClass 类在内存中以结构化的形式表示一个类。它包含一个指向从类文件中加载的原始字节流的指针。
内存中的 Java 类
为了简化将值存储在内存类表示中,我们在这里继承了JavaClassFileFormat。我们必须解析内存中的原始类文件以获取 JavaClassFileFormat 字段的值。class JavaClass: public JavaClassFileFormat { public: JavaClass(void); virtual ~JavaClass(void); public: virtual BOOL LoadClassFromFile(CString lpszFilePath); void SetByteCode(void* pByteCode); BOOL ParseClass(void); BOOL ParseInterfaces(char* &p); BOOL ParseFields(char* &p); BOOL ParseMethods(char* &p); BOOL ParseAttributes(char* &p); BOOL GetConstantPool(u2 nIndex, cp_info& const_pool); BOOL GetStringFromConstPool(int nIndex,CString& strValue); CString GetName(void); CString GetSuperClassName(void); BOOL ParseMethodCodeAttribute(int nMethodIndex, Code_attribute* pCode_attr); int GetMethodIndex(CString strMethodName, CString strMethodDesc, JavaClass* &pClass); int GetFieldIndex(CString strName, CString& strDesc); void SetClassHeap(ClassHeap *pClassHeap){this->m_pClassHeap=pClassHeap;} virtual u4 GetObjectSize(void); virtual u4 GetObjectFieldCount(void); JavaClass* GetSuperClass(void); BOOL CreateObject(u2 index, ObjectHeap *pObjectHeap, Object& object); BOOL CreateObjectArray(u2 index, u4 count, ObjectHeap *pObjectHeap, Object& object); private: size_t m_nByteCodeLength; void *m_pByteCode; u2 m_nObjectFieldsCount; BOOL ParseConstantPool(char* &p); int GetConstantPoolSize(char* p); ClassHeap *m_pClassHeap; };
为此,我们首先将文件加载到内存中,然后调用下一节所示的 ParseClass 方法。
类加载器
由于存在一些可变长度字段,因此无法直接加载结构。所以我们逐个加载值。首先,我们加载 magic 的值,它是一个无符号整数(u4)。它必须是 0xCafeBabe。如果不是,则类文件可能已损坏,或者根本不是 Java 类文件。然后我们加载其他值和结构。要加载结构,我们首先加载计数,然后加载结构。例如,我们首先加载 short(u2)值 constant_pool_count,然后加载该数量的常量池。为了解析,我使用了 getu4(p) 或类似的定义,它只是从 p 开始取 4 个字节并返回无符号整数值。要解析结构,这里使用了单独的方法,如 ParseConstantPool。它接受字节流指针的*引用*并在该类中递增。我这样做只是为了简化。如果我返回总长度并在 ParseClass 方法中递增,那会更具可读性,但管理起来会更困难。
    
    BOOL JavaClass::ParseClass(void ) 
    { 
        //just to be safe 
        if (m_pByteCode==NULL || 
                m_nByteCodeLength < sizeof (JavaClassFileFormat)+20) 
            return FALSE; 
        char *p=( char *)m_pByteCode; 
        magic = getu4(p); p+=4; 
        ASSERT(magic == 0xCAFEBABE); 
            
        if(magic != 0xCAFEBABE)
            return FALSE;
                
        minor_version=getu2(p); p+=2; 
        major_version=getu2(p); p+=2; 
        constant_pool_count=getu2(p); p+=2; 
        if (constant_pool_count>0) 
            ParseConstantPool(p); 
        access_flags=getu2(p); p+=2; 
        this_class=getu2(p); p+=2; 
        super_class=getu2(p); p+=2; 
        interfaces_count=getu2(p); p+=2; 
        if (interfaces_count>0) 
            ParseInterfaces(p); 
        fields_count=getu2(p); p+=2; 
        if (fields_count > 0) 
            ParseFields(p); 
        methods_count = getu2(p);p+=2; 
            
        if (methods_count > 0) 
        { 
            ParseMethods(p); 
        } 
        attributes_count = getu2(p);p+=2; 
            
        if (attributes_count > 0) 
            ParseAttributes(p); 
            
        return 0; 
    }
常量池
Java 类中存储了几个常量值。它在一个池中存储数值、字符串和引用值,这些值在称为“Java 字节码”的机器码中使用。常量池包含 constant_pool_count 个项,以以下结构的顺序列表形式存在。
    struct cp_info
    {
        u1 tag;
        u1* info;
    };
常量池信息结构以一个字节的 tag 信息开始,该信息指示常量池的类型。常量池结构的长度可变,取决于常量的类型。常量池 tag 值可以是一个或多个以下值,具体取决于常量类型。
#define CONSTANT_Integer 3 #define CONSTANT_Float 4 #define CONSTANT_Long 5 #define CONSTANT_Double 6 #define CONSTANT_Utf8 1 #define CONSTANT_String 8 #define CONSTANT_Class 7 #define CONSTANT_Fieldref 9 #define CONSTANT_Methodref 10 #define CONSTANT_InterfaceMethodref 11 #define CONSTANT_NameAndType 12
根据 tag 的值,我们可以将 cp_info 结构转换为此处列出的更精确的结构。
CONSTANT_Integer_info
如果 cp_info 结构中的 tag 值等于 CONSTANT_Integer,则它是一个整数常量。我们可以将 cp_info 结构转换为以下结构。
    struct CONSTANT_Integer_info {
        u1 tag;
        u4 bytes;
    };
此结构不引用任何其他常量。它表示直接的 4 字节整数值。
CONSTANT_Float_info
如果 cp_info 结构中的 tag 值等于 CONSTANT_Float,则它是一个浮点数常量。我们可以将 cp_info 结构转换为以下结构。
    struct CONSTANT_Float_info {
        u1 tag;
        u4 bytes;
    };
这是一个直接值常量,没有任何引用。
CONSTANT_Long_info
如果 cp_info 结构中的 tag 值等于 CONSTANT_Long,则它是一个长整型常量。我们可以将 cp_info 结构转换为以下结构。
    struct CONSTANT_Long_info {
        u1 tag;
        u4 high_bytes;
        u4 low_bytes;
    };
这是一个直接值常量,没有任何引用。它使用两个四字节值来构造 8 字节的长值。
CONSTANT_Long_info
如果 cp_info 结构中的 tag 值等于 CONSTANT_Double,则它是一个双精度浮点数常量。我们可以将 cp_info 结构转换为以下结构。
    struct CONSTANT_Double_info {
        u1 tag;
        u4 high_bytes;
        u4 low_bytes;
    };
这是一个直接值常量,没有任何引用。它使用两个四字节值来构造 8 字节的双精度浮点数值。
CONSTANT_Utf8_info
如果 cp_info 结构中的 tag 值等于 CONSTANT_Utf8,则它是一个 utf8 字符串常量。我们可以将 cp_info 结构转换为以下结构。
struct CONSTANT_Utf8_info { u1 tag; u2 length; u1* bytes;//[length]; };
这是一个直接值常量,没有任何引用。short 值 length 定义了后面跟着 length 个字节的字节数组的长度。
CONSTANT_String_info
如果 cp_info 结构中的 tag 值等于 CONSTANT_String,则它是一个字符串*引用*常量。我们可以将 cp_info 结构转换为以下结构。
    struct CONSTANT_String_info {
        u1 tag;
        u2 string_index;
    };
这是一个引用值常量。short 值 string_index 指向常量池中的 CONSTANT_Utf8_info 索引。
CONSTANT_Class_info
如果 cp_info 结构中的 tag 值等于 CONSTANT_Class,则它是一个类*引用*常量。我们可以将 cp_info 结构转换为以下结构。
    struct CONSTANT_Class_info {
         u1 tag;
           u2 name_index;
    };
这是一个引用值常量。short 值 name_index 指向常量池中的 CONSTANT_Utf8_info 索引,该索引是类的完全限定名称(例如 java/lang/String),其中点被斜杠替换。
CONSTANT_Fieldref_info
如果 cp_info 结构中的 tag 值等于 CONSTANT_Fieldref,则它是一个字段*引用*常量。我们可以将 cp_info 结构转换为以下结构。
    struct CONSTANT_Fieldref_info {
        u1 tag;
        u2 class_index;
        u2 name_and_type_index;
    };
这是一个引用值常量。short 值 class_index 指向常量池中的 CONSTANT_Class_info 索引,而 name_and_type_index 指向常量池中的一个字符串索引,该字符串索引是类的完全限定名称(例如 java/lang/String@valueOf(F)Ljava/lang/String;),其中点被斜杠替换。
CONSTANT_Methodref_info
如果 cp_info 结构中的 tag 值等于 CONSTANT_Methodref,则它是一个方法*引用*常量。我们可以将 cp_info 结构转换为以下结构。
    struct CONSTANT_Methodref_info {
        u1 tag;
        u2 class_index;
        u2 name_and_type_index;
    };
这是一个引用值常量。short 值 class_index 指向常量池中的 CONSTANT_Class_info 索引,而 name_and_type_index 指向常量池中的一个字符串索引,该字符串索引是类的完全限定名称(例如 java/lang/String@valueOf(F)Ljava/lang/String;),其中点被斜杠替换。
CONSTANT_InterfaceMethodref_info
如果 cp_info 结构中的 tag 值等于 CONSTANT_InterfaceMethodref,则它是一个接口方法*引用*常量。我们可以将 cp_info 结构转换为以下结构。
    struct CONSTANT_InterfaceMethodref_info {
        u1 tag;
        u2 class_index;
        u2 name_and_type_index;
    };
这是一个引用值常量。short 值 class_index 指向常量池中的 CONSTANT_Class_info 索引,而 name_and_type_index 指向常量池中的一个字符串索引,该字符串索引是类的完全限定名称(例如 java/lang/String@valueOf(F)Ljava/lang/String;),其中点被斜杠替换。
CONSTANT_NameAndType_info
如果 cp_info 结构中的 tag 值等于 CONSTANT_NameAndType,则它是一个接口方法*引用*常量。我们可以将 cp_info 结构转换为以下结构。
    struct CONSTANT_NameAndType_info {
        u1 tag;
        u2 name_index;
        u2 descriptor_index;
    };
这是一个引用值常量。short 值 name_index 指向常量池中的一个字符串索引,而 descriptor_index 指向常量池中的另一个字符串索引。
解析常量池
在这里,我们设置常量池列表指针的值。当我们需要检索实际值时,我们查看 tag 并直接获取值。
BOOL JavaClass::ParseConstantPool(char* &p) { constant_pool = new cp_info*[constant_pool_count-1]; if(constant_pool == NULL) return FALSE; for(int i=1;i<constant_pool_count;i++) { //We set the constant pointer here constant_pool[i]=(cp_info*)p; //We now calculate constant size. If it is an integer we get size = 5 int size = GetConstantPoolSize(p); p+= size; // If constant type is long or double constant pool takes two entries. // Second entry is not used by virtual machine but kept NULL to walk // constant pool correctly. if(constant_pool[i]->tag == CONSTANT_Long || constant_pool[i]->tag == CONSTANT_Double) { constant_pool[i+1]=NULL; i++; } } return TRUE; }
接口
在类的 interfaces 字段中,有 interfaces_count 个 short(u2)值。每个值都是一个指向 CONSTANT_Class 类型常量池项的*引用*。我们解析它们并将它们存储在我们的内存对象中-
BOOL JavaClass::ParseInterfaces(char* &p) { interfaces = new u2[interfaces_count]; for(int i=0;i<interfaces_count;i++) { interfaces[i] = getu2(p); p+=2; } return TRUE; }
字段
一个类可能包含零个、一个或多个字段。实际数量存储在 fields_count 字段中。一个 field_info 结构的列表紧随该值之后。
struct field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info* attributes;//[attributes_count]; };
short 值 access_flags 描述了允许的字段访问。以下是可能的访问标志值,这些标志也由方法和类共享-
#define ACC_PUBLIC 0x0001 /*Declared public; may be accessed from outside its package. */ #define ACC_PRIVATE 0x0002 /*Declared private; accessible only within the defining class. */ #define ACC_PROTECTED 0x0004 /*Declared protected; may be accessed within subclasses. */ #define ACC_STATIC 0x0008 /*Declared static. */ #define ACC_FINAL 0x0010 /*Declared final; may not be overridden. */ #define ACC_SYNCHRONIZED 0x0020 /*Declared synchronized; invocation is wrapped in a monitor lock. */ #define ACC_NATIVE 0x0100 /*Declared native; implemented in a language other than Java. */ #define ACC_ABSTRACT 0x0400 /*Declared abstract; no implementation is provided. */ #define ACC_STRICT 0x0800 /*Declared strictfp; floating-point mode is FP-strict */
name_index 和 descriptor_index 是对 utf8 字符串类型的两个常量池的引用。attributes 字段定义了字段的属性。属性稍后描述。这是我们在类的原始字节中解析字段的方法-
BOOL JavaClass::ParseFields(char* &p) { fields = new field_info_ex[fields_count]; if(fields == NULL) return FALSE; for(int i=0;i<fields_count;i++) { fields[i].pFieldInfoBase = (field_info*)p; fields[i].access_flags= getu2(p); p+=2; //access_flags fields[i].name_index= getu2(p);p+=2; // fields[i].descriptor_index= getu2(p);p+=2; // fields[i].attributes_count=getu2(p); p+=2; if(fields[i].attributes_count>0) { //skip attributes - we do not need in simple cases for(int a=0;a<fields[i].attributes_count;a++) { u2 name_index=getu2(p); p+=2; //printf("Attribute name index = %d\n", name_index); u4 len=getu4(p);p+=4; p+=len; } } } return TRUE; }
方法
Java 类文件可能包含任意数量的方法。数量存储在类文件结构中的 methods_count 成员中。由于这是一个双字节字段,理论上的上限基本上是 2^16。与字段信息一样,方法信息结构包含访问标志、名称索引、描述符索引和属性。
struct method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info* attributes;//[attributes_count]; };
方法体(如果有)存储在一个名为 Code 的属性中——其中包含实际的“Java 字节码”。这是我们在虚拟机中解析方法的方法-
//TODO: Cashe the findings here BOOL JavaClass::ParseMethods(char* &p) { methods = new method_info_ex[methods_count]; if(methods == NULL) return FALSE; for(int i=0;i<methods_count;i++) { //methods[i] = new method_info_ex; methods[i].pMethodInfoBase=(method_info*)p; methods[i].access_flags= getu2(p); p+=2; //access_flags methods[i].name_index = getu2(p); p+=2; //name_index methods[i].descriptor_index= getu2(p); p+=2; //descriptor_index methods[i].attributes_count=getu2(p); p+=2; CString strName, strDesc; GetStringFromConstPool(methods[i].name_index, strName); GetStringFromConstPool(methods[i].descriptor_index, strDesc); TRACE(_T("Method = %s%s\n"),strName, strDesc); TRACE("Method has total %d attributes\n",methods[i].attributes_count); methods[i].pCode_attr=NULL; if(methods[i].attributes_count>0) { //skip attributes for(int a=0;a<methods[i].attributes_count;a++) { u2 name_index=getu2(p); p+=2; TRACE("Attribute name index = %d\n", name_index); u4 len=getu4(p);p+=4; p+=len; } methods[i].pCode_attr = new Code_attribute; ParseMethodCodeAttribute(i, methods[i].pCode_attr); } } return TRUE; }
在方法结构(以及字段结构中也是如此)的情况下,我使用了 method_info_ex 而不是 method_info 结构。这个扩展结构指向类文件内存字节流中原始方法信息的指针。在这里,除了其他字段,我们还解析 Code 属性。详细信息将在后面的属性部分给出。
属性
在大多数类中,属性占据了文件的大部分空间。类有属性,方法有属性,字段有属性。属性的原始定义如下-
struct attribute_info { u2 attribute_name_index; u4 attribute_length; u1* info;//[attribute_length]; };
attribute_name_index 字段是常量池中字符串类型常量的引用索引。attribute_length 字段是 info 字段的长度——这是一个取决于属性类型/名称的另一个结构。属性可以是常量值、异常表或代码类型。
常量值属性
    struct ConstantValue_attribute {
        u2 attribute_name_index;
        u4 attribute_length;
        u2 constantvalue_index;
    };
Code 属性
它也是一个特定于方法属性。该属性的名称硬编码为“Code”。此属性具有方法的最大堆栈和最大局部变量值。code 字段是可变长度的,由 code_length 定义,并且包含实际的“Java 字节码”。
struct 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; Exception_table* exception_table;//[exception_table_length]; u2 attributes_count; attribute_info* attributes;//[attributes_count]; };
异常表结构
此结构用于定义方法的异常表。异常表根据字节码的程序计数器值或偏移量描述异常处理程序。处理程序代码也是字节码中的一个偏移量。
    struct Exception_table
    {
        u2 start_pc;
        u2 end_pc;
        u2  handler_pc;
        u2  catch_type;
    };
catch_type 字段是对常量池条目的引用,该条目描述了异常的类型——例如,对名为“java/lang/Exception”的类的引用。
Java 指令集
Java 有 200 多条指令。Java 语言文件在编译后会转换为包含字节码指令的类文件。如果我们有一个像这样的方法-
public int mul(int a, int b) { return a * b; }
我们将在字节码属性中得到这个方法,如下所示-(Java 也有类似汇编的表示法,用于将指令表示为人类可读的字节码格式)
  Code Attribute:
    Stack=2, Locals=3, Args_size=3,  Code Length = 4
    Code:
    0:   iload_1
    1:   iload_2
    2:   imul
    3:   ireturn
如果我们按照指令操作,我们会这样进行
0: Push (load) the local variable 1 on stack 1: Push the local variable 2 on stack 3: Pop two values from stack, do an integer multipucation and push the result 4: Return the integer value from stack top.
在我们的虚拟机中,我们需要做的是加载类并按照方法中的指令执行。有创建新对象、调用对象方法的方法。也可以从 Java 方法调用本地方法。有关大多数其他代码(opcodes.h)的详细信息,请参阅源代码,或查阅 Java 虚拟机规范以获取完整列表。
类堆
在虚拟机中,我们必须维护一个堆,用于存储类定义对象。为了简化,我将其实现为一个单独的堆。在这个堆中,我们从文件中加载类并将其存储在堆中。ClassHeap 类负责在内存中维护类堆。
class ClassHeap { CMapStringToPtr m_ClassMap; FilePathManager *pFilePathManager; public: ClassHeap(void); public: virtual ~ClassHeap(void); public: BOOL AddClass(JavaClass* pJavaClass); JavaClass* GetClass(CString strClassName); BOOL LoadClass(CString strClassName, JavaClass *pClass); };
我们使用类名作为键,将 JavaClass 对象指针存储在 m_ClassMap 成员中。
对象堆
对象堆是虚拟机的 RAM。所有对象都在对象堆上创建,其引用可以存储在另一个对象中或存储在堆栈中。任何引用都存储在名为 Variable 的联合类型存储中。类的任何字段都可以使用变量对象来表示。任何内容都可以存储在 Variable 对象中。
union Variable { u1 charValue; u2 shortValue; u4 intValue; f4 floatValue; LONG_PTR ptrValue; Object object; };
对象在堆上创建的详细信息将在后面描述。
虚拟机栈
Java 指令集的设计方式是它可以使用非常有限的寄存器。取而代之的是,它广泛使用其堆栈。JVM 堆栈元素是一个项,无论它是原始类型还是对象类型。只有 long 和 double 类型占用两个堆栈空间。“执行引擎”维护 JVM 堆栈。例如,当执行引擎执行 iadd 指令时,它会从堆栈中弹出两个数值,将它们相加,然后将结果推送到堆栈上。虚拟机最初是空堆栈,在初始线程和初始方法开始执行后,堆栈才会被填充。每个方法指令都允许在堆栈的有限边界内进行操作。编译器在每个方法的 Code 属性中将上限(顶部)设置为 max_stack 字段。下限是前一个方法的顶部+1 堆栈位置。这个堆栈的边界被称为该方法的*堆栈帧*。
堆栈帧
正如我们提到的,JVM 堆栈中每个方法的边界称为“堆栈帧”。每个堆栈帧都为该方法的参数和局部变量保留位置。如果不是静态方法,第一个参数是方法所属类的对象引用(this 参数)。*执行引擎*在帧之间操作,当方法返回值时,它会弹出当前帧中的所有元素(包括 this 引用),并将返回值(如果方法不是 void 返回类型)推送到前一帧顶部的帧上。为了保持实现简单,我使用了一种略有不同的方法。我使用了堆栈的堆栈。每个帧都是堆栈类型,并被推送到 JVM 堆栈上。堆栈在方法调用时增长,在方法返回时收缩。
局部变量
在堆栈帧中,局部变量从零到 max_locals - 1 或更少的位置。如果方法不是静态的,对象占用位置零,其他局部变量跟随它。局部变量使用 putfield 和 getfield 指令访问。
本地方法栈
与虚拟机栈不同,本地方法栈不是由 JVM 维护的。它由本地系统维护。实际上,当本地方法正在执行时,负责管理 Java 线程的虚拟机组件会一直等待,直到本地方法完成并返回。
运行时环境
每个 Java 线程都有自己的帧栈。一个进程中的所有 Java 线程共享公共的类堆和对象堆。这些组件被打包到一个 RuntimeEnvironment 对象中,并在执行引擎组件之间传递。
class RuntimeEnvironment { public: Frame *pFrameStack; ClassHeap *pClassHeap; ObjectHeap *pObjectHeap; };
执行单元
这是 JVM 的主要模块。它解释指令。高级 JVM 可能会使用 JIT 编译器将 Java 指令转换为本地指令。但我没有这样做,因为 JIT 编译器的复杂性。当 JVM 启动时,它通常以初始类名作为参数。我们的 JVM 也将类名作为参数。然后会请求类堆加载该类。然后 JVM 找到其 main 方法(在我的第一个实现中,它可以是 Entry 等任何名称),创建初始堆栈帧,并请求执行引擎开始执行。*执行单元*的核心是 Execute 方法。这是骨架
    u4 ExecutionEngine::Execute(Frame* pFrameStack)
    {
        ASSERT(pFrameStack);
        ASSERT(pFrame);
        Frame* pFrame=&pFrameStack[0];
        DbgPrint(_T("Current Frame %ld Stack start at %ld\n"),
                pFrame-Frame::pBaseFrame,     pFrame->stack-Frame::pOpStack );
        if(pFrame->pMethod->access_flags & ACC_NATIVE)
        {
            ExecuteNativeMethod(pFrame);
            return 0;
        }
        u1 *bc=pFrame->pMethod->pCode_attr->code + pFrame->pc;    
        
        i4 error=0;
        JavaClass *pClass = pFrame->pClass;
        CString strMethod;
        pClass->GetStringFromConstPool(pFrame->pMethod->name_index, strMethod);
        DbgPrint(_T("Execute At Class %s Method %s \n"), pClass->GetName(), strMethod); 
        i4 index=0;
        i8 longVal;
        while(1)
        {
            switch(bc[pFrame->pc])
            {
            case nop: //Do nothing
                pFrame->pc++;
                break;
                
            //Integer Arithmetic 
            case iadd: //96 : Pop two int values from stack add them and push result
                pFrame->stack[pFrame->sp-1].intValue=pFrame->stack[pFrame->sp-1].intValue 
                        + pFrame->stack[pFrame->sp].intValue;    
                pFrame->sp--;
                pFrame->pc++;
                break;
                
            //Method return instructions            
            case ireturn: 
                //172 (0xac) : Pop everything from stack and push return value (int)
                pFrame->stack[0].intValue=pFrame->stack[pFrame->sp].intValue;            
                return ireturn; // here we break the while loop
                break;
                
            // Method invokation Instructions
            // Here actually we do a recursive call to Execute
            // to keep things simple- after the java method return we
            // also return from Execute- some memory waste for simplicity
            case invokevirtual: //182: Invoke a virtual method. 
                // The object reference and parameters are on stack by java instructions
                ExecuteInvoke(pFrame, invokevirtual);
                pFrame->pc+=3;
                break;                                            
            }
            //Instructions that deal with objects
            case _new:// 187 (0xbb)
                ExecuteNew(pFrame);
                pFrame->pc+=3;
                break;
            case putfield: //181 (0xb5): Set field in object from stack top
                PutField(pFrame);
                pFrame->sp-=2;
                pFrame->pc+=3;
                break;
            case getfield: 
                //180 (0xb4) Fetch field from object and push on stack
                GetField(pFrame);
                pFrame->pc+=3;
                break;
        }
        return 0;
    }
在对象堆上创建对象
通常,当执行 new、newarray 或 multinewarray 指令时,JVM 会创建一个对象。当虚拟机创建一个对象时,它首先计算对象的大小。为了计算对象大小,我们首先取类结构中的 fields_count 值,然后将其与父类的 fields_count 值相加,然后继续将父类的父类的 fields_count 相加,依此类推,直到达到最终基类 java.lang.Object。这样,我们就计算出了对象的所有字段,并在其中添加一个用于存储 ClassHeap 中的类指针。现在我们将 sizeof(Variable) 乘以计数,得到对象所需的字节数。然后我们分配所需的字节,并在堆栈顶部以 Variable 对象的形式返回该内存的指针。这是实现。
int ExecutionEngine::ExecuteNew(Frame* pFrame) { pFrame->sp++; u1 *bc=pFrame->pMethod->pCode_attr->code; u2 index=getu2(&bc[pFrame->pc+1]); if(!pFrame->pClass->CreateObject( index, this->pObjectHeap, pFrame->stack[pFrame->sp].object)) return -1; return 0; } BOOL JavaClass::CreateObject(u2 index, ObjectHeap *pObjectHeap, Object& object) { char *cp=(char*)this->constant_pool[index]; ASSERT(cp[0] == CONSTANT_Class); ASSERT(pObjectHeap); if(cp[0] != CONSTANT_Class) return FALSE; u2 name_index=getu2(&cp[1]); CString strClassName; if(!this->GetStringFromConstPool(name_index, strClassName)) return FALSE; JavaClass *pNewClass=this->m_pClassHeap->GetClass(strClassName); if(pNewClass == NULL) return FALSE; object=pObjectHeap->CreateObject(pNewClass); return TRUE; }
设置或获取对象中的值
putfield 指令用于设置一个字段的值(来自堆栈),getfield 指令用于将变量的值加载到堆栈。当执行引擎需要执行 getfield 指令时,它会从堆栈中弹出两个值。一个值是对象指针,另一个是字段位置(零基索引)。这是我的实现
// Gets value or reference from stack and set in object void ExecutionEngine::PutField(Frame* pFrameStack) { u2 nIndex = getu2( &pFrameStack[0].pMethod->pCode_attr->code[pFrameStack[0].pc+1]); Variable obj=pFrameStack[0].stack[pFrameStack[0].sp-1]; Variable value=pFrameStack[0].stack[pFrameStack[0].sp]; Variable *pVarList=this->pObjectHeap->GetObjectPointer(obj.object); pVarList[nIndex+1]=value; } //Gets the value from variable and push on stack void ExecutionEngine::GetField(Frame* pFrame) { //TODO: Bug check for long and double u2 nIndex = getu2( &pFrame->pMethod->pCode_attr->code[pFrame->pc+1]); Variable obj=pFrame->stack[pFrame->sp]; Variable *pVarList=this->pObjectHeap->GetObjectPointer(obj.object); pFrame->stack[pFrame->sp]=pVarList[nIndex+1]; }
调用方法
当执行引擎需要进行方法调用时,它需要创建一个新的“堆栈帧”,并将 pc 或程序计数器设置为方法字节码的第一个字节。在此之前,执行引擎必须保存当前方法的 pc,以便在被调用方法返回后能够恢复方法执行。这是我们的实现。请注意我们如何处理静态方法调用——我们只是在堆栈上没有 this 引用。
void ExecutionEngine::ExecuteInvoke(Frame* pFrameStack, u2 type) { u2 mi=getu2( &pFrameStack[0].pMethod->pCode_attr->code[pFrameStack[0].pc+1]); Variable objectRef = pFrameStack[0].stack[pFrameStack[0].sp]; char *pConstPool = (char *)pFrameStack[0].pClass->constant_pool[mi]; ASSERT(pConstPool[0] == CONSTANT_Methodref); u2 classIndex = getu2(&pConstPool[1]); u2 nameAndTypeIndex = getu2(&pConstPool[3]); //get class at pool index pConstPool = (char *)pFrameStack[0].pClass->constant_pool[classIndex]; ASSERT(pConstPool[0] == CONSTANT_Class); u2 ni=getu2(&pConstPool[1]); CString strClassName; pFrameStack[0].pClass->GetStringFromConstPool( ni, strClassName); JavaClass *pClass=pClassHeap->GetClass(strClassName); pConstPool = (char *)pFrameStack[0].pClass->constant_pool[nameAndTypeIndex]; ASSERT(pConstPool[0] == CONSTANT_NameAndType); method_info_ex method; method.name_index = getu2(&pConstPool[1]); method.descriptor_index = getu2(&pConstPool[3]); method.access_flags = 0; // set later CString strName, strDesc; pFrameStack[0].pClass->GetStringFromConstPool( method.name_index, strName); pFrameStack[0].pClass->GetStringFromConstPool( method.descriptor_index, strDesc); JavaClass *pVirtualClass=pClass; int nIndex=pClass->GetMethodIndex(strName, strDesc, pVirtualClass); memset(&pFrameStack[1],0,sizeof(pFrameStack[1])); pFrameStack[1].pMethod = &pClass->methods[nIndex]; method.access_flags = getu2((char *)pFrameStack[1].pMethod); if( ACC_SUPER & method.access_flags) { pFrameStack[1].pClass = pVirtualClass->GetSuperClass(); } else { pFrameStack[1].pClass=pVirtualClass; } int params=GetMethodParametersStackCount(strDesc)+1; //invokestatic - there is no this pointer if(type==invokestatic) params--; // else invokevirtual has this pointer int nDiscardStack =params; if(pFrameStack[1].pMethod->access_flags & ACC_NATIVE) { } else { nDiscardStack+=pFrameStack[1].pMethod->pCode_attr->max_locals; } pFrameStack[1].stack = &Frame::pOpStack[pFrameStack->stack-Frame::pOpStack+pFrameStack[0].sp-params+1]; pFrameStack[1].sp=nDiscardStack-1; this->Execute(&pFrameStack[1]); //if returns then get on stack if(strDesc.Find(_T(")V")) < 0) { nDiscardStack--; } //Before we return to caller make the stack of caller right pFrameStack[0].sp-=nDiscardStack; }
调用本地方法
在 Java 类中,方法可以标记为 native-
public class Test { public native void Print(string message); }
在字节码中,ACC_NATIVE 设置在 method_info 结构的 access_flags 字段中。我们在此处做出如下决定
if(pFrame->pMethod->access_flags & ACC_NATIVE) { ExecuteNativeMethod(pFrame); return 0; }
每个本地方法通常都有一个固定的预定义原型。这是我们 JVM 的类型定义
    typedef Variable (*pNativeMethod)(RuntimeEnvironment* pRuntimeEnvironment);
这是我们在 JVM 中处理本地方法的方式
    u4 ExecutionEngine::ExecuteNativeMethod(Frame* pFrameStack)
    {
        ASSERT(pFrameStack);
        
        ASSERT(pFrame->pMethod->access_flags & ACC_NATIVE);
        Frame* pFrame=&pFrameStack[0];
        JavaClass *pClass = pFrame->pClass;
        CString strClassName, strMethod, strDesc, strSignature;
        strClassName=pClass->GetName();
        pClass->GetStringFromConstPool(
            pFrame->pMethod->name_index, strMethod);
        pClass->GetStringFromConstPool(
            pFrame->pMethod->descriptor_index, strDesc);
        DbgPrint(_T("Execute At Class %s Method %s%s  \n")
            ,strClassName , strMethod, strDesc);
        strSignature=strClassName+_T("@")+strMethod+strDesc;
        pNativeMethod pNativeMethod=GetNativeMethod(strSignature);
        RuntimeEnvironment rte;
        rte.pFrameStack=pFrameStack;
        rte.pClassHeap= pClassHeap;
        rte.pObjectHeap= pObjectHeap;
        if(pNativeMethod == NULL)
        {
            // what should I do here??
            // System Panic??
            ASSERT(FALSE);
            return -1;
        }
        else
        {
            //Here we go native
            Variable retVal = pNativeMethod(&rte);
            //if returns then get on stack    
            if(strDesc.Find(_T(")V")) < 0)
            {
                pFrame->stack[0]=retVal;
            }
        }
        return 0;
    }这是 Test 类中 Print 方法的实现 
    //Signature: _T("Test@Print(Ljava/lang/String;)V")
     Variable Print(RuntimeEnvironment* pRuntimeEnvironment)
    {
        Variable returnVal;
        Frame *pFrame=&pRuntimeEnvironment->pFrameStack[0];
        Object object=pFrame->stack[pFrame->sp].object;
        Variable *pVar
            =pRuntimeEnvironment->pObjectHeap->GetObjectPointer(object);
        if(pVar)
        {
            CString *pString = (CString *)pVar[1].ptrValue;
            if(pString)    wprintf(_T("%s"),*pString);
        }
        returnVal.intValue=0;
        return returnVal;    
    }
本地方法负责正确操作堆栈。Java 指令在此处超出范围。这里一切都在真实机器上运行。因此,我们从堆栈中弹出字符串类型的对象引用,将其转换为 CString 对象,并执行我们本地的控制台打印。通过这种方式,我们可以处理任何本地操作,如创建新窗口、绘图或进行网络操作。所有这些都在 Morpheus 项目的实现中完成。
垃圾收集器
Java 语言没有内存释放机制。因此,当某些对象超出范围、不再需要或不再被应用程序引用时,JVM 必须负责释放内存。为此,JVM 可以采取多种策略,例如引用计数、标记-清除等。我使用了标记-清除方法,因为它简单准确。我们从堆栈开始。我们标记从堆栈引用中引用的每个对象。然后,我们标记所有被已标记对象引用的对象,依此类推,递归进行。标记操作完成后,我们就知道哪些对象是连接的,哪些对象已超出范围。然后,我们逐个处理每个对象,并从堆中释放其内存。在此之前,我们必须调用该对象的 finalize 方法来执行该对象本身编程上所需的任何清理。
结论
目前关于如何实现一个简单的 JVM 就说到这里。这里介绍的 JVM 是一个非常有限的实现——尽管支持大多数 Java 指令。它在库和本地接口方面存在严重不足。请参阅上面下载的研讨会演示文稿,了解 JVM 的可视化描述以及 JVM 中指令的执行方式。最佳的演示文稿全屏视图是通过按空格键切换到下一张幻灯片。我忙于实现 Morpheus 项目,并希望在此基础上推出新 JVM,该 JVM 具有新的窗口子系统实现,具有 Windows Vista 和 Office 2007 的外观和感觉(好吧,这在技术上不是什么大事,但对于用户界面设计来说很好),哦,还有 .NET 虚拟机系统,并支持 Morpheus for .NET。 .NET VES 类似,除了复杂的联机结构在解码时确实很麻烦。所有这些事情我都因为我喜欢做而做——所以我不会试图创建一个零 bug 的系统。当它对“Hello World ++”应用程序似乎工作时,我就离开了。


