前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入JVM内幕

深入JVM内幕

作者头像
码代码的陈同学
发布2018-06-24 11:52:17
1.3K3
发布2018-06-24 11:52:17
举报
文章被收录于专栏:码代码的陈同学

原文:Understanding JVM Internals by Se Hoon Park On 05/30/2017 翻译:码代码的陈同学 翻译参考:java字节序、主机字节序和网络字节序扫盲贴

众所周知,Java字节码运行在JRE(Java Runtime Environment)中,JVM又是JRE中最重要的部分,主要用于分析和执行字节码。虽然不深入了解JVM,开发人员也已经开发出许多优秀的应用和Library,但如果了解JVM,你可以更好的理解Java语言,同时也可以解决一些看上去很简单却不好解决的问题。

因此,本文我将阐述JVM如何工作、JVM的结构、JVM如何执行字节码以及执行的顺序,常见的错误及其解决方案,也会介绍下Java SE 7的新特性。

虚拟机(Virtual Machine)

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

虚拟机可以像物理机一样运行程序,它是通过软件的方式来模拟实现的机器(例如计算机)。Java被设计成基于虚拟机运行的初衷是希望通过和物理机分离以达到 WORA(Write One Run Anywhere)的目标,尽管这个目标早已被淡忘。正因如此,JVM才可以既不改变Java代码却又能运行在各种硬件上。

JVM的特性如下:

  • 基于栈的虚拟机(Stack-Based VM):Intel x86 和ARM这两种最为流行的架构都是基于寄存器运行的,然而JVM却是基于Stack运行。
  • 符号引用(Symbolic reference):除基本数据类型外,所有的数据类型(类和接口)都是通过符号引用来引用,而不是通过具体的内存地址来引用。
  • 垃圾回收(Garbage Collection):对象由用户编写的代码创建,由垃圾回收机制自动销毁。
  • 通过对基本数据类型的明确定义来保证平台独立性:像C/C++这种传统语言,int类型的长度取决于平台。JVM明确定义了基本数据类型来确保它的兼容性和独立性。
  • 网络字节序(Network byte order):Java class文件使用了网络字节序,为了在小端字节序(如Intel x68体系)和大端字节序(如RISC系列体系)之间维持平台独立性,必须保证固定的字节顺序。因此,JVM使用了用于网络传输的网络字节序,网络字节序属于大端。

虽然Sun公司开发了Java,但是所有JVM提供商都可以基于JVM规范开发自己的JVM。正因如此,市面上有许多不同的虚拟机,包含Oracle的HotSpot JVM和IBM JVM。Google 安卓操作系统中的Dalvik虚拟机也是一种JVM,尽管它没有基于JVM规范,不像基于Stack的Java虚拟机,Dalvik虚拟机是基于寄存器的架构,Dalvik虚拟机会将Java字节码转换成基于寄存器的指令集。

字节码(Java bytecode)

为了实现 WORA 目标,JVM使用字节码这种介于Java(用户语言)和机器语言之间的中间语言,字节码是部署Java代码的最小单位。

在解释Java字节码之前,让我们先看看它的样子。下面是一个开发过程中遇到的真实案例总结:

现象

一个一直运行的程序在某个依赖的Library被更新后发生了如下错误.

译者注:为了便于理解,译者举个例子。例如:一个应用的war包没做任何变更,但是替换了某个依赖的jar包。

代码语言:txt
复制
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)

程序代码如下,而且没有任何变更.

代码语言:txt
复制
// UserService.java
…
public void add(String userName) {
    admin.addUser(userName);
}

Library中被更新的部分的前后代码如下:

代码语言:txt
复制
// 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()方法也存在,那为什么还要报 NoSuchMethodError 呢?

原因

原因在于应用程序的代码没有基于新的Library重新编译,换句话说,程序中还是执行了正确的方法,只是没有返回值而已。然而,编译后的class文件却表明这个方法是有返回值的。可以通过下面的错误消息来了解:

代码语言:txt
复制
java.lang.NoSuchMethodError: com.nhn.user.UserAdmin.addUser(Ljava/lang/String;)V

由于找不到方法报了NoSuchMethodError,看一下 Ljava/lang/String;和后面的 V,在Java字节码表达式中,L<classname>; 表示类实例, Ljava/lang/String;表示方法有一个String类型的参数。在上面的例子中,参数没有变化,所以是正常的。最后的V表示方法的返回值,只有一个V表示没有返回值。上述异常消息表示没有找到这个方法。

