大家好,今天我们一起聊聊 JVM 的即时编译及用到的逃逸分析。
通常情况下,Java 对象是被分配在堆上的,但 Java 对象一定是分配在堆上的么?通过本文的阐述,会找到对应的答案。
本文大纲
即时编译
JVM(Java 虚拟机)的即时编译(Just-In-Time Compilation,JIT)是一种将 Java 字节码动态转换为本地机器码的过程,这个过程发生在程序运行时。JIT 编译器的目标是提高程序的执行效率,特别是那些频繁执行的方法(热点代码)。
JIT 编译器工作原理的三个主要步骤
解释执行
Java 程序最初是通过解释器进行执行的。解释器逐条解释字节码指令并执行,这种方式具有跨平台性和灵活性,但执行效率较低。
热点探测
在解释执行的过程中,JVM 会监视程序的执行情况,以发现热点代码。热点代码是指那些被频繁执行的代码段,例如循环、方法调用等。JVM 使用统计信息来确定哪些代码段是热点代码。
即时编译
一旦确定了热点代码,JIT 编译器就会对这些代码进行优化编译,将其转换成本地机器码。JIT 编译器会根据程序的实际运行情况和运行时环境,利用一系列优化技术生成高效的机器码。
JIT 编译器编译触发规则
方法计数器
当某个方法被执行一定次数后,就会触发即时编译。
回边计数器
当发现某个循环的迭代次数达到一定阈值后,也会触发即时编译。
内联缓存
当发现某个方法调用的接收者对象类型发生变化时,会触发即时编译。
JIT 编译器编译优化技术
方法内联
将频繁调用的方法直接内联到调用者的代码中,避免了方法调用的开销。
逃逸分析
分析对象的生命周期,确定对象是否可以在栈上分配,以减少堆内存的使用和垃圾回收的开销。
JIT 编译器并不是虚拟机必须的部分,Java 虚拟机规范并没有规定 Java 虚拟机内必须要有即时编译器存在。然而,即时编译器编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一。它也是虚拟机中最核心且最能体现虚拟机技术水平的部分。在部分商用虚拟机中(如 HotSpot),Java 程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”,并在运行时将其编译成与本地平台相关的机器码,并进行各种层次的优化。这种解释器与编译器并存的架构旨在实现高效执行和快速响应之间的平衡。
逃逸分析
JVM 逃逸分析(Escape Analysis)是 Java 虚拟机(JVM)中的一项前沿优化技术。逃逸分析是一种跨函数全局数据流分析算法,通过动态分析对象的作用域,来判断对象是否会被外部方法或线程所引用。基于这个分析,JVM 可以对代码进行一系列优化,以提高性能和减少内存压力。
对象逃逸状态
全局逃逸(GlobalEscape)
当对象的作用范围逃出了当前方法或当前线程。这通常发生在以下情况:对象是一个静态变量、对象是一个已经发生逃逸的对象,或者对象作为当前方法的返回值。
参数逃逸(ArgEscape)
当对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸。这个状态是通过被调方法的字节码确定的。
没有逃逸
即方法中的对象没有发生逃逸。逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸。
逃逸分析代码优化
同步消除
如果一个对象被逃逸分析发现只能被一个线程所访问,那对于这个对象的操作可以不同步。这是因为线程同步本身比较消耗性能,如果确定一个对象不会逃逸出线程,无法被其他线程访问到,该对象的读写就不存在竞争,因此可以消除该对象的同步锁,从而提高性能。
同步消除前的代码:
public void testSyncEscape(){
Object o = new Object();
synchronized (o){
System.out.println(o);
}
}
public void testSyncEscapeAft(){
Object o = new Object();
System.out.println(o);
}
栈上分配
在以往的 Java 程序运行中,对象的内存空间都是通过堆来进行分配的。然而,如果对象的生命周期仅限于一个方法内部,那么可以将这个对象的内存空间分配在栈上。随着栈的出栈操作,对象就会被销毁,这样可以减少垃圾回收的压力,提高性能。
-XX:+DoEscapeAnalysis 启用逃逸分析
-XX:-DoEscapeAnalysis 关闭逃逸分析
-XX:+PrintGCDetails 打印 gc 信息
-XX:+HeapDumpOnOutOfMemoryError
栈上分配测试代码:
class Score{
private int score=0;
public Score(int score){
this.score = score;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
}
public void testStackObj(){
for(int i=0; i<900000; i++){
mkScore(90);
}
System.out.println("对象生成结束 。。。。");
}
private Score mkScore(int score){
return new Score(score);
}
public static void main(String[] args){
TestEscapeAnalysis test = new TestEscapeAnalysis();
test.testStackObj();
System.out.println("等待 20S 后退出");
try{
Thread.sleep(20*1000L);
}catch(Exception e){
e.printStackTrace();
}
}
使用 jmap -histo pid(进程 id,通过 jps 或 ps aux |grep java 执行获取)获取堆分配结果信息。
通过以下的测试用例,我们发现启用了逃逸分析参数运行时,原本需在堆上分配90万次空间的Score对象,仅仅在堆上分配了17万次,剩余的在栈上分配了;而关闭逃逸分析参数运行时,Score对象在堆上分配了90万次。
使用-XX:+DoEscapeAnalysis 启用逃逸分析参数的结果如下:
num #instances #bytes class name
----------------------------------------------
1: 562 15134840 [I
2: 6134 5426336 [B
3: 170510 2728160 com.flycloud.test.jvm.Score
使用-XX:-DoEscapeAnalysis 关闭逃逸分析参数的结果如下:
num #instances #bytes class name
----------------------------------------------
1: 564 16045944 [I
2: 900000 14400000 com.flycloud.test.jvm.Score
标量替换
标量是指不可分割的量,如 Java 中的基本数据类型和引用类型。相对的一个数据可以继续分解,称为聚合量。标量替换是指把一个对象拆散,将其成员变量恢复到基本类型来访问。这可以减少内存占用和提高性能。JVM 逃逸分析是一种重要的优化技术,通过分析对象的动态作用域来优化代码,减少内存占用,提高性能和降低同步负载。在实际开发中,合理利用逃逸分析可以带来显著的性能提升。
标量替换前的代码:
public void testScalarReplace(){
Score score = new Score(99);
System.out.println(score.getScore());
}
public void testScalarReplaceAft(){
int score = 99;
System.out.println(score);
}
总结
JVM 即时编译,是对 Java 程序通过解释器解释执行的补充优化,以提升程序的执行效率。JVM 通过监控程序执行而发现热点代码,并通过 JIT 编译器对热点代码进行优化编译,生成执行效率高的机器码。通过逃逸分析,可以有三种方式进行代码优化:同步消除、栈上分配和标量替换。通过逃逸分析的优化技术,减少了内存的占用,同时减少了垃圾回收的压力,从而提升了性能。另外,通过 JVM 逃逸分析的代码优化,我们也清晰了,Java 对象,通常是分配在堆上的,但也可能是被判定为热点代码,而被分配在了栈上。
原文始发于微信公众号(扬哥手记):一文掌握JVM即时编译及逃逸分析
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/239542.html