[TOC]
无关性的基石
代码编译的结果从本地机器码编程字节码,是存储格式发展的一小步,却是编程语言发展的一大步。Java 在诞生之初,曾经有一个著名的口号“一次编译,到处运行”,这句话充分表达了软件开发人员对冲破平台界限的渴求。
各种不同平台的虚拟机都统一使用的存储格式 —— 字节码,是构成平台无关性的基石。虚拟机的另外一种特性是语言无关性,目前已经出现了一大批在 Java 虚拟机之上运行的语言,比如 Scala、Clojure、Groovy 等。实现语言无关特性的基础仍然是虚拟机和字节码存储格式,Java 虚拟机不和包括 Java 在内的任何语言绑定,它只与 class 文件这种特定的二进制文件格式关联。
Class文件的结构
Class 文件是一组以 8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件中,中间没有添加任何分隔符。
Class 文件格式采用一种类似于 C 语言结构体的伪结构来存储数据,这种伪结构只有两种数据类型:无符号数和表。无符号数属于基本的数据类型,以 u1、u2、u4、u8 分别代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数,可以用来描述数字、索引引用、数量值或者 UTF8 编码构成的字符串。
表是由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性地以 _info 结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表,它由以下数据项构成:
魔数和版本号
Class 文件的头 4 个字节称为魔数,其唯一作用是确定该文件是否是一个能被虚拟机接受的 Class 文件,其值固定为 0xCAFEBABE。
紧接着魔数的 4 个字节是 Class 文件的版本号,第 5、6 字节是次版本号 minor version,第 7、8 字节是主版本号 major version。jdk7 的主版本号是 51。
常量池
紧接着主次版本号之后的是常量池,常量池可以认为是 Class 文件中的资源仓库,它是 Class 文件中与其他项目关联最多的数据类型。
常量池的入库放置一个 u2 类型的数据,表示常量池容量计数值。与 Java 中语言习惯不同的是,这个容量计数从 1 而非从 0 开始。如果常量池容量为十六进制的 0x0016,即十进制的 22,那么表示常量池里有 21 项数据,索引值范围是 1 到 21。设计者将第 0 项常量空出来,是由特殊考虑的,方便在特定情况下表达“不引用任何一个常量池项目”的含义。Class 文件结构中,只有常量池的容量计数从 1 开始,其他都是从 0 开始。
常量池中主要存放两类数据:字面量和符号引用。字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 类型的常量值等。而符号引用则属于编译原理方面的概念,包括以下三类常量:
-
类和接口的全限定名;
-
字段的名称和描述符;
-
方法的名称和描述符。
常量池中的每一项常量都是一个表,JDK1.7 一共支持 14 种常量类型:
其中每一项常量的结构组成为:
访问标志
在常量池结束之后,紧接着的 2 个字节代表访问标志 access flags,用于识别一些类或接口级别的访问信息,比如:这个 Class 是类还是接口?是否 public?如果是类的话,是否被声明为 final?具体标志见下表:
类索引、父类索引和接口索引集合
Class 文件中由这三项数据来确定这个类的继承关系。由于 Java 不允许多重继承,因此父类索引也只有一个,类索引和父类索引使用两个 u2 类型的索引值表示,各自执行一个类型为 CONSTANT_Class_info 的类描述符常量。对于接口索引集合,入口的第一项 u2 类型数据表示索引表的容量,也就是接口数量,其他 u2 类型数据同样指向类描述符常量。
字段表集合
字段表结构
字段表 field_info 用于描述接口或类中声明的变量,包括类级变量和实例级变量,但不包括方法内部声明的局部变量。在 Java 中描述一个字段的信息有哪些呢?
-
字段的作用域:public、private、protected
-
实例级变量还是类级变量:static
-
可变性:final
-
可见性:volatile
-
可否序列化:transient
-
字段数据类型
-
字段名称
字段表结构如下图所示,第一项数据是 access_flags,与类中的访问标志类似。紧随其后的是两个索引值:name_index、descriptor_index,分别表示字段的简单名称以及字段和方法的描述符。
描述符
这里解释一下,什么是简单名称、描述符,以及前面提到的“全限定名”?全限定名和简单名称好理解,比如 com/apache/util/TestClass 是类的全限定名,就是把类全名里的 . 换成了 /,结尾加上分号 ; 用于分割多个连续的全限定名。简单名称是没有类型和参数修饰的方法或字段名称,比如 int m(),其简单名称就是 m。
相对于全限定名和简单名称来说,方法和字段的描述符更复杂些,因为是包含的信息更多:字段类型、方法参数和返回值类型。基本数据类型以及 Void 类型都用一个大写字符标识,对象类型用 L 加对象的全限定名标识,对于数组每个维度使用一个 [ 来表示,二维数组就是 [[,一个整型数组 int[] 会被记录为 [I。
当使用描述符来描述方法的时候,按照先参数列表后返回值的顺序表示,参数列表按照顺序放在 () 之内。比如 void inc(),描述符就是 ()V;java.lang.String toString(int v) 的描述符是 “(I)Ljava/lang/String;”。
字段表访问标志
字段属性表集合
字段表最后是属性表集合,多数时候为空,如果自动是常量,则用来存放常量值信息,即ConstantValue类型的属性表。
在定义field字段的过程中,我们有时候会很自然地对field字段直接赋值,如下所示:
public static final int MAX=100;
public int count=0;
对于虚拟机而言,上述的两个field字段赋值的时机是不同的:
1.对于非静态(即无static修饰)的field字段的赋值将会出现在实例构造方法()中
2.对于静态的field字段,有两个选择:1、在静态构造方法()中进行;2 、使用ConstantValue属性进行赋值
Sun javac编译器对于静态field字段的初始化赋值策略
目前的Sun javac编译器的选择是:如果使用final和static同时修饰一个field字段,并且这个字段是基本类型或者String类型的,那么编译器在编译这个字段的时候,会在对应的field_info结构体中增加一个ConstantValue类型的结构体,在赋值的时候使用这个ConstantValue进行赋值;如果该field字段并没有被final修饰,或者不是基本类型或者String类型,那么将在类构造方法()中赋值。
对于上述的public static final init MAX=100; javac编译器在编译此field字段构建field_info结构体时,除了访问标志、名称索引、描述符索引外,会增加一个ConstantValue类型的属性表。
方法表集合
方法表结构
class 文件中对方法的描述和对字段的描述格式一样,依次包括:访问标志、名称索引、描述符索引、属性表集合。方法里的代码位于属性表中 Code 属性里,属性表是 class 文件中最具扩展性的数据项。
访问标志(access_flags):
method_info结构体最前面的两个字节表示的访问标志(access_flags),记录这这个方法的作用域、静态or非静态、可变性、是否可同步、是否本地方法、是否抽象等信息,实际上不止这些信息,我们后面会详细介绍访问标志这两个字节的每一位具体表示什么意思。
名称索引(name_index):
紧跟在访问标志(access_flags)后面的两个字节称为名称索引,这两个字节中的值指向了常量池中的某一个常量池项,这个方法的名称以UTF-8格式的字符串存储在这个常量池项中。如public void methodName(),很显然,“methodName”则表示着这个方法的名称,那么在常量池中会有一个CONSTANT_Utf8_info格式的常量池项,里面存储着“methodName”字符串,而mehodName()方法的方法表中的名称索引则指向了这个常量池项。
描述索引(descriptor_index):
描述索引表示的是这个方法的特征或者说是签名,一个方法会有若干个参数和返回值,而若干个参数的数据类型和返回值的数据类型构成了这个方法的描述,其基本格式为: (参数数据类型描述列表)返回值数据类型 。我们将在后面继续讨论。
属性表(attribute_info)集合:
这个属性表集合非常重要,方法的实现被JVM编译成JVM的机器码指令,机器码指令就存放在一个Code类型的属性表中;如果方法声明要抛出异常,那么异常信息会在一个Exceptions类型的属性表中予以展现。Code类型的属性表可以说是非常复杂的内容,也是本文最难的地方。
方法访问标志
名称索引和描述符索引----一个方法的签名
紧接着访问标志(access_flags)后面的两个字节,叫做名称索引(name_index),这两个字节中的值是指向了常量池中某个常量池项的索引,该常量池项表示这这个方法名称的字符串。
方法描述符索引(descrptor_index)是紧跟在名称索引后面的两个字节,这两个字节中的值跟名称索引中的值性质一样,都是指向了常量池中的某个常量池项。这两个字节中的指向的常量池项,是表示了方法描述符的字符串。
所谓的方法描述符,实质上就是指用一个什么样的字符串来描述一个方法,方法描述符的组成如下图所示:
特征签名
特征签名是指的方法的参数列表的描述,不包括返回值。
对于一个方法的重载(同一个类中,方法的简单名称相同,但是参数不同),要求特征签名不同,即方法参数不同才能编译通过。
假如参数相同,但是返回值不同,那也是不能重载,不能编译通过的。
返回值不参与是否可以重载的判断。
但是描述符是参数+返回值都包括的。
方法属性表集合
属性表集合记录了某个方法的一些属性信息,这些信息包括:
- 这个方法的代码实现,即方法的可执行的机器指令
- 这个方法声明的要抛出的异常信息
- 这个方法是否被@deprecated注解表示
- 这个方法是否是编译器自动生成的
属性表集合
属性表是一个很常见的数据项,在 class 文件、字段表、方法表里都可以携带自己的属性表,用于描述某些场景专有的信息。
与 Class 文件中其他数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松些,不再要求各个属性表具有严格顺序。只要不与已有属性名重复,任何人实现的编译器都可以往属性表中写入自定义信息,Java 虚拟机运行时会忽略掉它不认识的属性。
Java 虚拟机规范 (Java SE 7) 中定义了 21 种属性,属性表里包含三项数据:指向属性名常量的索引值、属性值长度、属性值。
属性表结构如下图:
虚拟机中预定义属性列表见下图:
Code属性
Code类型的属性表(attribute_info)可以说是class文件中最为重要的部分,因为它包含的是JVM可以运行的机器码指令,JVM能够运行这个类,就是从这个属性中取出机器码的。除了要执行的机器码,它还包含了一些其他信息,如下所示
Code属性表结构体解释
1.attribute_name_index,属性名称索引,占有2个字节,其内的值指向了常量池中的某一项,该项表示字符串“Code”;
- attribute_length,属性长度,占有 4个字节,其内的值表示后面有多少个字节是属于此Code属性表的;
- max_stack,操作数栈深度的最大值,占有 2 个字节,在方法执行的任意时刻,操作数栈都不应该超过这个值,虚拟机的运行的时候,会根据这个值来设置该方法对应的栈帧(Stack Frame)中的操作数栈的深度;
- max_locals,最大局部变量数目,占有 2个字节,其内的值表示局部变量表所需要的存储空间大小;
- code_length,机器指令长度,占有 4 个字节,表示跟在其后的多少个字节表示的是机器指令;
- code,机器指令区域,该区域占有的字节数目由 code_length中的值决定。JVM最底层的要执行的机器指令就存储在这里;
- exception_table_length,显式异常表长度,占有2个字节,如果在方法代码中出现了try{} catch()形式的结构,该值不会为空,紧跟其后会跟着若干个exception_table结构体,以表示异常捕获情况;
- exception_table,显式异常表,占有8 个字节,start_pc,end_pc,handler_pc中的值都表示的是PC计数器中的指令地址。exception_table表示的意思是:如果字节码从第start_pc行到第end_pc行之间出现了catch_type所描述的异常类型,那么将跳转到handler_pc行继续处理。
- attribute_count,属性计数器,占有 2 个字节,表示Code属性表的其他属性的数目
- attribute_info,表示Code属性表具有的属性表,它主要分为两个类型的属性表:“LineNumberTable”类型和“LocalVariableTable”类型。
“LineNumberTable”类型的属性表记录着Java源码和机器指令之间的对应关系
“LocalVariableTable”类型的属性表记录着局部变量描述
Code属性表的组成部分:
机器指令----code:
目前的JVM使用一个字节表示机器操作码,即对JVM底层而言,它能表示的机器操作码不多于2的 8 次方,即 256个。class文件中的机器指令部分是class文件中最重要的部分,并且非常复杂,本文的重点不止介绍它,我将专门在一片博文中讨论它,敬请期待。
异常处理跳转信息—exception_table:
如果代码中出现了try{}catch{}块,那么try{}块内的机器指令的地址范围记录下来,并且记录对应的catch{}块中的起始机器指令地址,当运行时在try块中有异常抛出的话,JVM会将catch{}块对应懂得其实机器指令地址传递给PC寄存器,从而实现指令跳转;
Java源码行号和机器指令的对应关系—LineNumberTable属性表:
编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,如果抛出异常,JVM可以根据这个对应关系,抛出异常信息,告诉我们我们的源码的多少行有问题,方便我们定位问题。这个信息不是运行时必不可少的信息,但是默认情况下,编译器会生成这一项信息,如果你项取消这一信息,你可以使用-g:none 或-g:lines来取消或者要求设置这一项信息。如果使用了-g:none来生成class文件,class文件中将不会有LineNumberTable属性表,造成的影响就是 将来如果代码报错,将无法定位错误信息报错的行,并且如果项调试代码,将不能在此类中打断点(因为没有指定行号。)
局部变量表描述信息----LocalVariableTable属性表:
局部变量表信息会记录栈帧局部变量表中的变量和java源码中定义的变量之间的关系,这个信息不是运行时必须的属性,默认情况下不会生成到class文件中。你可以根据javac指令的-g:none或者-g:vars选项来取消或者设置这一项信息。
它有什么作用呢? 当我们使用IDE进行开发时,最喜欢的莫过于它们的代码提示功能了。如果在项目中引用到了第三方的jar包,而第三方的包中的class文件中有无LocalVariableTable属性表的区别如下所示:
Exceptions属性
有些方法在定义的时候,会声明该方法会抛出什么类型的异常,如下定义一个Interface接口,它声明了sayHello()方法,抛出Exception异常:
如上图所示,Exceptions类型的属性表(attribute_info)结构体由一下元素组成:
- 属性名称索引(attribute_name_index):占有 2个字节,其中的值指向了常量池中的表示"Exceptions"字符串的常量池项;
- 属性长度(attribute_length):它比较特殊,占有4个字节,它的值表示跟在其后面多少个字节表示异常信息;
- 异常数量(number_of_exceptions):占有2 个字节,它的值表示方法声明抛出了多少个异常,即表示跟在其后有多少个异常名称索引;
- 异常名称索引(exceptions_index_table):占有2个字节,它的值指向了常量池中的某一项,该项是一个CONSTANT_Class_info类型的项,表示这个异常的完全限定名称;
LineNumberTable属性
该属性用于描述 Java 源码行号和字节码行号之间的对应关系,它并不是运行时必须属性,但默认会生成到 class 文件中。在 javac 中分别使用 -g:none 和 -g:lines 选项来取消或要求生成这项信息。如果没有 LineNumberTable 属性,对程序运行的影响就是,当抛出异常时堆栈中不会显示出错的行号,调试时也无法按照源码行来断点。
LineNumberTable属性表是作为Code属性表的属性存在的
LineNumberTable属性表结构如下图
属性表结构里主要有两个数据:长度字段 line_number_table_length、行号对照表 line_number_info,line_number_info 表包括了两个 u2 类型的数据项:start_pc、line_number,前者是字节码行号,后者是 Java 源码行号
编译器在将java源码编译成class文件时,会将源码中的语句行号跟编译好的机器指令关联起来,这样的class文件加载到内存中并运行时,如果抛出异常,JVM可以根据这个对应关系,抛出异常信息,告诉我们我们的源码的多少行有问题,方便我们定位问题。这个信息不是运行时必不可少的信息,但是默认情况下,编译器会生成这一项信息,如果你项取消这一信息,你可以使用-g:none 或-g:lines来取消或者要求设置这一项信息。如果使用了-g:none来生成class文件,class文件中将不会有LineNumberTable属性表,造成的影响就是 将来如果代码报错,将无法定位错误信息报错的行,并且如果项调试代码,将不能在此类中打断点(因为没有指定行号。)
LocalVariableTable属性
LocalVariableTable属性表结构如下图
local_variable_info 描述了栈帧与源码中局部变量的关联,结构如下所示:
start_pc 和 length 分别代表了局部变量生命周期开始的字节码偏移量及其作用范围的覆盖长度,两者结合起来就是该局部变量在字节码之中的作用域范围。
name_index 和 descriptor_index 都是常量池中 CONSTANT_utf8_info 型常量的索引,分别代表了局部变量的名称以及描述符。index 是局部变量在栈帧局部变量表 slot 中的位置,当代表的数据类型为 64 位时,占用的 slot 为 index 和 index+1 两个。
在 JDK1.5 引入泛型后,多了一个姐妹属性 LocalVariableTypeTable,和 LocalVariableTable 很像,仅仅是把字段描述符 descriptor_index 换成了特征签名 signature。对于非泛型类型来说,描述符和特征签名是一致的,由于描述符中泛型的参数类型被擦除,描述符不能准确描述泛型类型了,因此出现了 LocalVariableTypeTable。
局部变量表信息会记录栈帧局部变量表中的变量和java源码中定义的变量之间的关系,这个信息不是运行时必须的属性,默认情况下不会生成到class文件中。你可以根据javac指令的-g:none或者-g:vars选项来取消或者设置这一项信息。
它有什么作用呢? 当我们使用IDE进行开发时,最喜欢的莫过于它们的代码提示功能了。如果在项目中引用到了第三方的jar包,而第三方的包中的class文件中有无LocalVariableTable属性表的区别如下所示:
ConstantValue属性
ConstantValue 属性的作用是通知虚拟机自动为静态变量赋值,只有 static 关键字修饰的类变量才可以使用这些属性。非 static 变量是在实例构造器方法中进行赋值的,而类变量既可以在类构造器中赋值,也可以使用 ConstantValue 属性赋值。目前 sun javac 编译器会把 final static 修饰的基本类型或 String 变量放在 ConstantValue 属性里,其他使用类构造器赋值,不过 Java 虚拟机规范只是规定了 ConstantValue 属性只能存放 static 变量,并不要求是 final 的。
ConstantValue 属性结构如下图所示:
字节码指令
字节码指令简介
Java 虚拟机的指令由一个字节长度的代表某种特定含义的数字(称作操作码,OpsCode)以及紧随其后的零到多个参数构成。由于 Java 指令是面向操作数栈,而非寄存器的架构,所以大多数指令都不包含操作数,只有一个操作码。
字节码与数据类型
在 Java 虚拟机指令集中,大多数指令都包含了操作对应的数据类型信息。例如,iload 是从局部变量表中加载 int 数据到操作数栈,而 fload 加载的是 float 类型的数据。对于大多数与数据类型相关的指令,它们的操作码助记符里都有特殊的字符来表明专门为哪种数据类型服务:i 代表 int 类型数据,l 代表 long,s 代表 short,b 代表 byte,c 代表 char,f 代表 float,d 代表 double,a 代表 reference。
由于 Java 虚拟机的操作码长度只有一个字节,所以包含了数据类型的操作码就为指令设计带来了巨大的压力:如果每种与数据类型相关的指令,都支持所有数据类型的话,那么指令数量就会超出一个字节所能表示的范围了。
下表列出了 Java 虚拟机所支持的指令集与数据类型之间的关系,从中可以看出大部分指令都没有支持 byte、char、short,甚至没有指令支持 boolean。编译器会在编译时,将 byte、short 类型的数据带符号扩展为 int 类型数据,将 boolean、char 类型数据零位扩展至 int 类型数据。因此,大多数对于 byte、short、boolean、char 类型数据的操作,实际上都是使用 int 类型作为运算类型。
指令分类
Java 虚拟机中主要有以下几种指令:
- 加载和存储指令:用于将数据在栈帧中的局部变量表和操作数栈直接来回传输。包括以下几类:
- 将局部变量加载到操作数栈:iload、iload_n、lload、lload_n、fload、fload_n、dload、dload_n、aload、aload_n
- 将数值从操作数栈存储到局部变量表:istore、istore_n、lstore、lstore_n、fstore、fstore_n、dstore、dstore_n、astore、astore_n
- 将一个常量加载到操作数栈的指令包括有 bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_i、lconst_l、fconst_f、dconst_d
- 扩充局部变量表的访问索引的指令:wide
- 运算指令
- 加法指令:iadd、ladd、fadd、dadd
- 减法指令:isub、lsub、fsub、dsub
- 乘法指令:imul、lmul、fmul、dmul
- 除法指令:idiv、ldiv、fdiv、ddiv
- 求余指令:irem、lrem、frem、drem
- 取反指令:ineg、lneg、fneg、dneg
- 位移指令:ishl、ishr、iushr、lshl、lshr、lushr
- 按位或指令:ior、lor
- 按位与指令:iand、land
- 按位异或指令:ixor、lxor
- 局部变量自增指令:iinc
- 比较指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp
- 类型转换指令
- Java虚拟机对于宽化类型转换直接支持,并不需要指令执行。
- 窄化类型转换指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。窄化类型转换很可能会造成精度丢失。
- 对象创建与操作指令
- 创建类实例的指令:new
- 创建数组的指令:newarray,anewarray,multianewarray
- 访问类字段(static字段,或者称为类变量)和实例字段(非static字段,或者成为实例变量)的指令:getfield、putfield、getstatic、putstatic
- 把一个数组元素加载到操作数栈的指令:baload、caload、saload、iaload、laload、faload、daload、aaload
- 将一个操作数栈的值储存到数组元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore
- 取数组长度的指令:arraylength
- 检查类实例类型的指令:instanceof、checkcast
- 控制转移指令
- 条件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt, if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne
- 复合条件分支:tableswitch、lookupswitch
- 无条件分支:goto、goto_w、jsr、jsr_w、ret
- 操作数栈管理指令
- 将栈顶元素出栈:pop、pop2
- 赋值栈顶元素再重新压入栈顶:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
- 栈顶两个元素互换:swap
- 方法调用和返回指令
- invokevirtual指令用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式。
- invokeinterface指令用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。
- invokespecial指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。
- invokestatic指令用于调用类方法(static方法)
- invokedynamic指令用于调用以绑定了invokedynamic指令的调用点对象(call site object)作为目标的方法。调用点对象是一个特殊的语法结构,当一条invokedynamic指令首次被Java虚拟机执行前,Java虚拟机将会执行一个引导方法(bootstrap method)并以这个方法的运行结果作为调用点对象。因此,每条invokedynamic指令都有独一无二的链接状态,这是它与其他方法调用指令的一个差异。
- 方法返回指令则是根据返回值的类型区分的,包括:ireturn、lreturn、freturn、dreturn、areturn、return(返回 void)。
-
显示抛出异常指令 athrow
-
同步指令:monitorenter 和 monitorexit。
公有设计与私有实现
Java 虚拟机规范描绘了 Java 虚拟机应有的共同程序存储格式:Class 文件格式以及字节码 指令集。这些内容与硬件、操作系统以及具体的虚拟机实现之间是完全独立的,Java 虚拟机实现必须能够读取 Class 文件并精确实现包含在其中的 Java 虚拟机代码的语义。
一个优秀的 Java 虚拟机实现,在满足虚拟机规范的约束下,对具体实现做出修改和优化也是可行的,并且虚拟机规范明确鼓励实现者这样做。虚拟机实现者可以利用这种伸缩性,来让 Java 虚拟机获得更高的性能、更低的内存消耗或更好的可移植性,选择哪种特性取决于 Java 虚拟机实现的目标和关注点是什么。虚拟机实现方式主要有以下两种:
-
将输入的 Java 虚拟机代码在加载或执行时翻译成另一种虚拟机的指令集。
-
将输入的 Java 虚拟机代码翻译成宿主机 CPU 的本地指令集(JIT 代码生成技术)。
参考资料
https://blog.csdn.net/luanlouis/article/details/40301985