由于程序代码是根据以前的Library编译的,class文件中并没有定义有返回值的addUser()方法。然而,在Library变更后,addUser()更新成了返回一个User的方法。因此,发生了 NoSuchMethodError

注:这个错误的发生是由于开发人员没有使用新的Library重新编译应用,但是,这种场景下,Library的提供者更应该为此负责。一个公共的没有返回值的方法变更成了一个返回一个对象的方法,这显然是变更类的签名信息,这也意味着打破了这个Library的向后兼容性。因此,Library的提供者必须告知使用者Library发生了变更。

让我们回到Java字节码,Java字节码是JVM的基本元素。JVM是一个模拟执行字节码的模拟器,Java编辑器不会将高级语言(如C/C++)转换成机器语言(CPU指令),它会将开发人员可以理解的Java语言转换成JVM可以理解的Java字节码。由于Java字节码是平台无关的代码,因此即使CPU或操作系统不同,它也可以运行在所有安装了JVM(准确的说,是与硬件匹配的JRE)的硬件上(一个class文件在 Windows PC上编译后不做任何改变就可以运行在Linux上)。编译后的字节码和源代码的大小基本一致,这样可以更容易的在网络上传输和执行编译后的代码。

class文件本身是一个开发人员无法理解的二进制文件,为了管理这些文件,JVM提供商提供了反汇编器javap,javap产生的结果是Java汇编语言。下面的代码是通过javap命令产生的:

代码语言:txt
复制
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

在这段汇编代码中,addUser()方法是在第4行的5: invokevirtual #23; 执行的,这表示对应的索引为23的方法会被执行,#23是由javap进行标记的。invokevirtual是Java字节码中最基本的操作码,一共有4种执行方法的操作码:invokeinterface, invokespecial, invokestatic, 和 invokevirtual.,其含义如下:

  • invokeinterface: 执行一个接口的方法
  • invokespecial: 执行一个初始化方法,私有方法或父类中的方法
  • invokestatic: 执行静态方法
  • invokevirtual:执行对象实例中的方法

Java字节码的指令集由操作码(OpCode)操作数(Operand)组成,像 invokevirtual 这样的操作码需要2字节的操作数。

通过编译上面例子中变更后的代码再反汇编得到的结果如下:

代码语言:txt
复制
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;

那上面的汇编代码中,最前面的数字又是什么意思呢?

这是字节数(byte number),也许这就是为什么JVM中执行的代码叫字节码的原因。简而言之,字节码指令中的像 aload_0getfield, 和 invokevirtual 这些操作码都是用1个字节来表示(aload_0 = 0x2a, getfield = 0xb4, invokevirtual = 0xb6)。因此,Java字节码指令的操作码最多有256个。

像aload_0、aload_1这种操作码不需要任何操作数,因此,aload_0后面的下一个字节会是下一条指令的操作码。然后,getfield和invokevirtual需要2个字节的操作数,因此,getfield之后的下一条指令会跳过2字节,是写在第4个字节上。下面是通过16进制编辑器看到的字节码:

代码语言:txt
复制

2a b4 00 0f 2b b6 00 17 57 b1

代码语言:txt
复制

在Java字节码中,"L"表示类实例,"V"表示void,其他类型也有它们自己的表达式,下面是字节码中的其他表达式:

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;

class文件格式(Class File Format)

在解释Java class文件格式之前,我们先回顾下在Java Web应用中经常出现的情景。

现象

在Tomcat上运行JSP时,JSP代码并没有正常运行,而是报了如下错误:

代码语言:txt
复制
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的一个限制,用来保证一个方法的 size不能超过65535字节。

我将详细的说明65535字节限制的意义以及为什么设置了这个限制。

Java字节码中的分支和跳转指令分别是 gotojsr

代码语言:txt
复制
goto [branchbyte1] [branchbyte2]
jsr [branchbyte1] [branchbyte2]

这两个指令都接收一个2字节的分支偏移量(有符号数)作为它们的操作数,因此偏移量最大为65535(2字节为16位)。然而,为了支持更多的分支,Java字节码准备了 goto_wjsr_w 这两个可以接收4字节有符号数分支偏移量的指令。

代码语言:txt
复制
goto_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]
jsr_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4]

