读书人

Zz 深入了解JVM内幕:从基本结构到Jav

发布时间: 2014-01-14 23:14:00 作者: rapoo

Zz 深入理解JVM内幕:从基本结构到Java 7新特性

深入理解JVM内幕:从基本结构到Java 7新特性摘要:许多没有深入理解JVM的开发者也开发出了很多非常好的应用和类库。不过,如果你更加理解JVM的话,你就会更加理解Java,这样你会有助于你处理类似于我们前面的案例中的问题。

每个Java开发者都知道Java字节码是执行在JRE(Java Runtime Environment Java运行时环境)上的。JRE中最重要的部分是Java虚拟机(JVM),JVM负责分析和执行Java字节码。Java开发人员并不需要去关心JVM是如何运行的。在没有深入理解JVM的情况下,许多开发者已经开发出了非常多的优秀的应用以及Java类库。不过,如果你了解JVM的话,你会更加了解Java的,并且你会轻松解决那些看似简单但是无从下手的问题。

因此,在这篇文件里,我会阐述JVM是如何运行的,包括它的结构,它如何去执行字节码,以及按照怎样的顺序去执行,同时我还会给出一些常见错误的示例以及对应的解决办法。最后,我还会讲解Java 7中的一些新特性。

虚拟机(Virtual Machine)

JRE是由Java API和JVM组成的。JVM的主要作用是通过Class Loader来加载Java程序,并且按照Java API来执行加载的程序。

虚拟机是通过软件的方式来模拟实现的机器(比如说计算机),它可以像物理机一样运行程序。设计虚拟机的初衷是让Java能够通过它来实现WORA(Write Once Run Anywher 一次编译,到处运行),尽管这个目标现在已经被大多数人忽略了。因此,JVM可以在不修改Java代码的情况下,在所有的硬件环境上运行Java字节码。

Java虚拟机的特点如下:

  1. 基于栈的虚拟机:Intel x86和ARM这两种最常见的计算机体系的机构都是基于寄存器的。不同的是,JVM是基于栈的。
  2. 符号引用:除了基本类型以外的数据(类和接口)都是通过符号来引用,而不是通过显式地使用内存地址来引用。
  3. 垃圾回收机制:类的实例都是通过用户代码进行创建,并且自动被垃圾回收机制进行回收。
  4. 通过对基本类型的清晰定义来保证平台独立性:传统的编程语言,例如C/C++,int类型的大小取决于不同的平台。JVM通过对基本类型的清晰定义来保证它的兼容性以及平台独立性。
  5. 网络字节码顺序:Java class文件用网络字节码顺序来进行存储:为了保证和小端的Intel x86架构以及大端的RISC系列的架构保持无关性,JVM使用用于网络传输的网络字节顺序,也就是大端。

虽然是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字节码之前,我们先通过实例来简单了解它。这个案例是一个在开发环境出现的真实案例的总结。

现象

一个一直运行正常的应用突然无法运行了。在类库被更新之后,返回下面的错误。

  1. Exception?in?thread?14) ?
  2. ????at?com.nhn.service.UserService.main(UserService.java:19)?

应用的代码如下,而且它没有被改动过。

  1. //?UserService.java ?
  2. … ?
  3. }?

更新后的类库的源代码和原始的代码如下。

  1. //?UserAdmin.java?-?Updated?library?source?code ?
  2. … ?
  3. ????User?prevUser?=?userMap.put(userName,?user); ?
  4. ????} ?
  5. //?UserAdmin.java?-?Original?library?source?code ?
  6. … ?
  7. ????User?user?=?}?

简而言之,之前没有返回值的addUser()被改修改成返回一个User类的实例的方法。不过,应用的代码没有做任何修改,因为它没有使用addUser()的返回值。

咋一看,com.nhn.user.UserAdmin.addUser()方法似乎仍然存在,如果存在的话,那么怎么还会出现NoSuchMethodError的错误呢?

原因

