JVM架构模型图:
机器码
用二进制编码方式表示的指令(010101...等等),叫做机器指令码,也叫硬编码。最初人们采用它编写程序,这就是机器语言。
指令
指令集
不同的硬件平台,各自支持的指令是有差别的。因此每个平台所支持的指令,称之为对应平台的指令集。
汇编语言(硬件级别的语言,不能跨平台)
高级语言
JVM主要负责装载字节码到其内部,但字节码并不能够运行在操作系统之上,因为字节码指令并非机器码,且只能JVM识别,所以为了让Java程序运行起来,JVM中的执行引擎扮演将高级语言翻译为机器语言的角色,以及对字节码优化和执行GC清理垃圾。
JVM执行引擎包含:解释器(2种)、JIT即时编译器、GC。
JVM设计初期,仅仅为满足Java程序实现跨平台特性,因此避免采用静态编译的方式直接生成本地机器指令,从而实现了在运行时采用逐行解释字节码到机器码执行程序的方案。
JVM发展史中,共有两套解释执行器:
在HotSpot VM中,解释器主要由Interpreter模块和Code模块构成。
由于解释器在设计和实现上非常简单,且相当低效。为了解决这个问题,JVM提供了即时编译技术:即时编译可以将整个函数体编译成为机器码,有效到避免函数体被解释执行,而是重复执行时直接执行编译后的机器码即可,大大提示了执行效率。
HotSpot VM目前采用解释器与即时编译器并存的架构,解释器和即时编译器相互协作,各自取长补短,选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。
HotSpot VM默认的程序执行方式:
HotSpot VM可以设置程序执行方式有三种,可以通过JVM命令指定:
三种方式执行效率测试:
// 不同模式下执行三次所需的时间,单位ms
// -Xint 6962、6537、6627
// -Xcomp 1032、877、897
// -Xmixed 1283、921、977
// 测试表面:纯解释器模式速度最慢
public class JITTest {
public static void main (String[] args) {
long start = System.currentTimeMillis();
getSum(1000000);
long end = System.currentTimeMillis();
System.out.println(end - start);
}
public static void getSum(int count) {
for (int i = 0; i < count; i++) {
// 计算100以内的质数
label:for(int j = 2;j <= 100;j++){
for(int k = 2;k <= Math.sqrt(j);k++){
if(j % k == 0){
continue label;
}
}
}
}
}
}
在HotSpot VM中内嵌两个JIT编译器:Client Compiler(C1编译器)和Server Compiler(C2编译器)。
JVM相关命令配置:
C1和C2编译器不同的优化策略:
相关概念:
前端编译器:把.java文件转变成.class文件。包括Sun的Javac、Eclipse JDT中的增量式编辑器(ECJ)。
后端运行期即时编译器(JIT编译器,Just In Time Compiler):把字节码转成机器码。包括HotSpot VM的C1、C2编译器。
静态提前编译器(AOT编译器,Ahead Of Time Compiler):把*.java编译成本地机器码。包括GNU Compiler for the Java(GCJ)、Excelsior JET。
AOT编译器
Graal编译器
为什么说Java是半解释半编译型语言?
为什么解释器依然存在?
热机切冷机故障?
热机指在生产环境运行一段时间的机器/容器,冷机指刚启动的机器/容器。当服务集群需要扩容或者流量切换操作时,如果以热机状态的流量标准切换到冷机上面,可能会导致服务器马上宕机。
因为机器在热机状态可以承受的负载要大于冷机状态。刚启动的JVM均是解释执行,还没有进行热点代码统计和JIT动态编译,所以是很低效的。
JVM根据代码被调用执行的频率,通过指定条件判断是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令。
一个被多次调用的方法,或者是一个方法体内循环多次的循环体都可以被称之为"热点代码";需要被编译为本地代码的字节码,也被称之为"热点代码",因此热点代码都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR (On Stack Replacement)编译。
衡量一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到JIT即使编译的标准。
HotSpot VM目前采用的热点探测方式是基于计数器的热点探测:
用于统计方法被调用的次数,它的默认阈值在Client 模式 下是1500 次,在Server 模式下是10000 次,超过这个阈值,就会触发JIT编译。阈值可以通过JVM参数-XX:CompileThreshold来设定。
// 通过java -XX:+PrintFlagsFinal查看JVM的参数最终值
// 通过java -XX:+PrintFlagsInitial查看JVM的参数初识值
// 通过grep过滤出关键词
// 查看服务端模式下方法调用次数阀值
java -XX:+PrintFlagsFinal -version -client | grep CompileThreshold
intx CompileThreshold = 10000 {pd product}
uintx IncreaseFirstTierCompileThresholdAt = 50 {product}
intx Tier2CompileThreshold = 0 {product}
intx Tier3CompileThreshold = 2000 {product}
intx Tier4CompileThreshold = 15000 {product}
java version "1.8.0_241"
Java(TM) SE Runtime Environment (build 1.8.0_241-b07)
Java HotSpot(TM) 64-Bit Server VM (build 25.241-b07, mixed mode)
触发OSR编译逻辑图:
用于统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边” (Back Edge)。当然建立回边计数器统计的目的是为了触发OSR编译。
触发OSR编译逻辑图:
代码:
public class EscapeAnalysis {
public static void getEscapeAnalysis() {
EscapeAnalysis ea = new EscapeAnalysis();
}
public static void main(String[] args) throws Exception {
long t1 = System.currentTimeMillis();
for (int i=0; i<5000000; i++) {
getEscapeAnalysis();
}
long t2 = System.currentTimeMillis();
System.out.println("test执行时间:" + (t2 - t1));
Thread.sleep(1000000);
}
}
// 启动命令,关闭逃逸分析
java -XX:-DoEscapeAnalysis edward.com/EscapeAnalysis
// 查看EscapeAnalysis实例个数
jmap -histo 10882
理论上:for循环了500_0000次,EscapeAnalysis实例个数应该是循环多少遍,就生成多少实例;
实际上:EscapeAnalysis实例个数只有91_6238个,可能由于GC机制导致大部分实例被回收,则调大堆内存避免GC。
// 启动命令,关闭逃逸分析,执行堆内存大小
java -XX:-DoEscapeAnalysis -Xmx3G -Xmn2G edward.com/EscapeAnalysis
// 查看EscapeAnalysis实例个数
jmap -histo 10925
可以发现调大堆内存后实例对象一个不少的都在堆上进行分配。
// 查看JVM参数命令
java -XX:+PrintFlagsFinal -version | grep CompileThreshold
默认CompileThreshold阀值为10000。
// 启动命令,开启逃逸分析,执行JIT阀值为20000
java -XX:+DoEscapeAnalysis -XX:CompileThreshold=20000 -Xmx3G -Xmn2G edward.com/EscapeAnalysis
// 查看EscapeAnalysis实例个数
jmap -histo 11305
理论上:CompileThreshold=20000,EscapeAnalysis实例个数应该和阀值次数相同,生成多少实例;
实际上:EscapeAnalysis实例个数有10_8045个,远大于阀值数据;
// 启动命令,开启逃逸分析,执行JIT阀值为20000,关闭分层编译和异步编译
java -XX:+DoEscapeAnalysis -XX:CompileThreshold=20000 -XX:-TieredCompilation -XX:-BackgroundCompilation -Xmx3G -Xmn2G edward.com/EscapeAnalysis
// 查看EscapeAnalysis实例个数
jmap -histo 11428
EscapeAnalysis实例个数和阀值配置次数相同,CompileThreshold=20000。