JVM系列之逃逸分析
一、前言
思考:对象是否都是创建在堆中?
答案是否定的,Java对象的创建一般情况是在堆内存中,当堆内存空间不足时,就会触发GC回收,GC次数多了就会影响到程序的性能;
因此逃逸分析
就是为了缓解这一问题而诞生的。
二、什么是逃逸分析
因为堆内存空间总是有限的,随着对象的积累,gc的次数会越来越频繁,这显然会影响到程序的整体性能;
逃逸分析的目的是分析新创建对象的使用范围,并决定对象是否可以分配在Java堆上;
内存逃逸现象:
当一个对象不仅在方法中被调用,还在其他地方被引用了,当这个方法结束时,gc无法就立即回收这个对象。
三、逃逸状态
- 全局逃逸(GlobalEscape):一个对象的作用范围逃出了当前方法或者当前线程
全局逃逸的场景:
- 一个对象的引用是复制给了一个类变量
- 存储在一个已经逃逸的对象当中
- 这个对象的引用作为方法的返回值返回给了调用方法
参数逃逸(ArgEscape):一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这种状态可以通过分析被调方法的二进制代码确定
没有逃逸(NoEscape):顾名思义,就是没有逃逸
- 开启逃逸分析(JDK1.8 默认开启):
-XX:+DoEscapeAnalysis
- 关闭逃逸分析:
-XX:-DoEscapeAnalysis
- 显示分析结果:
-XX:+PrintEscapeAnalysis
四、逃逸分析的作用
示例代码:
/**
* 测试逃逸分析
*
* @author 小白
* @date 2021/4/3 14:31
*/
public class Test {
public static void main(String[] args) throws InterruptedException {
long t1 = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
getObject();
}
long t2 = System.currentTimeMillis();
System.out.println(t2 - t1);
TimeUnit.MINUTES.sleep(10);
}
private static void getObject() {
Object obj = new Object();
}
}
开启逃逸分析:
关闭逃逸分析:
分析:
开启逃逸分析时,jvm中大概产生11万左右的对象,代码耗时3ms,关闭逃逸分析时,大概产生60w左右的对象,代码耗时428ms,比较来看,看起逃逸分析是有助于虚拟机性能提升的。
1.标量替换
基础类型和对象的引用可以理解为标量(不能被进一步分解);
对象就是聚合量,能被进一步分解。
用其成员变量(分散的标量)代替该对象整体,从而不需要连续的存储空间,就可以将对象的部分甚至全部都保存在CPU寄存器内,这就叫做标量替换。
如果一个对象没有发生逃逸,那就不用创建这个对象,虚拟机只会在栈或者寄存器上创建它会使用到的成员标量,节省了内存空间,从而提升了应用程序性能。
java -XX:+DoEscapeAnalysis -XX:+EliminateAllocations Test
代码执行时间:3 ms
java -XX:+DoEscapeAnalysis -XX:-EliminateAllocations Test
代码执行时间:415 ms
java -XX:-DoEscapeAnalysis -XX:+EliminateAllocations Test
代码执行时间:392 ms
java -XX:-DoEscapeAnalysis -XX:-EliminateAllocations Test
代码执行时间:387 ms
分析
开启和关闭标量替换对于程序性能的影响很大,同时也验证了标量替换功能生效的前提是逃逸分析已经开启,否则没有意义。
- 开启标量替换:
-XX:+EliminateAllocations
- 关闭标量替换:
-XX:-EliminateAllocations
- 显示标量替换详情:
-XX:+PrintEliminateAllocations
2. 锁消除
示例代码:
private static void getObject() {
Object obj = new Object();
synchronized (obj) {
obj.toString();
}
}
java -XX:+DoEscapeAnalysis -XX:+EliminateLocks Test
代码执行时间:4 ms
java -XX:+DoEscapeAnalysis -XX:-EliminateLocks Test
代码执行时间:388 ms
分析
开启和关闭锁消除对于程序性能的影响也是很大。
- 开启锁消除(JDK1.8 默认开启):
-XX:+EliminateLocks
- 关闭锁消除:
-XX:-EliminateLocks
3.栈内存分配
将原本分配在堆内存上的对象转而分配在栈内存上,减少堆内存的占用,从而减少 GC 的次数
Oracle HotSpot 虚拟机并没有这么做
五、非逃逸分析优化
锁粗化
当连续获取同一个对象的锁时,HotSpot虚拟机会去检查多个锁区域是否能合并成一个更大的锁区域。这种聚合被称作锁粗化,它能够减少加锁和解锁的消耗。嵌套锁
同步块可能会一个嵌套一个,进而两个块使用同一个对象的监视器锁来进行同步也是很有可能的。这种情况我们称之为嵌套锁,HotSpot虚拟机是可以识别出来并删除掉内部块中的锁的。当一个线程进入外部块时就已经获取到锁了,因此当它尝试进入内部块时,肯定也仍持有这个锁,所以这个时候删除锁是可行的。
总结
开发中尽量使用局部变量的,就不要使用在方法外定义。
作者:简楼
原文链接:https://www.jianshu.com/p/6866a3699be3