上面问题的原因是在于应用的代码没有用新的类库来进行编译。换句话来说,应用代码似乎是调了正确的方法,只是没有使用它的返回值而已。不管怎样,编译后的class文件表明了这个方法是有返回值的。你可以从下面的错误信息里看到答案。

  1. java.lang.NoSuchMethodError:?com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)?

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对UserService.add()方法进行反汇编得到的。

  1. ???0:???aload_0 ?
  2. ???1:???getfield????????#15;?//Field?admin:Lcom/nhn/user/UserAdmin; ?
  3. ???4:???aload_1 ?
  4. ???5:???invokevirtual???#23;?//Method?com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)V ?
  5. ???8:???
  6. ???0:???aload_0 ?
  7. ???1:???getfield????????#15;?//Field?admin:Lcom/nhn/user/UserAdmin; ?
  8. ???4:???aload_1 ?
  9. ???5:???invokevirtual???#23;?//Method?com/nhn/user/UserAdmin.addUser:(Ljava/lang/String;)Lcom/nhn/user/User; ?
  10. ???8:???pop ?
  11. ???9:???
  12. 2a?b4?00?0f?2b?b6?00?17?57?b1?

表一:Java字节码中的类型表达式在Java字节码里,类的实例用字母“L;”表示,void 用字母“V”表示。通过这种方式,其他的类型也有对应的表达式。下面的表格对此作了总结。

Zz 深入了解JVM内幕:从基本结构到Java 7新特性

下面的表格给出了字节码表达式的几个实例。

表二:Java字节码表达式范例

Zz 深入了解JVM内幕:从基本结构到Java 7新特性

想了解更多细节的话,参考《The java Virtual Machine Specification,第二版》中的“4.3 Descriptors"。想了解更多的Java字节码的指令的话,参考《The Java Virtual Machined Instruction Set》的“6.The Java Virtual Machine Instruction Set"

Class文件格式

在讲解Java class文件格式之前,我们先看看一个在Java Web应用中经常出现的问题。

当我们编写完Jsp代码,并且在Tomcat运行时,Jsp代码没有正常运行,而是出现了下面的错误。

现象

当我们编写完Jsp代码,并且在Tomcat运行时,Jsp代码没有正常运行,而是出现了下面的错误。

  1. Servlet.service()?65535?bytes?limit" ?

原因在不同的Web服务器上,上面的错误信息可能会有点不同,不过有有一点肯定是相同的,它出现的原因是65535字节的限制。这个65535字节的限制是JVM规范里的限制,它规定了一个方法的大小不能超过65535字节。

下面我会更加详细地讲解这个65535字节限制的意义以及它出现的原因。

Java字节码里的分支和跳转指令分别是”goto"和"jsr"。

  1. goto_w?[branchbyte1]?[branchbyte2]?[branchbyte3]?[branchbyte4] ?
  2. jsr_w?[branchbyte1]?[branchbyte2]?[branchbyte3]?[branchbyte4]?

有了这两个指令,索引超过65535的分支也是可用的。因此,Java方法的65535字节的限制就可以解除了。不过,由于Java class文件的更多的其他的限制,使得Java方法还是不能超过65535字节。

为了展示其他的限制,我会简单讲解一下class 文件的格式。

Java class文件的大致结构如下:

  1. ClassFile?{ ?
  2. ????u4?magic; ?
  3. ????u2?minor_version; ?
  4. ????u2?major_version; ?
  5. ????u2?constant_pool_count; ?
  6. ????cp_info?constant_pool[constant_pool_count-1]; ?
  7. ????u2?access_flags; ?
  8. ????u2?this_class; ?
  9. ????u2?super_class; ?
  10. ????u2?interfaces_count; ?
  11. ????u2?interfaces[interfaces_count]; ?
  12. ????u2?fields_count; ?
  13. ????field_info?fields[fields_count]; ?
  14. ????u2?methods_count; ?
  15. ????method_info?methods[methods_count]; ?
  16. ????u2?attributes_count; ?
  17. ????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文件的格式。

读书人网 >编程

热点推荐