英文原文:cubrid,编译:ImportNew - 朱伟杰html
译文连接:http://www.importnew.com/1486.htmljava
【如需转载,请在正文中标注并保留原文连接、译文连接和译者等信息,谢谢合做!】web
每一个Java开发者都知道Java字节码是执行在JRE((Java Runtime Environment Java运行时环境)上的。JRE中最重要的部分是Java虚拟机(JVM),JVM负责分析和执行Java字节码。Java开发人员并不须要去关心JVM是如何运行的。在没有深刻理解JVM的状况下,许多开发者已经开发出了很是多的优秀的应用以及Java类库。不过,若是你了解JVM的话,你会更加了解Java的,而且你会轻松解决那些看似简单可是无从下手的问题。apache
所以,在这篇文件里,我会阐述JVM是如何运行的,包括它的结构,它如何去执行字节码,以及按照怎样的顺序去执行,同时我还会给出一些常见错误的示例以及对应的解决办法。最后,我还会讲解Java 7中的一些新特性。编程
JRE是由Java API和JVM组成的。JVM的主要做用是经过Class Loader来加载Java程序,而且按照Java API来执行加载的程序。bootstrap
虚拟机是经过软件的方式来模拟实现的机器(好比说计算机),它能够像物理机同样运行程序。设计虚拟机的初衷是让Java可以经过它来实现WORA(Write Once Run Anywhere 一次编译,处处运行),尽管这个目标如今已经被大多数人忽略了。所以,JVM能够在不修改Java代码的状况下,在全部的硬件环境上运行Java字节码。数组
Java虚拟机的特色以下:缓存
虽然是Sun公司开发了Java,可是全部的开发商均可以开发而且提供遵循Java虚拟机规范的JVM。正是因为这个缘由,使得Oracle HotSpot和IBM JVM等不一样的JVM可以并存。Google的Android系统里的Dalvik VM也是一种JVM,虽然它并不遵循Java虚拟机规范。和基于栈的Java虚拟机不一样,Dalvik VM是基于寄存器的架构,所以它的Java字节码也被转化成基于寄存器的指令集。安全
为了保证WORA,JVM使用Java字节码这种介于Java和机器语言之间的中间语言。字节码是部署Java代码的最小单位。性能优化
在解释Java字节码以前,咱们先经过实例来简单了解它。这个案例是一个在开发环境出现的真实案例的总结。
一个一直运行正常的应用忽然没法运行了。在类库被更新以后,返回下面的错误。
1
2
3
|
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
)
|
应用的代码以下,并且它没有被改动过。
1
2
3
4
5
|
// UserService.java
…
public
void
add(String userName) {
admin.addUser(userName);
}
|
更新后的类库的源代码和原始的代码以下。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// UserAdmin.java - Updated library source code
…
public
User addUser(String userName) {
User user =
new
User(userName);
User prevUser = userMap.put(userName, user);
return
prevUser;
}
// UserAdmin.java - Original library source code
…
public
void
addUser(String userName) {
User user =
new
User(userName);
userMap.put(userName, user);
}
|
简而言之,以前没有返回值的addUser()被改修改为返回一个User类的实例的方法。不过,应用的代码没有作任何修改,由于它没有使用addUser()的返回值。
咋一看,com.nhn.user.UserAdmin.addUser()方法彷佛仍然存在,若是存在的话,那么怎么还会出现NoSuchMethodError的错误呢?
上面问题的缘由是在于应用的代码没有用新的类库来进行编译。换句话来讲,应用代码彷佛是调了正确的方法,只是没有使用它的返回值而已。无论怎样,编译后的class文件代表了这个方法是有返回值的。你能够从下面的错误信息里看到答案。
1
|
java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V
|
NoSuchMethodError出现的缘由是“com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V”方法找不到。注意一下”Ljava/lang/String;”和最后面的“V”。在Java字节码的表达式里,”L<classname>;”表示的是类的实例。这里表示addUser()方法有一个java/lang/String的对象做为参数。在这个类库里,参数没有被改变,因此它是正常的。最后面的“V”表示这个方法的返回值。在Java字节码的表达式里,”V”表示没有返回值(Void)。综上所述,上面的错误信息是表示有一个java.lang.String类型的参数,而且没有返回值的com.nhn.user.UserAdmin.addUser方法没有找到。
由于应用是用以前的类库编译的,因此返回值为空的方法被调用了。可是在修改后的类库里,返回值为空的方法不存在,而且添加了一个返回值为“Lcom/nhn/user/User”的方法。所以,就出现了NoSuchMethodError。
注:
这个错误出现的缘由是由于开发者没有用新的类库来从新编译应用。不过,出现这种问题的大部分责任在于类库的提供者。这个public的方法原本没有返回值的,可是后来却被修改为返回User类的实例。很明显,方法的签名被修改了,这也代表了这个类库的后向兼容性被破坏了。所以,这个类库的提供者应该告知使用者这个方法已经被改变了。
咱们再回到Java字节码上来。Java字节码是JVM很重要的部分。JVM是模拟执行Java字节码的一个模拟器。Java编译器不会直接把高级语言(例如C/C++)编写的代码直接转换成机器语言(CPU指令);它会把开发者能够理解的Java语言转换成JVM可以理解的Java字节码。由于Java字节码自己是平台无关的,因此它能够在任何安装了JVM(确切地说,是相匹配的JRE)的硬件上执行,即便是在CPU和OS都不相同的平台上(在Windows PC上开发和编译的字节码能够不作任何修改就直接运行在Linux机器上)。编译后的代码的大小和源代码大小基本一致,这样就能够很容易地经过网络来传输和执行编译后的代码。
Java class文件是一种人很难去理解的二进文件。为了便于理解它,JVM提供者提供了javap,反汇编器。使用javap产生的结果是Java汇编语言。在上面的例子中,下面的Java汇编代码是经过javap -c对UserServiceadd()方法进行反汇编获得的。
1
2
3
4
5
6
7
|
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
|
invokeinterface:调用一个接口方法在这段Java汇编代码中,addUser()方法是在第四行的“5:invokevitual#23″进行调用的。这表示对应索引为23的方法会被调用。索引为23的方法的名称已经被javap给注解在旁边了。invokevirtual是Java字节码里调用方法的最基本的操做码。在Java字节码里,有四种操做码能够用来调用一个方法,分别是:invokeinterface,invokespecial,invokestatic以及invokevirtual。操做码的做用分别以下:
Java字节码的指令集由操做码和操做数组成。相似invokevirtual这样的操做数须要2个字节的操做数。
用更新的类库来编译上面的应用代码,而后反编译它,将会获得下面的结果。
1
2
3
4
5
6
7
8
|
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”的方法。
在上面的反汇编代码里,代码前面的数字代码什么呢?
它表示的是字节数。大概这就是为何运行在JVM上面的代码成为Java“字节”码的缘由。简而言之,Java字节码指令的操做码,例如aload_0,getfield和invokevirtual等,都是用一个字节的数字来表示的(aload_0=0x2a,getfield=0xb4,invokevirtual=0xb6)。由此可知Java字节码指令的操做码最多有256个。
aload_0和aload_1这样的指令不须要任何操做数。所以,aload_0指令的下一个字节是下一个指令的操做码。不过,getfield和invokevirtual指令须要2字节的操做数。所以,getfiled的下一条指令是跳过两个字节,写在第四个字节的位置上的。十六进制编译器里查看字节码的结果以下所示。
1
|
2a b4
00
0f 2b b6
00
17
57
b1
|
表一:Java字节码中的类型表达式在Java字节码里,类的实例用字母“L;”表示,void 用字母“V”表示。经过这种方式,其余的类型也有对应的表达式。下面的表格对此做了总结。
Java Bytecode | Type | Description |
B | byte | signed byte |
C | char | Unicode character |
D | double | double-precision floating-point value |
F | float | single-precision floating-point value |
I | int | integer |
J | long | long integer |
L<classname> | reference | an instance of class <classname> |
S | short | signed short |
Z | boolean | true or false |
[ | reference | one array dimension |
下面的表格给出了字节码表达式的几个实例。
表二:Java字节码表达式范例
Java Code | Java Bytecode Expression |
double d[ ][ ][ ]; | [[[D |
Object mymethod(int I, double d, Thread t) | (IDLjava/lang/Thread;)Ljava/lang/Object; |
想了解更多细节的话,参考《The java Virtual Machine Specification,第二版》中的“4.3 Descriptors"。想了解更多的Java字节码的指令的话,参考《The Java Virtual Machined Instruction Set》的“6.The Java Virtual Machine Instruction Set"
在讲解Java class文件格式以前,咱们先看看一个在Java Web应用中常常出现的问题。
当咱们编写完jsp代码,而且在Tomcat运行时,Jsp代码没有正常运行,而是出现了下面的错误。
1
2
|
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"。
1
2
|
goto
[branchbyte1] [branchbyte2]
jsr [branchbyte1] [branchbyte2]
|
这两个指令都接收一个2字节的有符号的分支跳转偏移量作为操做数,所以偏移量最大只能达到65535。不过,为了支持更多的跳转,Java字节码提供了"goto_w"和"jsr_w"这两个能够接收4字节分支偏移的指令。
1
2
|
goto_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]
jsr_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]
|
有了这两个指令,索引超过65535的分支也是可用的。所以,Java方法的65535字节的限制就能够解除了。不过,因为Java class文件的更多的其余的限制,使得Java方法仍是不能超过65535字节。
为了展现其余的限制,我会简单讲解一下class 文件的格式。
Java class文件的大体结构以下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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];}
|
上面的内容是来自《The Java Virtual Machine Specification,Second Edition》的4.1节“The ClassFile Structure"。
以前反汇编的UserService.class文件反汇编的结果的前16个字节在十六进制编辑器中以下所示:
ca fe ba be 00 00 00 32 00 28 07 00 02 01 00 1b
经过这些数值,咱们能够来看看class文件的格式。
javap简要地给出了class文件的一个可读形式。当你用"java -verbose"命令来分析UserService.class时,会输出以下的内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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 …
}
|
javap输出的内容太长,我这里只是提出了整个输出的一部分。整个的输出展现了constant_pool里的不一样信息,以及方法的内容。
关于方法的65565字节大小的限制是和method_info struct相关的。method_info结构包含Code,LineNumberTable,以及LocalViriable attribute几个属性,这个在“javap -verbose"的输出里能够看到。Code属性里的LineNumberTable,LocalVariableTable以及exception_table的长度都是用一个固定的2字节来表示的。所以,方法的大小是不能超过LineNumberTable,LocalVariableTable以及exception_table的长度的,它们都是65535字节。
许多人都在抱怨方法的大小限制,并且在JVM规范里还说名了”这个长度之后有可能会是可扩展的“。不过,到如今为止,尚未为这个限制作出任何动做。从JVM规范里的把class文件里的内容直接拷贝到方法区这个特色来看,要想在保持后向兼容性的同时来扩展方法区的大小是很是困难的。
若是由于Java编译器的错误而致使class文件的错误,会怎么样呢?或者,由于网络传输的错误致使拷贝的class文件的损坏呢?
为了预防这种场景,Java的类装载器经过一个严格并且慎密的过程来校验class文件。在JVM规范里详细地讲解了这方面的内容。
注意
咱们怎样可以判断JVM正确地执行了class文件校验的全部过程呢?咱们怎么来判断不一样提供商的不一样JVM实现是符合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的测试。
Java编写的代码会按照下图的流程来执行
图 1: Java代码执行流程.
类装载器装载负责装载编译后的字节码,并加载到运行时数据区(Runtime Data Area),而后执行引擎执行会执行这些字节码。
Java提供了动态的装载特性;它会在运行时的第一次引用到一个class的时候对它进行装载和连接,而不是在编译期进行。JVM的类装载器负责动态装载。Java类装载器有以下几个特色:
每一个类装载器都有一个本身的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会经过保存在命名空间里的类全局限定名(Fully Qualified Class Name)进行搜索来检测这个类是否已经被加载了。若是两个类的全局限定名是同样的,可是若是命名空间不同的话,那么它们仍是不一样的类。不一样的命名空间表示class被不一样的类装载器装载。
下图展现了类装载器的代理模型。
图 2: 类加载器代理模型
当一个类装载器(class loader)被请求装载类时,它首先按照顺序在上层装载器、父装载器以及自身的装载器的缓存里检查这个类是否已经存在。简单来讲,就是在缓存里查看这个类是否已经被本身装载过了,若是没有的话,继续查找父类的缓存,直到在bootstrap类装载器里也没有找到的话,它就会本身在文件系统里去查找而且加载这个类。
相似于web应用服务(WAS)之类的框架会用这种结构来对Web应用和企业级应用进行分离。换句话来讲,类装载器的代理模型能够用来保证不一样应用之间的相互独立。WAS类装载器使用这种层级结构,不一样的WAS供应商的装载器结构有稍许区别。
若是类装载器查找到一个没有装载的类,它会按照下图的流程来装载和连接这个类:
图 3: 类加载的各个阶段
每一个阶段的描述以下:
JVM规范定义了上面的几个任务,不过它容许具体执行的时候可以有些灵活的变更。
图 4: 运行时数据区
运行时数据区是在JVM运行的时候操做所分配的内存区。运行时内存区能够划分为6个区域。在这6个区域中,一个PC Register,JVM stack 以及Native Method Statck都是按照线程建立的,Heap,Method Area以及Runtime Constant Pool都是被全部线程公用的。
图 5: JVM堆栈
如今咱们再会过头来看看以前反汇编的字节码
1
2
3
4
5
6
7
8
|
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
|
把上面的反汇编代码和咱们平时所见的x86架构的汇编代码相比较,咱们会发现这二者的结构有点类似,都使用了操做码;不过,有一点不一样的地方是Java字节码并不会在操做数里写入寄存器的名称、内存地址或者偏移量。以前已经说过,JVM用的是栈,它不会使用寄存器。和使用寄存器的x86架构不一样,它本身负责内存的管理。它用索引例如15和23来代替实际的内存地址。15和23都是当前类(这里是UserService类)的常量池里的索引。简而言之,JVM为每一个类建立了一个常量池,而且这个常量池里保存了实际目标的引用。
每行反汇编代码的解释以下:
下图能够帮助你更好地理解上面的内容。
图 6: Java字节码装载到运行时数据区示例
顺便提一下,在这个方法里,局部变量数组没有被修改。因此上图只显示了操做数栈的变化。不过,大部分的状况下,局部变量数组也是会改变的。局部变量数组和操做数栈之间的数据传输是使用经过大量的load指令(aload,iload)和store指令(astore,istore)来实现的。
在这个图里,咱们简单验证了运行时常量池和JVM栈的描述。当JVM运行的时候,每一个类的实例都会在堆上进行分配,User,UserAdmin,UserService以及String等类的信息都会保存在方法区。
经过类装载器装载的,被分配到JVM的运行时数据区的字节码会被执行引擎执行。执行引擎以指令为单位读取Java字节码。它就像一个CPU同样,一条一条地执行机器指令。每一个字节码指令都由一个1字节的操做码和附加的操做数组成。执行引擎取得一个操做码,而后根据操做数来执行任务,完成后就继续执行下一条操做码。
不过Java字节码是用一种人类能够读懂的语言编写的,而不是用机器能够直接执行的语言。所以,执行引擎必须把字节码转换成能够直接被JVM执行的语言。字节码能够经过如下两种方式转换成合适的语言。
不过,用JIT编译器来编译代码所花的时间要比用解释器去一条条解释执行花的时间要多。所以,若是代码只被执行一次的话,那么最好仍是解释执行而不是编译后再执行。所以,内置了JIT编译器的JVM都会检查方法的执行频率,若是一个方法的执行频率超过一个特定的值的话,那么这个方法就会被编译成本地代码。
图 7:Java编译器和JIT编译器
JVM规范没有定义执行引擎该如何去执行。所以,JVM的提供者经过使用不一样的技术以及不一样类型的JIT编译器来提升执行引擎的效率。
大部分的JIT编译器都是按照下图的方式来执行的:
图 8: JIT编译器
JIT编译器把字节码转换成一个中间层表达式,一种中间层的表示方式,来进行优化,而后再把这种表示转换成本地代码。
Oracle Hotspot VM使用一种叫作热点编译器的JIT编译器。它之因此被称做”热点“是由于热点编译器经过分析找到最须要编译的“热点”代码,而后把热点代码编译成本地代码。若是已经被编译成本地代码的字节码再也不被频繁调用了,换句话说,这个方法再也不是热点了,那么Hotspot VM会把编译过的本地代码从cache里移除,而且从新按照解释的方式来执行它。Hotspot VM分为Server VM和Client VM两种,这两种VM使用不一样的JIT编译器。
Figure 9: Hotspot Client VM and Server VM.
Client VM 和Server VM使用彻底相同的运行时,不过如上图所示,它们所使用的JIT编译器是不一样的。Server VM用的是更高级的动态优化编译器,这个编译器使用了更加复杂而且更多种类的性能优化技术。
IBM 在IBM JDK 6里不只引入了JIT编译器,它同时还引入了AOT(Ahead-Of-Time)编译器。它使得多个JVM能够经过共享缓存来共享编译过的本地代码。简而言之,经过AOT编译器编译过的代码能够直接被其余JVM使用。除此以外,IBM JVM经过使用AOT编译器来提早把代码编译器成JXE(Java EXecutable)文件格式来提供一种更加快速的执行方式。
大部分Java程序的性能都是经过提高执行引擎的性能来达到的。正如JIT编译器同样,不少优化的技术都被引入进来使得JVM的性能一直可以获得提高。最原始的JVM和最新的JVM最大的差异之处就是在于执行引擎。
Hotspot编译器在1.3版本的时候就被引入到Oracle Hotspot VM里了,JIT编译技术在Anroid 2.2版本的时候被引入到Dalvik VM里。
引入一种中间语言,例如字节码,虚拟机执行字节码,而且经过JIT编译器来提高JVM的性能的这种技术以及普遍应用在使用中间语言的编程语言上。例如微软的.Net,CLR(Common Language Runtime 公共语言运行时),也是一种VM,它执行一种被称做CIL(Common Intermediate Language)的字节码。CLR提供了AOT编译器和JIT编译器。所以,用C#或者VB.NET编写的源代码被编译后,编译器会生成CIL而且CIL会执行在有JIT编译器的CLR上。CLR和JVM类似,它也有垃圾回收机制,而且也是基于堆栈运行。
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》里的内容。主要的变化总结以下:
最大的改变是添加了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规范里没有对它进行描述。
Java SE 7里添加了不少新的语法和特性。不过,在Java SE 7的版本里,相对于语言自己而言,JVM没有多少的改变。那么,这些新的语言特性是怎么来实现的呢?咱们经过反汇编的方式来看看switch语句里的String(把字符串做为switch()语句的比较对象)是怎么实现的?
例如,下面的代码:
1
2
3
4
5
6
7
8
9
10
|
// SwitchTest
public
class
SwitchTest {
public
int
doSwitch(String str) {
switch
(str) {
case
"abc"
:
return
1
;
case
"123"
:
return
2
;
default
:
return
0
;
}
}
}
|
由于这是Java SE 7的一个新特性,因此它不能在Java SE 6或者更低版本的编译器上来编译。用Java SE 7的javac来编译。下面是经过javap -c来反编译后的结果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
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
|
在#5和#8字节处,首先是调用了hashCode()方法,而后它做为参数调用了switch(int)。在lookupswitch的指令里,根据hashCode的结果进行不一样的分支跳转。字符串“abc"的hashCode是96354,它会跳转到#36处。字符串”123“的hashCode是48690,它会跳转到#50处。生成的字节码的长度比Java源码长多了。首先,你能够看到字节码里用lookupswitch指令来实现switch()语句。不过,这里使用了两个lookupswitch指令,而不是一个。若是反编译的是针对Int的switch()语句的话,字节码里只会使用一个lookupswitch指令。也就是说,针对string的switch语句被分红用两个语句来实现。留心标号为#5,#39和#53的指令,来看看switch()语句是如何处理字符串的。
在第#36,#37,#39,以及#42字节的地方,你能够看见str参数被equals()方法来和字符串“abc”进行比较。若是比较的结果是相等的话,‘0’会被放入到局部变量数组的索引为#3的位置,而后跳抓转到第#61字节。
在第#50,#51,#53,以及#56字节的地方,你能够看见str参数被equals()方法来和字符串“123”进行比较。若是比较的结果是相等的话,10’会被放入到局部变量数组的索引为#3的位置,而后跳转到第#61字节。
在第#61和#62字节的地方,局部变量数组里索引为#3的值,这里是'0',‘1’或者其余的值,被lookupswitch用来进行搜索并进行相应的分支跳转。
换句话来讲,在Java代码里的用来做为switch()的参数的字符串str变量是经过hashCode()和equals()方法来进行比较,而后根据比较的结果,来执行swtich()语句。
在这个结果里,编译后的字节码和以前版本的JVM规范没有不兼容的地方。Java SE 7的这个用字符串做为switch参数的特性是经过Java编译器来处理的,而不是经过JVM来支持的。经过这种方式还能够把其余的Java SE 7的新特性也经过Java编译器来实现。
我不认为为了使用好Java必须去了解Java底层的实现。许多没有深刻理解JVM的开发者也开发出了不少很是好的应用和类库。不过,若是你更加理解JVM的话,你就会更加理解Java,这样你会有助于你处理相似于咱们前面的案例中的问题。
除了这篇文章里提到的,JVM仍是用了其余的不少特性和技术。JVM规范提供了是一种扩展性很强的规范,这样就使得JVM的提供者能够选择更多的技术来提升性能。值得特别说明的一点是,垃圾回收技术被大多数使用虚拟机的语言所使用。不过,因为这个已经在不少地方有更加专业的研究,我这篇文章就没有对它进行深刻讲解了。
对于熟悉韩语的朋友,若是你想要深刻理解JVM的内部结构的话,我推荐你参考《Java Performance Fundamental》(Hando Kim,Seoul,EXEM,2009)。这本书是用韩文写的,更适合你去阅读。我在写这本书的时候,参考了JVM规范,同时也参考了这本书。对于熟悉英语的朋友,你能够找到大量的关于Java性能的书籍。