陈同学
微服务
Accelerator
About
# 深入JVM内幕 > 原文:[Understanding JVM Internals](https://www.cubrid.org/blog/understanding-jvm-internals/) by **Se Hoon Park** On 05/30/2017 > 翻译:[码代码的陈同学](https://chenyongjun.vip/) > 翻译参考:[java字节序、主机字节序和网络字节序扫盲贴](https://blog.csdn.net/aitangyong/article/details/23204817) 众所周知,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包。 ```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) ``` 程序代码如下,而且没有任何变更. ```java // UserService.java … public void add(String userName) { admin.addUser(userName); } ``` Library中被更新的部分的前后代码如下: ```java // 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文件却表明这个方法是有返回值的。可以通过下面的错误消息来了解: ```java 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命令产生的: ```java 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字节的操作数。 通过编译上面例子中变更后的代码再反汇编得到的结果如下: ```java 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_0*, *getfield*, 和 *invokevirtual* 这些操作码都是用1个字节来表示(aload_0 = 0x2a, getfield = 0xb4, invokevirtual = 0xb6)。因此,Java字节码指令的操作码最多有256个。 像aload_0、aload_1这种操作码不需要任何操作数,因此,aload_0后面的下一个字节会是下一条指令的操作码。然后,getfield和invokevirtual需要2个字节的操作数,因此,getfield之后的下一条指令会跳过2字节,是写在第4个字节上。下面是通过16进制编辑器看到的字节码: ``` 2a b4 00 0f 2b b6 00 17 57 b1 ``` 在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代码并没有正常运行,而是报了如下错误: ```java 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字节码中的分支和跳转指令分别是 **goto** 和 **jsr**: ```java goto [branchbyte1] [branchbyte2] jsr [branchbyte1] [branchbyte2] ``` 这两个指令都接收一个2字节的分支偏移量(有符号数)作为它们的操作数,因此偏移量最大为65535(2字节为16位)。然而,为了支持更多的分支,Java字节码准备了 **goto_w** 和 **jsr_w **这两个可以接收4字节有符号数分支偏移量的指令。 ```java goto_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4] jsr_w [branchbyte1] [branchbyte2] [branchbyte3] [branchbyte4] ``` 通过这两个指令,索引超过65535的分支也是OK的,这样Java方法65535字节的限制也许就搞定了。然而,由于class文件格式的其他限制,Java方法还是不能超过65535字节。为了了解其他限制,我先简单介绍下class文件的格式。 下面是class文件的格式: ``` 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* 得到的数据如下: ```java 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提供商可以决定怎么配置堆或者不对它进行垃圾回收。 让我们回到前面讨论的反汇编的字节码。 ```java 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规范以及结语部分。
本文由
cyj
创作,可自由转载、引用,但需署名作者且注明文章出处。
文章标题:
深入JVM内幕
文章链接:
https://chenyongjun.vip/articles/50
扫码或搜索 cyjrun 关注微信公众号, 结伴学习, 一起努力