自制 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 ++”应用程序似乎工作时,我就离开了。