通过这两个指令,索引超过65535的分支也是OK的,这样Java方法65535字节的限制也许就搞定了。然而,由于class文件格式的其他限制,Java方法还是不能超过65535字节。为了了解其他限制,我先简单介绍下class文件的格式。

下面是class文件的格式:

代码语言:txt
复制
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虚拟机》这本书。

javap以用户可读的格式简要的展示class文件的信息,使用javap -verbose 命令分析UserService.class 得到的数据如下:

代码语言:txt
复制
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 有关,method_info 结构包含了Code,LineNumberTable和LocalVariableTable,如上述 javap -verbose命令输出的信息。Code属性里的LineNumberTable、LocalVariableTable、Exception_table的长度都是用一个固定的2字节来表示,因此,方法的大小不能超过它们长度65535的限制。

许多人抱怨方法长度的限制,尽管JVM规范里表明后续会对此进行拓展,但到目前为止也没有任实质性的进展。考虑到JVM把class文件中的很多内容放到了方法区,为了保证向后兼容性,拓展方法的长度将愈加艰难。

如果由于Java编译错误创建了错误的class文件会发生什么呢?或者,因为网络传输或文件拷贝时class文件被损坏呢?

为了预防这种情况,JVM class loader有一个非常严格的校验过程。

JVM结构(JVM Structure)

Java中编写的代码通过以下流程来执行。

一个Class Loader装载编译后的字节码到运行时数据区,然后执行引擎用于执行Java字节码。

类装载器(Class Loader)

Java提供了一种动态装载特性,它可以在运行时首次引用某个class时对它进行装载和链接,而不是在编译时进行。JVM的class loader用于进行动态装载,下面是class loader的几个特性:

  • 层级结构(Hierarchical Structure): Java里的class loader被组织成了有父子关系的层级结构。Bootstrap class loader是所有class loader的父类。
  • 委派模型(Delegation mode): 基于层级结构,类的装载可以在class loaders之间进行委派。当一个class装载之时,会首先检查它是否在父装载器中进行了装载。如果上层装载器已经装载了这个类,将会直接使用这个类;如果没有,当前类装载器将会请求装载这个类。
  • 可见性限制(Visibility limit): 一个子装载器可以查找父装载器中的类,但是父装载器不能查找子装载器中的类。
  • 不允许卸载(Unload is not allowed): 类装载器可以装载一个类但是不能卸载它,不过可以删除当前类装载器,然后创建一个新的类装载器。

每一个class loader都一个自己的命名空间来保存装载的类,当一个class loader装载一个类时,它会使用类的全限定名(FQCN: Fully Qualified Class Name)去命名空间中查找类是否被装载。需要注意的是即使类的全限定名相同,但如果命名空间不同,也会被认为是不同的类,命名空间不同意味着类已经被其他class loader装载了。

下图演示了class loader的委派模型:

当一个class loader请求加载一个class时,它首先按顺序在上层装载器、父装载器以及自身的装载器缓存中检查类是否已存在。简单来说,首先会检查自己是否装载了该类,如果没有将继续检查父装载器,最后如果在Bootstrap装载器中都没有找到的话,将会从文件系统装载这个类。

  • 启动类装载器(Bootstrap class loader): 它在JVM启用时创建,它负责装载Java APIs,包含相关对象的class。不像其他class loader,这个类装载器由native代码实现,而不是Java代码。
  • 拓展类装载器(Extension class loader): 它负责装载除了Java API外的拓展类,也负责装载其他安全拓展功能。
  • 系统类装载器(System class loader): 如果说bootstrap class loader和 extension class loader是用来装载JVM组件的,那system class loader就是用来装载应用程序的类。它会装载用户在$CLASSPATH中指定的类。
  • 用户自定义类装载器(User-defined class loader): 这是用户在程序中直接创建的类装载器。

Web应用服务器(WAS)等框架会使用这种结构让Web应用和企业级应用保持独立运行,换句话说,通过class loader的委派模型来保证应用的独立性,不同WAS服务商的class loader在层级结构上可能稍有不同。

如果一个class loader找到了一个未装载的类,这个类装载和链接的流程如下图:

  • 加载(Loading): 从class文件获取类信息并加载到JVM内存
  • 验证(Verifying): 检查class是否符合Java语言规范和JVM规范,这是类装载中最为复杂的流程,会花费很长时间。大多数JVM TCK测试case就是用来测试在装载类的时候是否会出现错误
  • 准备(Preparing): 准备一个数据结构用来存储类信息,结构中包含:类的成员变量、方法和接口信息。
  • 解析(Resolving): 将这个类的常量池中所有的符号引用换成直接引用。
  • 初始化(Initializing): 将类的成员变量初始化成合适的值,执行静态初始化程序,把静态变量初始化成合适的值。

