[译]深入理解JVM Understanding JVM Internals
转载:
英文原版地址:http://www.cubrid.org/blog/dev-platform/understanding-jvm-internals/
翻不了墙的可以看这个英文版:https://www.cnblogs.com/davidwang456/p/3464743.html
我找了个翻译版看,但是图片刷不出来:https://segmentfault.com/a/1190000004206269
国内英文版那个代码排版又很差,但是有图,我这里把两个整合一下
http://itindex.net/detail/41088-理解-jvm-内幕
正文
每个使用Java的开发者都知道Java字节码是在JRE中运行(Java Runtime Environment Java运行时环境)。JRE中最重要的部分是 Java虚拟机(JVM),JVM负责分析和执行Java字节码,Java开发人员并不需要去关心JVM是如何运行的。在没有深入理解JVM的情况下,许多开发者已经开发出了非常多的优秀的应用以及Java类库。不过,如果你了解JVM的话,你会更加了解Java的,并且你会轻松解决那些看似简单但是无从下手的问题。
因此,在这篇文件里,我会阐述JVM是如何运行的,包括它的结构,它如何去执行字节码,以及按照怎样的顺序去执行,同时我还会给出一些常见错误的示例以及对应的解决办法。最后,我还会讲解Java 7中的一些新特性
虚拟机
JRE是由Java API和JVM组成的。JVM的主要作用是通过Class Loader来加载Java程序,并且按照Java API来执行加载的程序。
虚拟机(VM: Virtual Machine) 虚拟机是通过软件的方式来模拟实现的机器(比如说计算机),它可以像物理机一样运行程序。设计虚拟机的初衷是让Java能够通过它来实现 WORA( Write Once Run Anywhere 一次编译,到处运行),尽管这个目标现在已经被大多数人忽略了。因此,JVM可以在不修改Java代码的情况下,在所有的硬件环境上运行 Java字节码。
JVM的基本特性:
- 基于栈(Stack-based)的虚拟机: 不同于Intel x86和ARM等比较流行的计算机处理器都是基于_寄存器(register)架构,JVM是_基于栈执行的。
- 符号引用(Symbolic reference): 除了基本类型以外的数据(类和接口)都是通过符号来引用,而不是通过显式地使用内存地址来引用。
- 垃圾回收机制: 类的实例都是通过用户代码进行创建,并且自动被垃圾回收机制进行回收。
- 通过明确清晰基本类型确保平台无关性: 传统的编程语言,例如C/C++,int类型的大小取决于不同的平台。JVM通过对基本类型的清晰定义来保证它的兼容性以及平台独立性。
- 网络字节序(Network byte order): Java class文件的二进制表示使用的是基于网络的字节序(network byte order)。为了在使用小端(little endian)的Intel x86平台和在使用了大端(big endian)的RISC系列平台之间保持平台无关,必须要定义一个固定的字节序。JVM选择了网络传输协议中使用的网络字节序,即基于大端(big endian)的字节序。
虽然是Sun公司开发了Java,但是所有的开发商都可以开发并且提供遵循Java虚拟机规范的JVM。正是由于这个原因,使得Oracle HotSpot和IBM JVM等不同的JVM能够并存。Google的Android系统里的Dalvik VM也是一种JVM,虽然它并不遵循Java虚拟机规范。和基于栈的Java虚拟机不同,Dalvik VM是基于寄存器的架构,因此它的Java字节码也被转化成基于寄存器的指令集。
Java 字节码(Java bytecode)
为了保证WORA,JVM使用Java字节码这种介于Java和机器语言之间的中间语言。字节码是部署Java代码的最小单位。
在解释Java字节码之前,我们先通过实例来简单了解它。这个案例是一个在开发环境出现的真实案例的总结。
现象
一个一直运行正常的应用突然无法运行了。在类库被更新之后,返回下面的错误。
Exception in thread "main" java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V at com.nhn.service.UserService.add(UserService.java:14) at com.nhn.service.UserService.main(UserService.java:19)
程序代码如下,并在更新类库之前未曾对这段代码做过变更:
// UserService.java …
public void add(String userName) {
admin.addUser(userName);
}
类库中更新过的代码前后对比如下:
// UserAdmin.java - 更新后的源码 …
public User addUser(String userName) {
User user = new User(userName);
User prevUser = userMap.put(userName, user);
return prevUser;
}
// UserAdmin.java - 更新前的源码 …
public void addUser(String userName) {
User user = new User(userName);
userMap.put(userName, user);
}
简而言之,之前没有返回值的addUser()被改修改成返回一个User类的实例的方法。不过,应用的代码没有做任何修改,因为它没有使用addUser()的返回值。
咋一看,com.nhn.user.UserAdmin.addUser()方法似乎仍然存在,如果存在的话, 那么怎么还会出现NoSuchMethodError的错误呢?,
问题分析
上面问题的原因是在于应用的代码没有用新的类库来进行编译。换句话来说,应用代码似乎是调了正确的方法,只是没有使用它的返回值而已。不管怎样,编译后的class文件表明了这个方法是有返回值的。你可以从下面的错误信息里看到答案。
可以通过下面的异常信息说明这一点:
java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/langString;)V
NoSuchMethodError出现的原因是“com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V”方法找不到。注意一下”Ljava/lang/String;”和最后面的“V”。在Java字节码的表达式里,”L
因为程序代码是使用之前版本的类库进编译的,class文件中定义的是应该调用返回"V"类型的方法(返回值为空)。然而,在改变类库后,返回"V"类型(返回值为空)的方法已不存在,取而代之的是返回类型为"Lcom/nhn/user/User;"的方法。所以便发生了上面看到的NoSuchMethodError。
注释
因为开发者未针对新类库重新编译程序代码,所以发生了错误。尽管如此,类库提供者却也要为此负责。因为之前没有返回值的addUser()方法既然是public方法,但后面却改成了会返回user实现,这意味着方法签名发生了明显的变化。这意味了该类库不能对之前的版本进行兼容,所以类库提供者必须事前对此进行通知。
我们重新回到Java 字节码,Java 字节码 是JVM的基本元素,JVM本身就是一个用于执行Java字节码的执行器。Java编译器并不会把像C/C++那样把高级语言转为机器语言(CPU执行指令),而是把开发者能理解的Java语言转为JVM理解的Java字节码。因为Java字节码是平台无关的,所以它可以在安装了JVM(准确的说,是JRE环境)的任何硬件环境执行,即使它们的CPU和操作系统各不相同(所以在Windows PC机上开发和编译的class文件在不做任何调整的情况下就可以在Linux机器上执行)。编译后文件的大小与源文件大小基本一致,所以比较容易通过网络传输和执行Java字节码。
The class file itself is a binary file that cannot be understood by a human. Java class-文件是一种人很难去理解的二进制文件,所以我们很难直观的理解其中的指令。为了便于理解它,JVM提供者提供了javap,反汇编器。使用javap产生的结果是Java汇编语言。在上面的例子中,下面的Java汇编代码是通过javap -c对UserServiceadd()方法进行反汇编得到的。
public void add(java.lang.String);
Code:
0: aload_0
1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin;
4: aload_1
5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)V
8: return
在这段Java汇编代码中,addUser()方法是在第四行的“5:invokevitual#23″进行调用的。这表示对应索引为23的方法会被调用。索引为23的方法的名称已经被javap给注解在旁边了。 invokevirtual是Java字节码里调用方法的最基本的操作码(Opcode)。在Java字节码里,有四种操作码可以用来调用一个方法,分别是:invokeinterface,invokespecial,invokestatic以及invokevirtual。操作码的作用分别如下:
- invokeinterface: 调用接口方法
- invokespecial: 调用初始化方法、私有方法、或父类中定义的方法
- invokestatic: 调用静态方法
- invokevirtual: 调用实例方法
Java 字节码的指令集包含操作码(OpCode)和操作数(Operand)。像invokevirtual这样的操作码需要一个2字节长度的操作数。(需要2个字节的操作数。)
By compiling the application code above with the updated library and then disassembling it, the following result will be obtained.
对上面案例中的程序代码,如果在更新类库后重新编译程序代码,然后我们再反编译字节码将看到如下结果:
用更新的类库来编译上面的应用代码,然后反编译它,将会得到下面的结果:
public void add(java.lang.String);
Code:
0: aload_0
1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin;
4: aload_1
5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
8: pop
9: return
你会发现,对应索引为23的方法被替换成了一个返回值为”Lcom/nhn/user/User”的方法。
在上面的反编译结果中,代码前面的数字是具有什么含义?
表示该Opcode位于第几个字节(以0开始),大概这就是为什么运行在JVM上面的代码成为Java“字节”码的原因。像 aload_0, getfield 和 invokevirtual 都被表示为一个单字节数字。(aload_0 = 0x2a, getfiled = 0xb4, invokevirtual = 0xb6)。因此Java字节码表示的最大指令码为256。(一个字节是2的8次方,可以表示2的8次方,0-255)
像aload_0和aload_1这样的操作码不需要任何操作数,因此aload_0的下一个字节就是下一个指令的操作码。而像getfield和invokevirtual这样的操作码却需要一个2字节的操作数,因此第一个字节里的第二个指令getfield指令的一下指令是在第4个字节,其中跳过了2个字节。通过16进制编辑器查看字节码如下:
0 1 2 3 4 5 6 7 8 9
2a b4 00 0f 2b b6 00 17 57 b1
(aload_0 = 0x2a, getfiled = 0xb4, invokevirtual = 0xb6) 怎样计算来的??
aload_0, aload_1, getfield 和 invokevirtual 都为操作码 占一个字节
getfield ,getfield 都需要两个字节的操作数。
0x2a --> aload_0 OpCode
0xb4 --> getfield OpCode
0x00 --> one of Operand of getfield OpCode
0x0f --> one of Operand of getfield OpCode
0x2b --> aload_1 OpCode
0xb6 --> invokevirtual Opcode
0x00 --> one of Operand of invokevirtual OpCode
0x17 --> one of Operand of invokevirtual OpCode
0x57 --> pop OpCode
0xb1 --> return OpCode
在Java字节码中,类实例表示为"L;",而void表示为"V",类似的其他类型也有各自的表示。下表列出了Java字节码中类型表示。
表1: Java字节码里的类型表示
Java 字节码 | 类型 | 描述 |
---|---|---|
B | byte | 单字节 |
C | char | Unicode字符 |
D | double | 双精度浮点数 |
F | float | 单精度浮点数 |
I | int | 整型 |
J | long | 长整型 |
L | 引用 | classname类型的实例 |
S | short | 短整型 |
Z | boolean | 布尔类型 |
[ | 引用 | 一维数组 |
表2: Java代码的字节码示例
| Java 字节码 |Java 字节码表示 |
| -------- | -----: | :----: |
| double d[][][] | [[[D |
|Object mymethod(int i, double d, Thread t) | mymethod(I,D,Ljava/lang/Thread;)Ljava/lang/Object; |
在《Java虚拟机技术规范第二版》的4.3 描述符(Descriptors)章节中有关于此的详细描述,在第6章"Java虚拟机指令集"中介绍了更多不同的指令。
类文件格式
现象
当我们编写完jsp代码,并且在Tomcat运行时,Jsp代码没有正常运行,而是出现了下面的错误。
Servlet.service() for servlet jsp threw exception org.apache.jasper.JasperException: Unable to compile class for JSP Generated servlet error: The code of method _jspService(HttpServletRequest, HttpServletResponse) is exceeding the 65535 bytes limit"
问题分析
在不同的Web服务器上,上面的错误信息可能会有点不同,不过有有一点肯定是相同的,它出现的原因是65535字节的限制。这个65535字节的限制是JVM规范里的限制,它规定了 一个方法的大小不能超过65535字节。
下面我会更加详细地讲解这个65535字节限制的意义以及它出现的原因。
Java字节码里的分支和跳转指令分别是”goto"和"jsr"。
goto [branchbyte1] [branchbyte2]
jsr [branchbyte1] [branchbyte2]
这两个指令都接收一个2字节的有符号的分支跳转偏移量做为操作数,因此偏移量最大只能达到65535。不过,为了支持更多的跳转,Java字节码提供了"goto_w"和"jsr_w"这两个可以接收4字节分支偏移的指令。
goto_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]
jsr_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]
受这两个指令所赐,分支能表示的最大偏移远远超过了65535,这么说来java 方法就不会再有65535个字节的限制了。然而,由于Java 类文件的各种其他限制,java方法的定义仍然不能够超过65535个字节的限制。下面我们通过对类文件的解释来看看java方法不能超过65535字节的其他原因。
Java类文件的大体结构如下:
ClassFile {
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 fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];}
上面的文件结构出自《Java虚拟机技术规范第二版》的4.1节 类文件结构。
之前讲过的UserService.class文件的前16个字节的16进制表示如下:
ca fe ba be 00 00 00 32 00 28 07 00 02 01 00 1b
我们通过对这一段符号的分析来了解一个类文件的具体格式。
- magic: 类文件的前4个字节是一组魔数,是一个用于区分Java类文件的预定义值。如上所看到的,其值固定为0xCAFEBABE。也就是说一个文件的前4个字节如果是0xCAFABABE,就可以认为它是Java类文件。"CAFABABE"是与"JAVA"有关的一个有趣的魔数。
- minor_version, major_version: 接下来的4个字节表示类的版本号。如上所示,0x00000032表示的类版本号为50.0。由JDK 1.6编译而来的类文件的版本号是50.0,而由JDK 1.5编译而来的版本号则是49.0。JVM必须保持向后兼容,即保持对比其版本低的版本的类文件的兼容。而如果在一个低版本的JVM中运行高版本的类文件,则会出现java.lang.UnsupportedClassVersionError的发生。
- constant_pool_count, constant_pool[]: 紧接着版本号的是类的常量池信息。这里的信息在运行时会被分配到运行时常量池区域,后面会有对内存分配的介绍。在JVM加载类文件时,类的常量池里的信息会被分配到运行时常量池,而运行时常量池又包含在方法区内。上面UserService.class文件的constant_pool_count为0x0028,所以按照定义contant_pool数组将有(40-1)即39个元素值。
- access_flags: 2字节的类的修饰符信息,表示类是否为public, private, abstract或者interface。
- this_class, super_class: 分别表示保存在constant_pool数组中的当前类及父类信息的索引值。
- interface_count, interfaces[]: interface_count为保存在constant_pool数组中的当前类实现的接口数的索引值,interfaces[]即表示当前类所实现的每个接口信息。
- fields_count, fields[]: 类的字段数量及字段信息数组。字段信息包含字段名、类型、修饰符以及在constant_pool数组中的索引值。
- methods_count, methods[]: 类的方法数量及方法信息数组。方法信息包括方法名、参数的类型及个数、返回值、修饰符、在constant_pool中的索引值、方法的可执行代码以及异常信息。
- attributes_count, attributes[]: attribute_info有多种不同的属性,分别被field_info, method_into使用。
javap程序把class文件格式以可阅读的方式输出来。在对UserService.class文件使用"javap -verbose"命令分析时,输出内容如下:
Compiled from "UserService.java"
public class com.nhn.service.UserService extends java.lang.Object
SourceFile: "UserService.java"
minor version: 0
major version: 50
Constant pool:const #1 = class #2; // com/nhn/service/UserService
const #2 = Asciz com/nhn/service/UserService;
const #3 = class #4; // java/lang/Object
const #4 = Asciz java/lang/Object;
const #5 = Asciz admin;
const #6 = Asciz Lcom/nhn/user/UserAdmin;;// … omitted - constant pool continued …
{
// … omitted - method information …
public void add(java.lang.String);
Code:
Stack=2, Locals=2, Args_size=2
0: aload_0
1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin;
4: aload_1
5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
8: pop
9: return LineNumberTable:
line 14: 0
line 15: 9 LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/nhn/service/UserService;
0 10 1 userName Ljava/lang/String; // … Omitted - Other method information …
}
由于篇幅原因,上面只抽取了部分输出结果。在全部的输出信息中,会为你展示包括常量池和每个方法内容等各种信息。
方法的65535个字节的限制受到了method_info struct的影响。如上面"javap -verbose"的输出所示,method_info结构包含Code,LineNumberTable,以及LocalViriable attribute几个属性,这个在“javap -verbose"的输出里可以看到。Code属性里的LineNumberTable,LocalVariableTable以及exception_table的长度都是用一个固定的2字节来表示的。因此,方法的大小是不能超过LineNumberTable,LocalVariableTable以及exception_table的长度的,它们都是65535字节(即不能超过65535个字节)。
尽管许多人都在抱怨方法的大小限制,JVM规范里也声称了”这个长度以后有可能会是可扩展的“。不过,到现在为止,还没有为这个限制做出任何动作。从JVM规范里的把class文件里的内容直接拷贝到方法区这个特点来看,要想在保持后向兼容性的同时来扩展方法区的大小是非常困难的。
对于一个由Java编译器错误而导致的错误的类文件将发生怎样的情况?如果是在网络传输或文件复制过程中,类文件被损坏又将发生什么?
为了应对这种场景,Java的类装载器通过一个严格而且慎密的过程来校验class文件。在JVM规范里详细地讲解了这方面的内容。
注释 我们怎样能够判断JVM正确地执行了class文件校验的所有过程呢?我们怎么来判断不同提供商的不同JVM实现是符合JVM规范的呢?
划重点:验证jvm
1.验证jvm是否符合jvm规范
2.验证jvm是否成功执行类文件的验证过程
为了能够验证以上两点,Oracle提供了一个测试工具TCK(Technology Compatibility Kit)。这个TCK工具通过执行成千上万的测试用例来验证一个JVM是否符合规范,这些测试里面包含了各种非法的class文件。只有通过了TCK的测试的JVM才能称作JVM。
和TCK相似,有一个组织JCP(Java Community Process; http://jcp.org)负责Java规范以及新的Java技术规范。对于JCP而言,如果要完成一项Java规范请求(Java Specification Request, JSR)的话,需要具备规范文档,可参考的实现以及通过TCK测试。任何人如果想使用一项申请JSR的新技术的话,他要么使用RI提供许可的实现,要么自己实现一个并且保证通过TCK的测试。
JVM 结构
Java程序的执行过程如下图所示:
图1: Java代码执行过程
类装载器负责装载编译后的字节码,并加载到运行时数据区(Runtime Data Area),然后执行引擎执行会执行这些字节码。
类加载
Java提供了动态的装载特性;它会在运行时的第一次引用到一个class的时候对它进行装载和链接,而不是在编译期进行。JVM的类装载器负责动态装载。Java类装载器有如下几个特点:
- 层次结构: Java的类加载器是按父子关系的层次结构组织的。Boostrap类加载器处于层次结构的顶层,是所有类加载器的父类。
- 委派模式: 基于类加载器的层次组织结构,类加载器之间是可以进行委派的。当一个类需要被加载,会先去请求父加载器判断该类是否已经被加载。如果父类加器已加载了该类,那它就可以直接使用而无需再次加载。如果尚未加载,才需要当前类加载器来加载此类。
- 可见性限制: 子类加载器可以从父类加载器中获取类,反之则不行。一个子装载器可以查找父装载器中的类,但是一个父装载器不能查找子装载器里的类。
- 不能卸载: 类装载器可以装载一个类但是不可以卸载它,不过可以删除当前的类装载器,然后创建一个新的类装载器。
Each class loader has its namespace that stores the loaded classes. When a class loader loads a class, it searches the class based on FQCN (Fully Qualified Class Name) stored in the namespace to check whether or not the class has been already loaded. Even if the class has an identical FQCN but a different namespace, it is regarded as a different class. A different namespace means that the class has been loaded by another class loader.
每个类加载器都有自己的空间,用于存储其加载的类信息。当类加载器需要加载一个类时,它通过FQCN)(Fully Quanlified Class Name: 全限定类名)的方式先在自己的存储空间中检测此类是否已存在。在JVM中,即便具有相同FQCN的类,如果出现在了两个不同的类加载器空间中,它们也会被认为是不同的。存在于不同的空间意味着类是由不同的加载器加载的。
下图解释了类加载器的委派模型:
图2: 类加载器的委派模型
When a class loader is requested for class load, it checks whether or not the class exists in the class loader cache, the parent class loader, and itself, in the order listed. In short, it checks whether or not the class has been loaded in the class loader cache. If not, it checks the parent class loader. If the class is not found in the bootstrap class loader, the requested class loader searches for the class in the file system.
当JVM请示类加载器加载一个类时,加载器总是按照从类加载器缓存、父类加载器以及自己加载器的顺序查找和加载类。也就是说加载器会先从缓存中判断此类是否已存在,如果不存在就请示父类加载器判断是否存在,如果直到Bootstrap类加载器都不存在该类,那么当前类加载器就会从文件系统中找到类文件进行加载。
- Bootstrap加载器:
这个类装载器是在JVM启动的时候创建的。它负责装载Java API,包含Object对象。和其他的类装载器不同的地方在于这个装载器是通过native code来实现的,而不是用Java代码。 - 扩展加载器(Extension class loader): 扩展加载器用于加载除基本Java APIs以外扩展类。也用于加载各种安全扩展功能。
- 系统加载器(System class loader): 如果说Bootstrap和Extension加载器用于加载JVM运行时组件,那么系统加载器加载的则是应用程序相关的类。它会加载用户指定的CLASSPATH里的类。
If the bootstrap class loader and the extension class loader load the JVM components, the system class loader loads the application classes. It loads the class in the $CLASSPATH specified by the user. - 用户自定义加载器: 这个是由用户的程序代码创建的类加载器。This is a class loader that an application user directly creates on the code.
Frameworks such as Web application server (WAS) use it to make Web applications and enterprise applications run independently. In other words, this guarantees the independence of applications through class loader delegation model. Such a WAS class loader structure uses a hierarchical structure that is slightly different for each WAS vendor.
像Web应用服务器(WAS: Web Application Server)等框架通过使用用户自定义加载器使Web应用和企业级应用可以隔离开在各自的类加载空间独自运行。也就是说可以通过类加载器的委派模式来保证应用的独立性。不同的WAS在自定义类加载器时会有略微不同,但都不外乎使用加载器的层次结构原理。
如果一个类加载器发现了一个未加载的类,则该类的加载和链接过程如下图:
图3: 类加载步骤
每一步的具体描述如下:
- 加载(Loading): 从.class文件中获取类并载入到JVM内存空间。
- 验证(Verifying): 检查读入的结构是否符合Java语言规范以及JVM规范的描述。这是类装载中最复杂的过程,并且花费的时间也是最长的。
Most cases of the JVM TCK test cases are to test whether or not a verification error occurs by loading wrong classes.
并且JVM TCK工具的大部分场景的用例也用来测试在装载错误的类的时候是否会验证失败。 - 准备(Preparing): 分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量,方法和接口的信息。
- 解析(Resolving): 把类常量池中所有的符号引用转为直接引用。Change all symbolic references in the constant pool of the class to direct references.
- 初始化(Initializing): 把类中的变量初始化成合适的值。执行静态初始化程序,把静态变量初始化成指定的值。 Initialize the class variables to proper values. Execute the static initializers and initialize the static fields to the configured values.
JVM规范定义了上面的几个任务,不过它允许具体执行的时候能够有些灵活的变动。
运行时数据区
图4: 运行时数据区(Runtime Data Areas)
Runtime Data Areas are the memory areas 【which is】assigned(分配) when the JVM program runs on the OS. The runtime data areas can be divided into 6 areas. Of the six, one PC Register, JVM Stack, and Native Method Stack are created for one thread. Heap, Method Area, and Runtime Constant Pool are shared by all threads.
运行时数据区域是在操作系统上运行JVM程序时分配的内存区域。运行时数据区域可分为6个区域。在这6个区域中,一个PC Register,JVM stack 以及Native Method Statck都是为每一个线程创建的,Heap,Method Area以及Runtime Constant Pool都是被所有线程共享的。
PC 寄存器(PC register)::
One PC (Program Counter) register exists for one thread, and is created when the thread starts. PC register has the address of a JVM instruction being executed now.
每个线程都会有一个PC(Program Counter)寄存器,并跟随线程的启动而创建。PC寄存器里保存有当前正在执行的JVM指令的地址。
JVM 栈(JVM stack):
One JVM stack exists for one thread, and is created when the thread starts. It is a stack that saves the struct (Stack Frame). The JVM just pushes or pops the stack frame to the JVM stack.
If any exception occurs, provides programmatic access to the stack trace information printed by printStackTrace(). Returns an array of stack trace elements, each representing one stack frame
每个线程启动的时候,都会创建一个JVM stack。它是用来保存栈帧(Stack Frame)。JVM只会在JVM stack上对栈帧进行push和pop的操作。如果出现了异常,使用printStackTrace()方法。可以获取一个栈跟踪元素数组,每个元素表示一个栈帧。
图5: JVM栈结构
- 1.- 栈帧(stack frame):
One stack frame is created whenever a method is executed in the JVM, and the stack frame is added to the JVM stack of the thread. When the method is ended, the stack frame is removed.
Each stack frame has the reference for local variable array, Operand stack, and runtime constant pool of a class where the method being executed belongs.
在JVM中一旦有方法执行,JVM就会为之创建一个栈帧,并把其添加到当前线程的JVM栈中。当方法运行结束时,栈帧也会相应的从JVM栈中移除。
Each stack frame has the reference for local variable array, Operand stack, and runtime constant pool of a class where the method being executed belongs
每个栈帧里都包含有当前正在执行的方法所属类的局部变量数组 的引用,操作数栈 的引用,以及运行时常量池 的引用
The size of local variable array and Operand stack is determined while compiling. Therefore, the size of stack frame is fixed according to the method.
局部变量数组的和操作数栈的大小都是在编译时确定的。因此,一个方法的栈帧的大小也是固定不变的。
-
2.- 局部变量数组(Local variable array ):
It has an index starting from 0. 0 is the reference of a class instance where the method belongs. From 1, the parameters sent to the method are saved. After the method parameters, the local variables of the method are saved.
这个数组的索引从0开始。索引为0的变量表示这个方法所属的类的实例。从1开始,首先存放的是传给该方法的参数,在参数后面保存的是方法的局部变量。 -
3 - 操作数栈(Operand stack ):
An actual workspace of a method. Each method exchanges data between the Operand stack and the local variable array, and pushes or pops other method invoke results. The necessary size of the Operand stack space can be determined during compiling. Therefore, the size of the Operand stack can also be determined during compiling.
方法实际运行的工作空间。每个方法都在操作数栈和局部变量数组之间交换数据,并把调用其它方法的结果从栈中弹或压入。在编译时,编译器就能计算出操作数栈所需的内存大小,因此操作数栈的大小在编译时也是确定的。
本地方法栈( Native method stack):
A stack for native code written in a language other than Java. In other words, it is a stack used to execute C/C++ codes invoked through JNI (Java Native Interface). According to the language, a C stack or C++ stack is created.
A stack for native code(不用Java语言编写的代码) 。它是通过调用JNI(Java Native Interface Java本地接口)去执行C/C++代码。 According to the language,一个C堆栈或者C++堆栈会被创建。
方法区(Method area):
The method area is shared by all threads, created when the JVM starts.
It (The method area)stores runtime constant pool, field and method information, static variable,
and method bytecode for each of the classes and interfaces read by the JVM.
The method area can be implemented in various formats by JVM vendor.
Oracle Hotspot JVM calls it (指的是method area)Permanent Area or Permanent Generation (PermGen). The garbage collection for the method area is optional for each JVM vendor.
方法区是所有线程共享的,它是在JVM启动的时候创建的。
它保存所有被JVM加载的类和接口的 运行时常量池,成员变量以及方法的信息,静态变量以及方法的字节码。
JVM的提供者可以通过不同的方式来实现方法区。
在Oracle 的HotSpot JVM里,方法区被称为Permanent Area(永久区)或者Permanent Generation(PermGen)。
JVM规范并对方法区的垃圾回收未做强制限定,因此对于JVM vendor(提供者)来说,方法区的垃圾回收是可选操作。
运行时常量池( Runtime constant pool ):
An area that corresponds to the constant_pool table in the class file format. This area is included in the method area;
however, it plays the most core role in JVM operation. Therefore, the JVM specification separately describes its importance.
As well as the constant of each class and interface, it contains all references for methods and fields. In short, when a method or field is referred to, the JVM searches the actual address of the method or field on the memory by using the runtime constant pool.
这个区域和class文件里的the constant_pool table是相对应的。这个区域是包含在method area里的。
不过,对于JVM的操作而言,它是一个核心的角色。因此在JVM规范里特别提到了它的重要性。
除了包含每个类和接口的常量( the constant of each class and interface),它也包含了所有方法和变量的引用( all references for methods and fields.)。简而言之,当一个方法或者变量被引用时,JVM通过运行时常量区来查找方法或者变量在内存里的实际地址(the JVM searches the actual address of the method or field on the memory by using the runtime constant pool.)。
这部分空间虽然存在于方法区内,但却在JVM操作中扮演着举足轻重的角色,因此JVM规范单独把这一部分拿出来描述。除了每个类或接口中定义的常量,它还包含了所有对方法和字段的引用。因此当需要一个方法或字段时,JVM通过运行时常量池中的信息从内存空间中来查找其相应的实际地址。
堆(Heap):
A space that stores instances or objects, and is a target of garbage collection. This space is most frequently mentioned when discussing issues such as JVM performance. JVM vendors can determine how to configure the heap or not to collect garbage.
堆中存储着所有的类实例或对象,而且它是垃圾回收的主要目标。当涉及到类似于JVM性能之类的问题时,当讨论类似于JVM性能之类的问题时,它经常会被提及。JVM提供者可以决定划分堆空间或者不执行垃圾回收。
现在我们再会过头来看看之前反汇编的字节码:
public void add(java.lang.String);
Code:
0: aload_0
1: getfield #15; //Field admin:Lcom/nhn/user/UserAdmin;
4: aload_1
5: invokevirtual #23; //Method com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User;
8: pop
9: return
// UserService.java
…
public void add(String userName) {
admin.addUser(userName);
}
Comparing the disassembled code and the assembly code of the x86 architecture that we sometimes see, the two have a similar format, OpCode;
however, there is a difference in that Java Bytecode does not write register name, memory addressor, or offset on the Operand.
As described before, the JVM uses stack. Therefore, it does not use register, unlike the x86 architecture that uses registers,
and it uses index numbers such as 15 and 23 instead of memory addresses since it manages the memory by itself. The 15 and 23 are the indexes of the constant pool of the current class (here, UserService class)
In short, the JVM creates a constant pool for each class, and the pool stores the reference of the actual target.
把上面的反汇编代码和我们平时所见的x86架构的汇编代码相比较,我们会发现这两者的结构有点相似,都使用了操作码;
不过,有一点不同的地方是Java字节码并不会在操作数里写入寄存器的名称、内存地址或者偏移量。
之前已经说过,JVM用的是栈,它不会使用寄存器。和使用寄存器的x86架构不同,它自己负责内存的管理。它用索引例如15和23来代替实际的内存地址。15和23都是当前类(这里是UserService类)的常量池里的索引。
简而言之,JVM为每个类创建了一个常量池,并且这个常量池里保存了真实对象的引用。
Each row of the disassembled code is interpreted as follows.
上面每行代码的解释如下:
- aload_0:
Add the #0 index of the local variable array to the Operand stack. The #0 index of the local variable array is always this, the reference for the current class instance.
把局部变量数组中索引为#0的变量添加到操作数栈上。局部变量数组中索引#0所表示的变量是this,即是当前类实例对象的引用
- getfield /#15:
In the current class constant pool, add the #15 index to the Operand stack. UserAdmin admin field is added. Since the admin field is a class instance, a reference is added.
把当前类的常量池里的索引为#15的变量添加到操作数栈。这里添加的是UserAdmin的admin成员变量。因为admin变量是个类的实例,因此添加的是一个引用。
- aload_1:
Add the #1 index of the local variable array to the Operand stack. From the #1 index of the local variable array, it is a method parameter. Therefore, the reference of String userName sent while invoking add() is added.
把局部变量数组里的索引为#1的变量添加到操作数栈。本地变量数组中从第1个位置开始的元素存储着方法的参数。
因此,在调用add()方法的时候,会把userName指向的String的引用添加到操作数栈上。
- invokevirtual /#23:
Invoke the method corresponding to the #23 index in the current class constant pool. At this time, the reference added by using getfield and the parameter added by using aload_1 are sent to the method to invoke. When the method invocation is completed, add the return value to the Operand stack.
调用当前类的常量池里的索引为#23的方法。这个时候,通过getfile添加到操作数栈上的引用和通过aload_1添加到操作数栈上的参数(parameter)都被传给方法调用。当方法运行完成时,它的返回值结果会被添加到操作数栈上。
- pop:
Pop the return value of invoking by using invokevirtual from the Operand stack.
You can see that the code compiled by the previous library has no return value. In short, the previous has no return value, so there was no need to pop the return value from the stack.
把通过invokevirtual方法调用得到的结果从操作数栈中弹出。
在前面讲述中使用之前类库时没有返回值,也就不需要把结果从操作数栈中弹出了。
- return: Complete the method. 方法完成。
下图将帮助理解上面的文字解释:
图6: 从运行时数据区加载Java字节码示例
For reference, in this method, no local variable array has been changed.
So the figure above displays the changes in Operand stack only. However, in most cases, local variable array is also changed.
Data transfer between the local variable array and the Operand stack is made by using a lot of load instructions (aload, iload) and store instructions (astore, istore).
顺便提一下,在这个方法里,局部变量数组没有被修改。所以上图只显示了操作数栈的变化。不过,大部分的情况下,局部变量数组也是会改变的。
局部变量数组和操作数栈之间的数据传输是使用通过大量的load指令(aload,iload)和store指令(astore,istore)来实现的。
In this figure, we have checked the brief description of the runtime constant pool and the JVM stack. When the JVM runs, each class instance will be assigned to the heap, and class information including User, UserAdmin, UserService, and String will be stored in the method area.
在这个图里,我们简单验证了运行时常量池和JVM栈的描述。当JVM运行的时候,每个类的实例都会在堆上进行分配,User,UserAdmin,UserService以及String等类的信息都会被存储在方法区。
执行引擎
The bytecode that is assigned to the runtime data areas in the JVM via class loader is executed by the execution engine. The execution engine reads the Java Bytecode in the unit of instruction. It is like a CPU executing the machine command one by one. Each command of the bytecode consists of a 1-byte OpCode and additional Operand. The execution engine gets one OpCode and execute task with the Operand, and then executes the next OpCode.
通过类装载器装载的,被分配到JVM的运行时数据区的字节码会被执行引擎执行。执行引擎在指令单元读取Java字节码。(它就像一个CPU一样,一条一条地执行机器指令。)每个字节码指令都由一个1字节的操作码和附加的操作数组成。执行引擎取得一个操作码,然后根据操作数来执行任务,完成后就继续执行下一条操作码。
But the Java Bytecode is written in a language that a human can understand, rather than in the language that the machine directly executes. Therefore, the execution engine must change the bytecode to the language that can be executed by the machine in the JVM. The bytecode can be changed to the suitable language in one of two ways.
尽管如此,Java字节码还是以一种可以理解的语言编写的,而不是用机器可以直接执行的语言。因此,JVM的执行引擎必须把字节码转换成直接被机器(the machine in the JVM)执行的机器码。字节码可以通过以下两种方式转换成合适的语言。
ps:https://www.cnblogs.com/chanshuyi/p/jvm_serial_04_from_source_code_to_machine_code.html 这边有更加详细的介绍
当源代码转化为字节码之后,其实要运行程序,有两种选择。一种是使用 Java 解释器解释执行字节码,另一种则是使用 JIT 编译器将字节码转化为本地机器代码。
- 解释器(Interpreter):
Reads, interprets and executes the bytecode instructions one by one. As it interprets and executes instructions one by one, it can quickly interpret one bytecode, but slowly executes the interpreted result. This is the disadvantage of the interpret language. The 'language' called Bytecode basically runs like an interpreter.
一条一条地读取,解释并且执行字节码指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。这是解释执行的语言的一个缺点。字节码这种“语言”基本来说是解释执行的。
- 即时编译器(JIT: Just-In-Time):
The JIT compiler has been introduced to compensate for the disadvantages of the interpreter. The execution engine runs as an interpreter first, and at the appropriate time, the JIT compiler compiles the entire bytecode to change it to native code. After that, the execution engine no longer interprets the method, but directly executes using native code. Execution in native code is much faster than interpreting instructions one by one. The compiled code can be executed quickly since the native code is stored in the cache.
即时编译器被引入用来弥补解释器的缺点。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行它。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。
However, it takes more time for JIT compiler to compile the code than for the interpreter to interpret the code one by one. Therefore, if the code is to be executed just once, it is better to interpret it instead of compiling. Therefore, the JVMs that use the JIT compiler internally check how frequently the method is executed and compile the method only when the frequency is higher than a certain level.
不过,用JIT编译器来编译代码所花的时间要比用解释器去一条条解释执行花的时间要多。因此,如果代码只被执行一次的话,那么最好还是解释执行而不是编译后再执行。因此,内置了JIT编译器的JVM都会检查方法的执行频率,如果一个方法的执行频率超过一个特定的值的话,那么这个方法就会被编译成本地代码。
图7: Java编译器和即时编译器
How the execution engine runs is not defined in the JVM specifications. Therefore, JVM vendors improve their execution engines using various techniques, and introduce various types of JIT compilers.
JVM规范没有定义执行引擎该如何去执行。因此,JVM的提供者通过使用不同的技术以及不同类型的JIT编译器来提高执行引擎的效率。
Most JIT compilers run as shown in the figure below:
大部分的即时编译器运行流程如下图:
图8: 即时编译器
The JIT compiler converts the bytecode to an intermediate-level expression, IR (Intermediate Representation), to execute optimization, and then converts the expression to native code.
即时编译器先把字节码转为一种中间形式的表达式(IR: Itermediate Representation),来进行优化,然后再把这种表示转换成本地代码
Oracle Hotspot VM uses a JIT compiler called Hotspot Compiler. It is called Hotspot because Hotspot Compiler searches the 'Hotspot' that requires compiling with the highest priority through profiling, and then it compiles the hotspot to native code. If the method that has the bytecode compiled is no longer frequently invoked, in other words, if the method is not the hotspot any more, the Hotspot VM removes the native code from the cache and runs in interpreter mode. The Hotspot VM is divided into the Server VM and the Client VM, and the two VMs use different JIT compilers.
Oracle Hotspot VM使用一种叫做Hotspot Compiler 的JIT编译器。它之所以被称作”Hotspot“是因为Hotspot Compilerr会根据剖析找到具有更高编译优先级的热点代码,然后把热点代码编译成本地代码。如果已经被编译成本地代码的字节码不再被频繁调用了,换句话说,这个方法不再是热点了,Hotspot VM会把这些本地代码从缓存中删除并对其再次使用解释器模式执行。Hotspot VM分为Server VM和Client VM两种,这两种VM使用不同的JIT编译器。
图9: Hotspot ClientVM 和Server VM
The client VM and the server VM use an identical runtime; however, they use different JIT compilers, as shown in the above figure.
Advanced Dynamic Optimizing Compiler used by the server VM uses more complex and diverse (多样的)performance optimization techniques.
Client VM 和Server VM使用完全相同的运行时,不过如上图所示,它们所使用的JIT编译器是不同的。Server VM用的是更高级的动态优化编译器,这个编译器使用了更加复杂并且更多种类的性能优化技术。
IBM JVM has introduced AOT (Ahead-Of-Time) Compiler from IBM JDK 6 as well as the JIT compiler. This means that many JVMs share the native code (which is )compiled through the shared cache.
In short, the code that has been already compiled through the AOT compiler can be used by another JVM without compiling. In addition, IBM JVM provides a fast way of execution by pre-compiling code to JXE (Java EXecutable) file format using the AOT compiler.
IBM 在IBM JDK 6里不仅引入了JIT编译器,它同时还引入了AOT(Ahead-Of-Time)编译器。它使得多个JVM可以通过共享缓存来共享编译过的本地代码。
简而言之,通过AOT编译器编译过的代码可以直接被其他JVM使用。除此之外,IBM JVM通过使用AOT编译器通过提前把代码编译器成JXE(Java EXecutable)文件格式从而提供了一种快速执行代码的方式。
Most Java performance improvement is accomplished by improving the execution engine. As well as the JIT compiler, various optimization techniques are being introduced so the JVM performance can be continuously improved. The biggest difference between the initial JVM and the latest JVM is the execution engine.
大多数的Java性能提升都是通过优化执行引擎的性能实现的。像即时编译等各种优化技术被不断的引入,从而使得JVM性能得到了持续的优化和提升。老旧的JVM与最新的JVM之间最大的差异其实就来自于执行引擎。
Hotspot compiler has been introduced to Oracle Hotspot VM from version 1.3, and JIT compiler has been introduced to Dalvik VM from Android 2.2.
Hotspot编译器从Java 1.3开始便引入到了Oracle Hotspot VM中,而即时编译器从Android 2.2开始便被引入到了Android Dalvik VM中。
Note
The technique in which an intermediate language such as bytecode is introduced, the VM executes the bytecode, and the JIT compiler improves the performance of JVM is also commonly used in other languages that have introduced intermediate languages. For Microsoft's .Net, CLR (Common Language Runtime), a kind of VM, executes a kind of bytecode, called CIL (Common Intermediate Language). CLR provides the AOT compiler as well as the JIT compiler. Therefore, if source code is written in C# or VB.NET and compiled, the compiler creates CIL and the CIL is executed on the CLR with the JIT compiler. The CLR uses the garbage collection and runs as a stack machine like the JVM.
注释
引入一种中间语言,例如字节码,虚拟机执行字节码,并且通过JIT编译器来提升JVM的性能的这种技术以及广泛应用在使用中间语言的编程语言上。例如微软的.Net,CLR(Common Language Runtime 公共语言运行时),也是一种VM,它执行一种被称作CIL(Common Intermediate Language)的字节码。CLR提供了AOT编译器和JIT编译器。因此,用C#或者VB.NET编写的源代码被编译后,编译器会生成CIL并且CIL会执行在有JIT编译器的CLR上。CLR和JVM相似,它也有垃圾回收机制,并且也是基于栈运行。
Java 虚拟机规范,Java SE 第7版
2011年7月28日,Oracle发布了Java SE的第7个版本,并且把JVM规也更新到了相应的版本。在1999年发布《The Java Virtual Machine Specification,Second Edition》后,Oracle花了12年来发布这个更新的版本。这个更新的版本包含了这12年来累积的众多变化以及修改,并且更加细致地对规范进行了描述。此外,它还反映了《The Java Language Specificaion,Java SE 7 Edition》里的内容。主要的变化总结如下:
- 来自Java SE 5.0里的泛型,支持可变参数的方法
- 从Java SE 6以来,字节码校验的处理技术所发生的改变
- 添加invokedynamic指令以及class文件对于该指令的支持
- 删除了关于Java语言概念的内容,并且指引读者去参考Java语言规范
- 删除关于Java线程和锁的描述,并且把它们移到Java语言规范里
最大的改变是添加了invokedynamic指令。也就是说JVM的内部指令集做了修改,使得JVM开始支持动态类型的语言,这种语言的类型不是固定的,例如脚本语言以及来自Java SE 7里的Java语言。之前没有被用到的操作码186被分配给新指令invokedynamic,而且class文件格式里也添加了新的内容来支持invokedynamic指令。
Java SE 7的编译器生成的class文件的版本号是51.0。Java SE 6的是50.0。class文件的格式变动比较大,因此,51.0版本的class文件不能够在Java SE 6的虚拟机上执行。
尽管有了这么多的变动,但是Java方法的65535字节的限制还是没有被去掉。除非class文件的格式彻底改变,否者这个限制将来也是不可能去掉的。
值得说明的是,Oracle Java SE 7 VM支持G1这种新的垃圾回收机制,不过,它被限制在Oracle JVM上,因此,JVM本身对于垃圾回收的实现不做任何限制。也因此,在JVM规范里没有对它进行描述。
String in switch Statements
Java SE 7 adds various grammars and features. However, compared to the various changes in language of Java SE 7, there are not so many changes in the JVM. So, how can the new features of the Java SE 7 be implemented? We will see how String in switch Statements (a function to add a string to a switch() statement as a comparison) has been implemented in Java SE 7 by disassembling it.
For example, the following code has been written.
Java SE 7里添加了很多新的语法和特性。不过,在Java SE 7的版本里,相对于语言本身而言,JVM没有多少的改变。那么,这些新的语言特性是怎么来实现的呢?我们通过反汇编的方式来看看switch语句里的String(把字符串作为switch()语句的比较对象)是怎么实现的?
例如,下面的代码:
// SwitchTest
public class SwitchTest {
public int doSwitch(String str) {
switch (str) {
case "abc": return 1;
case "123": return 2;
default: return 0;
}
}
}
Since it is a new function of Java SE 7, it cannot be compiled using the Java compiler for Java SE 6 or lower versions. Compile it using the javac of Java SE 7. The following screen is the compiling result printed by using javap –c.
因为这是Java SE 7的一个新特性,所以它不能在Java SE 6或者更低版本的编译器上来编译。用Java SE 7的javac来编译。下面是通过javap -c来反编译后的结果。
C:Test>javap -c SwitchTest.classCompiled from "SwitchTest.java"
public class SwitchTest {
public SwitchTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return public int doSwitch(java.lang.String);
Code:
0: aload_1
1: astore_2
2: iconst_m1
3: istore_3
4: aload_2
5: invokevirtual #2 // Method java/lang/String.hashCode:()I
8: lookupswitch { // 2
48690: 50
96354: 36
default: 61
}
36: aload_2
37: ldc #3 // String abc
39: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
42: ifeq 61
45: iconst_0
46: istore_3
47: goto 61
50: aload_2
51: ldc #5 // String 123
53: invokevirtual #4 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
56: ifeq 61
59: iconst_1
60: istore_3
61: iload_3
62: lookupswitch { // 2
0: 88
1: 90
default: 92
}
88: iconst_1
89: ireturn
90: iconst_2
91: ireturn
92: iconst_0
93: ireturn
A significantly longer bytecode than the Java source code has been created. First, you can see that lookupswitch instruction has been used for switch() statement in Java bytecode.
However, two lookupswitch instructions have been used, not the one lookupswitch instruction.
When disassembling the case in which int has been added to switch() statement, only one lookupswitch instruction has been used.
This means that the switch() statement has been divided into two statements to process the string.
See the annotation of the #5, #39, and #53 byte instructions to see how the switch() statement has processed the string.
生成的字节码的长度比Java源码长多了。
首先,你可以看到字节码里用lookupswitch指令来实现switch()语句。
不过,这里使用了两个lookupswitch指令,而不是一个。
如果反编译的是针对Int的switch()语句的话,字节码里只会使用一个lookupswitch指令。
也就是说,针对string的switch语句被分成用两个语句来实现。
请参阅#5、#39和#53字节指令的注释,以了解switch()语句如何处理字符串。
In the #5 and #8 byte, first, hashCode() method has been executed and switch(int) has been executed by using the result of executing hashCode() method.
在#5和#8字节处,首先是调用了hashCode()方法,然后它作为参数调用了switch(int)。
In the braces of the lookupswitch instruction, branch is made to the different location according to the hashCode result value. String "abc" is hashCode result value 96354, and is moved to #36 byte. String "123" is hashCode result value 48690, and is moved to #50 byte.
在lookupswitch的指令里,根据hashCode的结果进行不同的分支跳转。字符串“abc"的hashCode是96354,它会跳转到#36处。字符串”123“的hashCode是48690,它会跳转到#50处。
In the #36, #37, #39, and #42 bytes, you can see that the value of the str variable received as an argument is compared using the String "abc" and the equals() method. If the results are identical, '0' is inserted to the #3 index of the local variable array, and the string is moved to the #61 byte.
在第#36,#37,#39,以及#42字节的地方,你可以看见str参数被equals()方法来和字符串“abc”进行比较。如果比较的结果是相等的话,‘0’会被放入到局部变量数组的索引为#3的位置,然后跳转到第#61字节。
In this way, in the #50, #51, #53, and #56 bytes, you can see that the value of the str variable received as an argument is compared by using the String "123" and the equals() method. If the results are identical, '1' is inserted to the #3 index of the local variable array and the string is moved to the #61 byte.
在第#50,#51,#53,以及#56字节的地方,你可以看见str参数被equals()方法来和字符串“123”进行比较。如果比较的结果是相等的话,'1'会被放入到局部变量数组的索引为#3的位置,然后跳转到第#61字节。
In the #61 and #62 bytes, the value of the #3 index of the local variable array, i.e., '0', '1', or any other value, is lookupswitched and branched.
在第#61和#62字节的地方,局部变量数组里索引为#3的值,这里是'0',‘1’或者其他的值,被lookupswitch用来进行搜索并进行相应的分支跳转。
In other words, in Java code, the value of the str variable received as the switch() argument is compared using the hashCode() method and the equals() method. With the result int value, switch() is executed.
换句话来说,在Java代码里的用来作为switch()的参数的字符串str变量是通过hashCode()和equals()方法来进行比较,然后根据比较的结果,来执行swtich()语句。
In this result, the compiled bytecode is not different from the previous JVM specifications. The new feature of Java SE 7, String in switch is processed by the Java compiler, not by the JVM itself. In this way, other new features of Java SE 7 will also be processed by the Java compiler.
在这个结果里,编译后的字节码和之前版本的JVM规范没有不兼容的地方。Java SE 7的这个用字符串作为switch参数的特性是通过Java编译器来处理的,而不是通过JVM来支持的。通过这种方式还可以把其他的Java SE 7的新特性也通过Java编译器来实现。
结束语
我不认为为了使用好Java必须去了解Java底层的实现。许多没有深入理解JVM的开发者也开发出了很多非常好的应用和类库。不过,如果你更加理解JVM的话,你就会更加理解Java,这样你会有助于你处理类似于我们前面的案例中的问题。
除了这篇文章里提到的,JVM还是用了其他的很多特性和技术。JVM规范提供了是一种扩展性很强的规范,这样就使得JVM的提供者可以选择更多的技术来提高性能。值得特别说明的一点是,垃圾回收技术被大多数使用虚拟机的语言所使用。不过,由于这个已经在很多地方有更加专业的研究,我这篇文章就没有对它进行深入讲解了。