目标
- JVM基本常识
- JVM架构理解
- JVM程序执行流程
- JIT使用
- JIT优化
JVM基本常识:
- 程序的执行方式有哪些?(静态编译执行,动态编译执行,动态解释执行)
- 为什么使用JVM?(跨平台)
- 字节码和机器码的区别(都是二进制文件,机器码能被计算机直接读取运行,字节码不能,需经过直译器转译)
- JDK、JRE与JVM的关系(图1)
- OracleJDK和OpenJDK (openJDK开源免费,oracleJDK收费,二者功能上基本一致)
- JVM和Hotspot的关系 (JVM是 《JVM虚拟机规范》中提出来的规范,Hotspot是JVM规范的商用产品)
- JVM和Java的关系(JAVA编译产生class文件,可以运行在JVM上,其他语言也可以编译 产生class文件。 如Groovy语言)
- JVM的运行模式(Server和Client模式,区别Client启动快,Server稳定运行后,程序运行比client模式程序快,Server模式采用重量级虚拟机,对程序采用了许多优化)
JVM架构理解(图2)分为三大部分:
- 类加载子系统(Class Loader SubSystem)
- 运行时数据区(Runtime Data Areas)
- 执行引擎( Execution Engine)
JVM程序执行流程
Java程序最初是通过
解释器(Interpreter)进行解释执行, 在运行时,虚拟机通过
即时编译器(Just In Time Compiler,下文统称
JIT编译器)将热点代码编译成与本地平台相关的机器码,并进行各种层次的优化。
- Java编译成字节码、动态编译和解释为机器码的过程分析 图示:
- 编译器和解释器的协调工作流程
JIT编译(just-in-time compilation)又叫“
即时编译”。JIT编译是动态编译的一种特例)
热点代码:包括 运行过程中会被多次调用的方法 ;被多次执行的循环体,两种。这两种情况,编译器都是以整个方法作为编译对象。 这种编译方法因为编译发生在方法执行过程之中,因此形象的称之为栈上替换(On Stack Replacement,OSR),即方法栈帧还在栈上,方法就被替换了。
- 哪些程序代码会被即时编译?(热点代码会被即时编译)
- 如何判断热点代码? 要知道方法或一段代码是不是热点代码,是不是需要触发即时编译,需要进行Hot Spot Detection(热点探测) 目前主要的热点探测方式有以下两种: 基于采样的热点探测 和 基于计数器的热点探测。
- HotSpot热点检测方式: 基于计数器的热点探测方法 Hotspot为每个方法准备了两个计数器:方法调用计数器和回边计数器。在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阈值,当计数器超过阈值溢出了,就会触发JIT编译。
- 方法调用计数器: 用于统计方法被调用的次数
- 回边计数器: 统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”
JIT使用
- 为什么要使用解释器与编译器并存的架构? 解释器与编译器特点
- 为何要实现两个不同的即时编译器? HotSpot虚拟机中内置了两个即时编译器:Client Complier和Server Complier,简称为C1、C2编译器,分别用在客户端和服务端。目前主流的HotSpot虚拟机中默认是采用解释器与其中一个编译器直接配合的方式工作。
JIT优化
-
公共子表达式的消除: 局部公共子表达式消除(Local Common Subexpression Elimination)和 全局公共子表达式消除(Global Common Subexpression Elimination)
-
方法内联: 在使用JIT进行即时编译时, 将方法调用 直接使用方法体中的代码 进行替换,称为方法内联 ,减少了方 法调用过程中压栈与入栈的开销。
-
逃逸分析: 逃逸分析(Escape Analysis) 是一种可以有效减少 Java 程序 中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。 逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中 ,称为 方法逃逸。
逃逸分析包括:
-
全局变量赋值逃逸
-
方法返回值逃逸
-
实例引用发生逃逸
-
线程逃逸:赋值给类变量或可以在其他线程中访问的实例变量
使用逃逸分析,编译器可以对代码做如下优化:
对象的栈上内存分配: 一般情况下,对象和数组元素的内存分配是在堆内存上进行的。但是通过JIT编译器的很多优化使这种分配策略并不绝对。
JIT
编译器就可以在编译期间根据逃逸分析的结果,来决定是
否可以将对象的内存分配从堆转化为栈。
开启了逃逸分析之后,在运行期间,
因为很多堆上分配被优化成了栈上分配,所以GC次数会明显减少。所以,
不是所有的对象和数组都会在堆内存分配空间。
标量替换:
标量(Scalar
)是指一个无法再分解成更小的数据的数据。 在JIT阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT优化,就会把这个对象拆解成若干个其中包含的若干个成员变量来代替。
同步锁消除:同样基于逃逸分析,当加锁的变量不会发生逃逸,是线程私有的完全没有必要加锁。
在JIT
编译时期就
可以将同步锁去掉,以减少加锁与解锁造成的资源开销。