JVM规范中定义了上面几个任务,但是在执行时可以进行灵活的变动。

运行时数据区(Runtime Data Area)

运行时数据区是JVM程序运行在操作系统上时的内存分配区域,它可以分称6个部分:程序计数器,JVM栈、本地方法栈都是为每个线程创建的,堆、方法区、运行时常量池是所有线程共享的。

  • 程序计数器(PC register): 每个线程有自己的程序计数器 (Program Counter) , 它在线程start的时候创建。.程序计数器保存了当前正在执行的JVM指令的地址。
  • JVM stack: 每个线程拥有一个JVM栈 , 它在线程start的时候创建。主要用来保存栈帧,JVM只会在Stack上进行栈帧的push和pop操作。如果发生任何异常,stack中的每一行都代表一个栈帧信息,这些信息可以通过像printStackTrace()这样的方法展示出来。
  • 本地方法栈(Native method stack): 提供给非Java语言写的本地方法使用的stack。换句话说,它是一个用于通过JNI(Java Native Interface)执行C/C++代码的stack,根据具体的语言,会创建一个C stack或C++ stack。
  • 方法区(Method area): 方法区被所有线程共享,在JVM启动时创建,它存储了运行时常量池、变量和方法信息,静态变量,class中每个方法的字节码以及接口信息。不同JVM提供商对于方法区有不同的实现,Oracle HotSpot JVM把它称为永久区(Permanent Area)或永久代(Permanent Generation (PermGen)),是否对方法区进行垃圾回收对于JVM的实现来说也是可选的。
  • 运行时常量池(Runtime constant pool): 这个区域和class文件中的常量池表(contant_pool table)对应,它属于方法区。由于在JVM操作中它却扮演着核心角色,因此JVM规范中单独提到了它的重要性。除了每个类和接口中的常量,它也包含了方法和变量中的所有引用。简而言之,当一个方法或变量被引用时,JVM会从运行时常量区检索方法或者变量的实际地址。
  • 堆(Heap): 是用于保存实例和对象的空间,也是垃圾回收的主要区域。当讨论JVM性能问题时,这个区域会频繁提及。JVM提供商可以决定怎么配置堆或者不对它进行垃圾回收。

让我们回到前面讨论的反汇编的字节码。

代码语言:txt
复制
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

Java汇编代码

对比Java汇编代码和x86架构下的汇编代码我们可以发现二者的格式有点相似:都有操作码;然而,有一点不同是在Java字节码中不会在操作数中写入寄存器名称、内存地址或偏移量。上面说过,JVM使用stack,因此,它不像x86架构直接使用寄存器,由于JVM自己管理内存,所以使用像15、23这样的索引数来代替内存地址。15和23都是当前类(UserService class)常量池中的索引。简而言之,JVM为每个类创建了一个常量池用于存储实际的引用。

上面每一行反汇编代码的含义如下:

  • aload_0: 将局部变量表中索引为#0的变量添加到操作数栈,#0永远表示的是this,即当前实例的引用。
  • getfield #15:将当前类常量池中索引为#15的变量添加到操作数栈,添加了UserAdmin的admin属性,由于admin属性是一个类的实例,因此添加的是一个引用。
  • aload_1: 将局部变量表中索引为#1的变量添加到操作数栈,局部变量表中#1位置的变量是一个方法的参数,因此,在执行add()方法时,会将字符串参数userName的引用添加到操作数栈。
  • invokevirtual #23: 执行当前类常量池中索引为#23的方法,此时,通过getfield添加的引用和aload_1添加的参数都将用于方法执行。当方法执行完后,返回值将会添加到操作数栈。
  • pop: 使用invokevirtual指令将返回值从操作数栈中pop出来,最上面的例子中,通过最开始的Library编译的方法是没有返回值的,由于方法没有返回值,因此没有必要将返回值从stack中Pop出来。
  • return: 结束方法。

下图可以辅助你理解上面的信息:

另外,在这个方法中,局部变量表并没有发生变化,所以上图只展示了操作数栈的变化。然而,在大多数场景中,局部变量表会发生变更,数据会通过一些load指令(如aload,iload)和store指令(如astore,istore)在局部变量表和操作数栈中进行传输。

