65.9K
CodeProject 正在变化。 阅读更多。
Home

自制 Java 虚拟机

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (82投票s)

2008 年 3 月 2 日

GPL3

17分钟阅读

viewsIcon

212021

downloadIcon

6263

功能性的 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_indexdescriptor_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 堆栈元素是一个项,无论它是原始类型还是对象类型。只有 longdouble 类型占用两个堆栈空间。“执行引擎”维护 JVM 堆栈。例如,当执行引擎执行 iadd 指令时,它会从堆栈中弹出两个数值,将它们相加,然后将结果推送到堆栈上。虚拟机最初是空堆栈,在初始线程和初始方法开始执行后,堆栈才会被填充。每个方法指令都允许在堆栈的有限边界内进行操作。编译器在每个方法的 Code 属性中将上限(顶部)设置为 max_stack 字段。下限是前一个方法的顶部+1 堆栈位置。这个堆栈的边界被称为该方法的*堆栈帧*。

堆栈帧

正如我们提到的,JVM 堆栈中每个方法的边界称为“堆栈帧”。每个堆栈帧都为该方法的参数和局部变量保留位置。如果不是静态方法,第一个参数是方法所属类的对象引用(this 参数)。*执行引擎*在帧之间操作,当方法返回值时,它会弹出当前帧中的所有元素(包括 this 引用),并将返回值(如果方法不是 void 返回类型)推送到前一帧顶部的帧上。为了保持实现简单,我使用了一种略有不同的方法。我使用了堆栈的堆栈。每个帧都是堆栈类型,并被推送到 JVM 堆栈上。堆栈在方法调用时增长,在方法返回时收缩。

局部变量

在堆栈帧中,局部变量从零到 max_locals - 1 或更少的位置。如果方法不是静态的,对象占用位置零,其他局部变量跟随它。局部变量使用 putfieldgetfield 指令访问。

本地方法栈

与虚拟机栈不同,本地方法栈不是由 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;
    }

在对象堆上创建对象

通常,当执行 newnewarraymultinewarray 指令时,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 ++”应用程序似乎工作时,我就离开了。

© . All rights reserved.