10. 对象实例化内存布局与访问定位
10.1 对象的实例化
10.1.1 创建对象的方式
10.1.2 创建对象的步骤
public class ObjectTest {
public static void main(String[] args) {
Object obj = new Object();
}
}
从字节码角度分析 :
-
new:类型加载、确定在堆中开辟空间所需大小、零值初始化 (如, boolean = false, int = 0, double = 0.0)
-
dup:两个指向堆空间实例,接近栈底的用于赋值操作,接近栈顶的作为句柄 (相关方法)
-
invokespecial:调用相关方法(<init>类构造器:显式初始化)
-
astore_1:将引用取出,放置在局部变量表中角标为1的位置
从执行步骤的角度分析 :
- 判断对象对应的类 是否
加载
、链接
、初始化
- 虚拟机遇到一条new指令,首先去检查这个指令的参数是否能在Metaspace的常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析、初始化(即判断类元信息是否存在)。
- 如果没有,那么在双亲委托模式下,使用当前类加载以
ClassLoader + 包名 + 类名
为 key 进行查找对应的.class
文件。如果没有找到文件,则抛出ClassNotFoundException
异常,如果找到,则进行类加载,并生成对应的 Class 对象。
- 为对象分配内存
- 首先计算对象占用空间大小,接着在堆中划分一块内存给新对象。
- 如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小。
- 如果【内存规整】使用指针碰撞
- 如果内存是规整的,那么虚拟机将采用的是指针碰撞法(Bump The Pointer)来为对象分配内存 。意思是所有用过的内存在一边,空闲的内存在另外一边,中间放着一个指针作为分界点的指示器,分配内存就仅仅是把指针向空闲那边挪动一段与对象大小相等的距离。
- 如果垃圾收集器选择的是Serial (串行)、ParNew (并行) 这种基于压缩算法的,虚拟机采用这种分配方式。一般使用带有compact(整理)过程的收集器时,使用指针碰撞。
- 如果【内存不规整】虚拟机需要维护一个列表,使用空闲列表分配
- 如果内存不是规整的,已使用的内存和未使用的内存相互交错,那么虚拟机将采用的时空闲列表法来为对象分配内存。
- 虚拟机维护了一个列表,记录上哪些内存块是可用的,再分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的内容 。这种分配方式称为 “空闲列表(Free List)”。
- 说明:选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。
- 处理并发安全问题
- 在分配内存空间时,另外一个问题是即使保证new对象时的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。虚拟机采用了两种方式解决并发问题:
- CAS(Compare And Swap)失败重试、区域加锁:保证指针更新操作的原子性;
- TLAB 把内存分配的动作按照线程划分在不同空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区(TLAB, Thread Local Allocation Buffer)虚拟机是否使用TLAB,可以通过
-XX:+/-UseTLAB
参数来设定。
- 在分配内存空间时,另外一个问题是即使保证new对象时的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。虚拟机采用了两种方式解决并发问题:
- 初始化分配到的空间
- 内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数值类型所对应的零值。
- 设置对象的对象头
- 将对象的所属类(类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
- 执行<init>方法进行初始化
- 在Java程序的视角看来,初始化才正式开始。初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
- 因此一般来说(由字节码中是否跟随invokespecial指令所决定),new指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。
给对象赋值的操作 :
- 属性的默认初始化
- 显式初始化 / 代码块中初始化
- 构造器中初始化
测试代码:
public class Customer {
int id = 1001;
String name;
Account acct;
{
name = "匿名客户";
}
public Customer() {
this.acct = new Account();
}
}
class Account {
}
字节码分析:
10.2 对象的内存布局
10.2.1 对象头(Header)
① 运行时元数据(Mark Word)
- 哈希值(HashCode)
- GC 分代年龄
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
② 类型指针
- 指向类元数据InstanceKlass,确定该对象所属的类型
说明:如果是数组,还需记录数组长度。
10.2.2 实例数据(Instance Data)
① 说明
- 它是对象真正的有效信息,包括程序代码中定义的各种类型的字段(包括从父类继承、本身拥有的字段)
② 规则
- 相同宽度的字段总是被分配在一起
- 父类中定义的变量会出现在子类之前
- 如果CompactFields参数为true (默认为true):子类的窄变量可能插入到父类变量的空袭
10.2.3 对齐填充(Padding)
不是必须的,也没有特别含义,仅仅起到占位符的作用。
10.3 对象的访问定位
创建对象的目的:使用它
10.3.1 JVM 是如何通过栈帧中的对象引用访问到其内部的对象实例的呢?
定位,通过栈上reference访问。
10.3.2 对象访问的方式
① 句柄访问
实现
将堆中内存分为两块,一块内存用来做句柄池,另一块内存用来做对象池。reference 中存储对象的句柄池地址,句柄中包含对象实例与对象类型的地址信息。
好处
reference 中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改。
② 直接指针(HotSpot采用)
实现
不对堆空间做分割。reference 中存储对象的实例地址,对象实例包含到对象类型数据的地址信息。
好处
无需将堆空间分为两部分,空间占用清晰明了。实例中包含类型数据,布局更加合理。
11. 直接内存(Direct Memory)
11.1 概述
- 不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
- 直接内存是在Java堆外的、直接向系统申请的内存空间。
- 来源于NIO (JDK4 - NIO / JDK7- NIO2),通过存在堆中的DirectByteBuffer操作Native内存。
- 通常,访问直接内存的速度会优于Java堆,即读写性能高。
- 因此出于性能考虑,读写频繁的场合可能会考虑使用直接内存。
- Java的 NIO库 / Unsafe核心类允许Java程序使用直接内存 (本地内存),用于数据缓冲区。
11.2 读写对比
11.2.1 传统I/O方式
读写文件,需要与磁盘交互,需要由用户态切换到内核态。在内核态时,需要内存操作如下图。
使用IO,这里需要两份内存存储重复数据,效率低。
11.2.2 NIO方式
使用NIO时,操作系统划出的直接缓冲区可以被Java代码直接访问,只有一份。NIO适合堆大文件的读写操作。
11.3 直接内存OOM
-
直接内存也可能导致
OutOfMemoryError
异常:java.lang.OutOfMemoryError: Direct buffer memory
-
由于直接内存存在Java堆外,因此它的大小不会直接受限于
-Xmx
指定的最大堆大小,但是系统内存是有限的,Java堆和直接内存的总和依然受限于操作系统能给出的最大内存。 -
缺点:
- 分配回收成本较高
- 不受JVM内存回收管理
我们可以使用MAT工具查看dump文件,若dump文件较小,但出现了异常,则可以推断直接/间接使用到的本地内存出现了问题。
11.4 直接内存大小设置
- 直接内存大小可以通过
MaxDirectMemorySize
设置。 - 如果不指定,默认与堆的最大值
-Xmx
参数值一致。
12. 执行引擎
12.1 执行引擎概述
- 执行引擎时Java虚拟机核心的组成部分之一。
- “虚拟机” 是一个相对于 “物理机” 的概念,这两种及其都有代码执行能力。
- 物理机的执行引擎是直接建立在处理器、缓存、指令集、操作系统层面上的。
- 虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地指定指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
JVM的主要任务是负责 装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表、其他辅助信息。
如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是 将字节码指令解释/编译为对应平台上的本地机器指令。简单来说,JVM的执行引擎充当了将高级语言翻译为机器语言的译者。
- 执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
- 每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
- 当然方法在执行的过程中,执行引擎有可能会通过存储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
- 从外观上来看,所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。
12.2 Java代码 编译+执行 过程
12.2.1 编译+执行 过程
大部分的程序代码转换成物理机的目标代码或虚拟机能执行的指令集之前,都需要经过上图的各个步骤。
在生成抽象语法树后,会遍历语法树形成线性字节码指令流。
Java代码编译是由Java源码编译器来完成 (流程图如下):
Java字节码的执行是由JVM执行引擎来完成 (流程图如下):
12.2.2 什么是解释器 (Interpreter)?
解释器:
- 当Java虚拟机启动时,会根据预定义的规范对 字节码采用逐行解释的方式执行,将每条字节码文件中的内容 “翻译” 为对应平台的本地机器指令执行。
12.2.3 什么是JIT编译器?
JIT (Just-In-Time Compiler) 编译器:
- 指虚拟机将源代码直接编译成和本地机器平台相关的机器语言。
12.2.4 为什么说Java是半编译半解释型语言?
JDK 1.0 时代,将 Java 语言定位为 “解释执行” 还是比较准确的。再后来,Java也发展出可以直接生成本地代码的编译器。
现在JVM在执行Java代码的时候,通常 会将解释执行与编译执行二者结合起来进行。
12.3 机器码、指令、汇编语言
12.3.1 机器码
- 各种用二进制编码表示的指令,叫做 机器指令码。开始,人们就利用它编写程序,这就是机器语言。
- 机器语言虽然能够被计算机理解和接受,但和人们的语言差别太大,不易被人们理解和记忆,并且用它编程容易出差错。
- 用它编写的程序一经输入计算机,CPU直接读取运行,因此和其他语言编写的程序相比,执行速度最快。
- 机器指令与CPU紧密相关,所以不同种类的CPU所对应的机器指令也就不同。
12.3.2 指令
- 由于机器码是由0和1组成的二进制序列,可读性实在太差,于是人们发明了指令。
- 指令就是把机器码中特定的0和1序列,简化成对应的指令(一般为英文简写:如 mov, inc 等),可读性稍好
- 由于不同的硬件平台,执行同一个操作,对应的机器码可能不同 ,所以不同的硬件平台的同一种指令(比如mov),对应的机器码也可能不同。
12.3.3 指令集
- 不同的硬件平台,各自支持的指令 ,是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集。
- x86指令集,对应的是x86架构的平台
- ARM指令集,对应的是ARM架构的平台
12.3.4 汇编语言
- 由于指令的可读性还是太差,于是人们又发明了汇编语言。
- 在汇编语言中,用 助记符(Mnemonics)代替 机器指令的操作码,用 地址符号(Symbol)或 标号(Label)代替 指令或操作数的地址。
- 在不同的硬件平台,汇编语言对应着不同的机器语言指令集,通过汇编过程转换成机器指令。
- 由于计算机只认识指令码,所以 用汇编语言编写的程序还必须翻译成机器指令码,计算机才能识别、执行。
12.3.5 高级语言
- 为了使计算机用户编写程序更容易,后来就出现了各种高级计算机语言。高级语言比机器语言、汇编语言 更接近人的语言。
- 当计算机执行高级语言编写的程序时,仍然需要把程序解释和编译成机器的指令码。完成这个过程的程序就叫做解释程序/编译程序。
12.3.6 字节码
- 字节码是一种中间状态(中间码)的二进制代码(文件),它比机器码更抽象,需要直译器转译后才能成为机器码
- 字节码主要为了实现特定软件运行的软件环境,与硬件环境无关。
- 字节码的实现方式是通过编译器和虚拟机器。编译器将源码编译成字节码,特定平台上的虚拟机器将字节码转译成可以直接执行的指令。
- 字节码的典型应用为 Java bytecode
12.4 解释器
12.4.1 概述
JVM 设计者们的初衷仅仅只是单纯地 为了满足Java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而诞生了实现解释其在运行时采用逐行解释字节码执行程序的想法。
12.4.2 解释器工作机制 (工作任务)
- 解释器真正意义上所承担的角色就是一个运行时 “翻译者”,将字节码文件中的内容 “翻译” 为对应平台的本地机器指令执行。
- 当一条字节码指令被解释执行完成后,接着再根据PC寄存器中记录的下一条需要被执行的字节码指令执行解释操作。
12.4.3 解释器分类
在Java的发展历史里,一共有两套解释执行器,即古老的 字节码解释器、现在普遍使用的 模板解释器。
- 字节码解释器在执行时通过 纯软件代码 模拟字节码的执行,效率非常低下。
- 模板解释器将 每一条字节码 和 一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,从而很大程度上提高了解释器的性能。
- 在 HotSpot VM 中,解释其主要由 Interpreter 模块和 Code 模块构成。
- Interpreter模块:实现了解释器的核心功能
- Code模块:用于管理 HotSpot VM 在运行时生成的本地机器指令
- 在 HotSpot VM 中,解释其主要由 Interpreter 模块和 Code 模块构成。
12.4.4 现状
- 由于解释器在设计和实现上简单,因此除了Java语言值外,还有许多高级语言同样也是基于解释器执行的,比如Python、Perl、Ruby等。但是在今天,基于解释器执行已经沦为低效的代名词,并且时常被一些C/C++程序员所调侃。
- 为了解决这个问题,JVM平台支持一种叫做即时编译的技术。即时编译的目的是避免函数被解释执行,而是 将整个 函数体编译成机器码,每次函数执行时,只执行编译后的机器码即可,这种方式可以使执行效率大幅度提升。
- 不过无论如何,基于解释器的执行模式仍然为中间语言的发展做出了 不可磨灭的贡献。
12.5 JIT编译器
12.5.1 Java代码的执行分类
- 第一种是将源代码编译成字节码文件,然后在运行时通过解释器将字节码转为机器码执行
- 第二种是编译执行(直接编译成机器码)。现代虚拟机为了提高执行效率,会使用即时编译技术(JIT, Just In Time),将方法编译成机器码后再执行。
- HotSpot VM是目前市面上高性能虚拟机的代表作之一。它 采用解释器与即时编译器并存的架构。在Java虚拟机运行时,解释器和即时编译器能够互相协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。
- 在今天,Java程序的运行性能早已脱胎换骨,已经达到了可以和C/C++程序一较高下的地步。
12.5.2 为什么要使用JIT编译器结合解释器?
有些开发人员会感到诧异,既然HotSpot VM 中已经内置了JIT编译器了,那么为什么还需要再使用解释器来 “拖累” 程序的执行性能呢?比如 JRockit VM 内部就不包含解释器,字节码全部依靠即使编译器编译后执行。
- 当程序启动后,解释器可以马上发挥作用,省去编译时间,立即执行。编译器想要 发挥作用,把代码编译成本地代码,需要一定的执行时间。但编译为本地代码后,执行效率高。
- 尽管 JRockit VM 中程序的执行性能会非常高效,但程序在启动时必然需要花费更长的时间来进行编译。对于服务端来说,启动时间并非是关注重点,但对于那些看重启动时间的应用场景而言,或许就需要采用解释器与即时编译器并存的架构来换取一个平衡点。在此模式下,当Java虚拟机启动时,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成后再执行,这样可以省去许多不必要的编译时间。随着时间的推移,编译器发挥作用,把越来越多的代码编译成本地代码,获得更高的执行效率。
- 同时,解释执行在编译器进行激进优化 不成立的时候,作为编译器的 “逃生门”。
12.5.3 HotSpot JVM 的执行方式
当虚拟机启动的时候,解释器可以首先发挥作用,而不必等待即时编译器全部编译完成再执行,这样可以省去许多不必要的编译时间。并且随着程序运行时间的推移,即时编译器逐渐发乎作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。
12.5.4 案例说明
注意解释执行与编译执行在线上环境微妙的辩证关系。机器在热机状态可以承受的负载要大于冷机状态。如果以热机状态时的流量进行切流,可能使处于冷机状态的服务器因无法承载流量而假死。
在生产环境发布过程中,以分批的方式进行发布根据机器数量划分成多个批次,每个批次的机器数至少占到整个集群的 1/8。曾经又这样的故障案例:某程序员在发布平台进行分批发布,在输入发布总批数时,误填写成两批发布。如果是热机状态,在正常情况下一半的机器可以勉强承载流量,但由于刚启动的JVM均是解释执行,还没有进行热点代码统计和JIT动态编译,导致机器启动之后,当前1/2发布成功的服务器马上全部宕机,此故障说明了JIT的存在 。
12.5.5 概述
Java语言的 ‘“编译期” 其实是一段 “不确定” 的操作过程。
- 它可能是指一个 前端编译器(编译器的前端),把
.java
文件转变成.class
文件的过程。 - 也可能是指虚拟机的 后端运行期编译器(JIT 编译器, Just In Time Compilier)把字节码转变成机器码的过程。
- 还可能是指使用 静态提前编译器(AOT 编译器, Ahead Of Time Compiler)直接把
.java
文件编译成本地机器代码的过程。
前端的编译器 :Sun 的 Javac、Eclipse JDT 中的增量式编译器(ECJ)
JIT编译器 :HotSpot VM 的 C1/ C2 编译器
AOT 编译器 :GUN Compiler for the Java(GCJ)、Excelsior JET
12.5.6 热点代码及探测方式
当然是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用 执行的频率 而定。关于那些需要被编译为本地代码的字节码,也被称之为 “热点代码”,JIT编译器在运行时会针对那些频繁被调用的 “热点代码” 做出 深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。
- 一个被多次调用的方法,或是一个方法体内部循环次数较多的循环体都可以被称之为 “热点代码”,因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为 OSR(On Stack Replacement)编译。
- 一个方法究竟 要被调用多少次,或者一个循环体究竟需要执行多少次u徐娜换才可以达到这个标准?必然需要一个明确的阈值,JIT编译器才会将这些 “热点代码” 编译为本地机器指令执行。这里主要依靠 热点探测功能。
- 目前HotSpot VM 所采用的热点探测方式是基于计数器的热点探测。
- 采用基于计数器的热点探测,HotSpot VM 将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器(Invocation Counter)和 回边计数器(Back Edge Counter)。
- 方法调用计数器用于统计方法的调用次数
- 回边计数器用于统计循环体执行的循环次数
12.5.7 方法调用计数器
- 这个计数器用于统计方法被调用的次数,它的默认阈值在 Client 模式下是 1500 次,在 Server 模式下是 10000 次。超过这个阈值,就会触发JIT编译。
- 这个阈值可以通过虚拟机参数
-XX:CompilerThreshold
来人为设定。 - 当一个方法被调用时,会先检查该方法是否存在被 JIT 编译过的版本,如果存在,则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本,则将此方法的调用计数器的值加1,然后判断 方法调用计数器与回边计数器之和 是否超过方法调用计数器的阈值。如果已超过阈值,那么将会向即时编译器提交一个该方法的代码编译请求。
※ 热度衰减
- 如果不做任何设置,方法调用计数器统计的并不是方法调用的绝对次数,而是一个相对的执行频率,即 一段时间之内方法被调用的次数。当超过 一定时间限度,如果方法的调用次数仍然不足以让它提交给即时编译器编译,那这个方法的调用计数器就会被 减少一半,这个过程称为方法调用计数器热度的 衰减 (Counter Decay),而这段时间就成为此方法统计的 半衰周期(Counter Half Life Time)。
- 进行热度衰减的动作是在虚拟机进行垃圾收集时顺便进行的,可以使用虚拟机参数
-XX:UseCounterDecay
来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。 - 另外可以使用
-XX:CounterHalfLifeTime
参数设置半衰周期的时间,单位是秒。
12.5.8 回边计数器
它的作用是统计一个方法中 循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为 “回边”(Back Edge)。显然,建立回边计数器统计的目的就是为了触发 OSR 编译。
12.5.9 HotSpot VM 可以设置程序执行方式
缺省情况下 HotSpot VM 是采用解释器与即时编译器并存的架构,当然开发人员可以根据具体的应用场景,通过命令显式地为Java虚拟机指定在运行时到底是 完全采用解释器,还是 完全采用即时编译器 执行。如下所示:
-Xint
:完全采用解释器模式执行程序。-Xcomp
:完全采用即时编译器模式执行程序。如果即时编译出现问题,解释器会介入执行。-Xmixed
:采用解释器 + 即使编译器的混合模式共同执行程序。
测试代码:
public class IntCompTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
testPrimeNumber(1000000);
long end = System.currentTimeMillis();
System.out.println("花费的时间为:" + (end - start));
}
public static void testPrimeNumber(int count) {
for (int i = 0; i < count; i++) {
// 计算100以内的质数
label: for (int j = 0; j <= 100; j++) {
for (int k = 2; k <= Math.sqrt(j); k++) {
if (j % k == 0) {
continue label;
}
}
}
}
}
}
纯解释器运行:
纯编译器运行:
混合模式运行:
12.5.10 HotSpot 中 JIT分类
在 HotSpot VM 中嵌有两个JIT编译器,分别为 Client Compiler 和 Server Compiler,但大多数情况下我们简称为 C1编译器 和 C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底是用哪一款即时编译器。
-client
:指定 Java虚拟机运行在Client模式下,并使用C1编译器(32-bits)- C1编译器会对字节码进行 简单和可靠的优化,耗时短。以达到更快的编译速度。
-server
:指定Java虚拟机运行在Server模式下,并使用C2编译器(64-bits)- C2编译器进行 耗时较长的优化,以及激进优化。但优化的代码执行效率更高。
分层编译(Tiered Compilation)策略:程序解释执行(不开启性能监控)可以触发C1编译,将字节码编译成机器码,可以进行简单优化,也可以加上性能监控,C2编译会根据性能监控信息进行激进优化。
不过在Java7 版本之后,一旦开发人员在程序中显式指定命令 -server
时,默认将会开启分层编译策略,由 C1编译器 和 C2编译器相互协作共同来执行编译任务。
※ C1和C2编译器不同的优化策略
- 在不同的编译器上有不同的优化策略,C1编译器上主要有方法内联、去虚拟化、冗余消除。
- 方法内联:将引用的函数代码到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程
- 去虚拟化:对唯一的实现类进行内联
- 冗余消除:在运行期间把一些不会执行的代码折叠掉
- C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化:
- 标量替换:用标量值代替聚合对象的属性值
- 栈上分配:对于未逃逸的对象分配在栈而不是堆
- 同步消除:清楚同步操作,通常指synchronized
总结:
- 一般来讲,JIT编译出来的机器码性能比解释器高。
- C2编译器启动比C1译器慢,系统稳定执行以后,C2编译器执行速度远远快于C1编译器。
12.5.11 Graal 编译器
- 自 JDK 10 起,HotSpot又加入一个全新的即时编译器:Graal 编译器。
- 编译效果短短几年间就追平了C2编译器,未来可期。
- 目前,带着 “实验状态” 标签,需要使用开关参数
-XX:+UnlockExperimentalVMOptions
和-XX:+UseJVMCICompiler
去激活,才可以使用。
12.5.12 关于AOT编译器
-
JDK9 引入了AOT 编译器(静态提前编译器,Ahead Of Time Compiler)
-
Java 9 引入了实验性 APO 编译工具jaotc。它借助了 Graal 编译器,将所输入的 Java 类文件转换为机器码,并存放至生成的动态共享库中。
-
所谓 AOT 编译,是与即时编译相对立的一个概念。我们知道,即时编译是指在 程序运行过程中,将字节码转化为可在硬件上直接运行的机器码,并部署至托管环境中的过程。而 AOT 编译指的是,在 程序运行 之前,便将字节码转换为机器码的过程。
-
最大好处:Java虚拟机加载已经预编译成二进制库,可以直接执行。不必等待即使编译器的预热,减少Java应用给人带来 “第一次运行慢” 的不良体验。
-
缺点:
- 破坏了Java “一次编译,到处运行”,必须为每个不同硬件、OS编译对应的发行包。
- 降低了Java链接过程的动态性,加载的代码在编译期就必须全部已知。
- 还需要继续优化,最初只支持 Linux x86 java base。