在图中,我们简单演示验证了运行时常量池和JVM Stack。在JVM运行时,类实例将在堆中进行分配,而像User、UserAdmin、UserService这些类信息和字符串将被存储到方法区。

执行引擎(Execution Engine)

字节码通过class loader加载到JVM的运行时数据区,然后由执行引擎执行。执行引擎像CPU一条一条执行机器码一样以指令为单位读取Java字节码,每个字节码指令由1个字节的操作码和附加的操作数组成。执行引擎获取一个操作码再结合操作数来执行任务,执行完后再执行下一条操作码。

与可以被机器直接执行的语言相比,字节码是以一种人类可读的语言来编写。因此,字节码必须在JVM中转换成一种可以被机器直接执行的语言。字节码可以通过以下两种方式转换成合适的语言:

  • 解释器(Interpreter):逐条读取、解释、执行字节码指令。由于是逐条解释、执行指令,所以可以很快的解释字节码,但是执行起来却比较慢,这也是解释型语言的缺陷,字节码这种"语言"基本上就是解释执行的。
  • 即时编译器(JIT(Just-In-Time) Compiler):JIT编译器被引入用来弥补解释器的不足。执行引擎首先以解释执行的方式来运行,然后在合适的时间,会把所有字节码编译成本地代码(Native Code),自此之后,执行引擎可以直接执行本地代码,不再需要解释执行。本地代码的执行比逐条执行字节码快很多,尤其是本地代码存储到缓存之后,编译后的代码可以执行的更快。

然而,与解释器逐条解释字节码相比,JIT编译器会消耗更多的时间用于编译代码。因此,如果代码只会被执行一次,最好用解释执行而不是编译代码。正因如此,JVM内部会使用JIT编译器检查方法的执行频率,而且只会编译执行频率超过一定水平的代码。

图7: Java编译器和即时编译器

JVM规范中并未定义执行引擎如何运行,因此,不同JVM提供商会使用不同的技术来优化他们的执行引擎,也会引入不同种类的JIT编译器。

大多数的JIT编译器以下图的方式执行:

图8: 即时编译器

JIT编译器将字节码转换成中间层表达式,使用中间层表达式来进行优化,再把这种中间层表达式转换成本地代码。

Oracle的HotSpot虚拟机使用了一种叫做 热点编译器(HotSpot Compiler)的JIT编译器,之所以这么取名是因为是热点编译器会通过分析找到需要编译的 "热点" 代码,然后将代码编译成本地代码。如果方法编译后的字节码不再被频繁执行,换句话说,如果这个方法不再是热点,HotSpot虚拟机会将这个方法对应的本地代码从缓存中移除,并且会用解释器模式来执行。HotSpot虚拟机分成了Server VM 和 Client VM,这两部分使用不同的JIT编译器。

图9: HotSpot VM的Client VM和Server VM

Client VM和Server VM使用相同的运行时,不过如上图所示,它们的JIT编译器是不同的。Server VM使用了更高级的动态优化编译器,这个编译器使用很多复杂的性能优化技术。

IBM JVM自IBM JDK 6起引入了一种叫AOT(Ahead-Of-Time)的编译器作为JIT编译器,这意味着许多JVM通过共享缓存来共享编译过的本地代码,简单来说,就是其他JVM可以直接使用AOT编译器编译过的代码,而不用重新编译。另外,IBM JVM通过使用AOT编译器将代码预编译成一种JXE(Java EXecutable)文件格式来提供一种更快速的执行方式。

大多数Java性能的提高是通过优化执行引擎来完成的,正如JIT编译器中,很多JVM性能的持续提高都是通过就、引各种优化技术来完成。早期JVM和最近的JVM之间最大的不同就是执行引擎的差异。

Oracle HotSpot JVM自1.3版本开始引入了热点编译器,Davlvik VM自android 2.2开始也引入了JIT编译器。

译者注:已省略翻译最后的 JVM7规范以及结语部分。

本文系外文翻译,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系外文翻译前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 虚拟机(Virtual Machine)
  • 字节码(Java bytecode)
  • class文件格式(Class File Format)
  • JVM结构(JVM Structure)
    • 类装载器(Class Loader)
      • 运行时数据区(Runtime Data Area)
        • Java汇编代码
        • 执行引擎(Execution Engine)
        相关产品与服务
        云服务器
        云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档