第十五篇 JVM之运行时数据区<11>: 栈顶优化
一、逃逸分析
逃逸分析是目前JVM前沿的优化分析技术,基本原理是分析对象的动态作用域,如果对象只能在方法内部被访问到,说明对象没有发生逃逸,反之,说明对象发生逃逸。如果一个方法内部定义的对象被外部方法引用,如作为方法调用参数或者方法返回,就说明发生了方法逃逸。如果被其他线程访问到,说明发生了线程逃逸。如下代码,obj1,obj2,obj3都发生了逃逸,而obj4没有发生逃逸。
public class EscapeAnalysisDemo { Object obj1; public void visit() { obj1 = new Object(); // 逃逸 Object obj2 = new Object(); // 逃逸 Object obj3 = new Object(); // 逃逸 Object obj4 = new Object(); String.valueOf(obj2); new Thread(() -> { System.out.println(obj3); }).start(); } }
HotSpot虚拟机默认开启逃逸分析,可以通过-XX:+DoEscapeAnalysis参数设置,
二、代码优化
根据逃逸分析,JVM会对象实例采取优化:
1、栈上分配
将堆中对象分配转为栈上分配,对象实例会随着方法执行结束而销毁,不用进行GC,节省资源开销,栈上分配支持方法逃逸,无法支持线程逃逸。
2、标量替换
若一个数据不能被分解成更小的数据表示了,如int、short、reference等,就被称为标量,相对的,如果一个数据能被分解就称为聚合量,如Java对象。一个对象通过逃逸分析如果不发生逃逸,在Java程序执行的时候,就不会创建对象,而是创建若干成员变量表示,拆分之后,成员变量可实现栈上分配和读写之外,还能为后一步优化创建条件。标量替换逃逸程序不能超出方法范围,标量替换可通过参数-XX:+EliminateAllocations设置
3、同步消除
如果逃逸分析能确定一个变量不发生逃逸,此时,变量是线程安全的,相关的线程同步措施将会被消除掉。
三、对象分配内存策略
根据目前分配对象的策略,可以发现对象分配有栈上分配、TLAB分配、Eden区分配和老年代分配,如图,在开启逃逸分析和TLAB之后,对象会优先栈上分配和TLAB分配,如果两者都不能成功分配内存,就会到Java堆中分配内存,通俗的讲,TLAB也是Eden的区域,在Java堆中,对象内存优先在Eden区分配。
如果满足老年代分配条件,则对象直接在老年代分配,如大对象,大对象(指需要大量连续内存的Java对象,如超长字符串,超长数组)直接进入老年代,因为当对象很大时,为了有足够的空间分配给大对象,在新生代就很容易触发GC,GC时,大对象的复制和Eden区与Survivor区、两个Survivor区之间的复制会带来高额内存开销,对象直接进入老年代可以避免这种问题,-XX: PretenureSizeThreshold参数可以设置触发阈值,当对象大于该值,会进入老年代,该值没有单位。
四、代码验证
1、逃逸分析
如下代码;设置JVM参数:-Xms64m -Xmx64m -XX:+PrintGC -XX:-DoEscapeAnalysis -XX:-EliminateAllocations,首先关闭逃逸分析和标量替换,此时,Pointer对象都会在Java堆中分配,由于堆只有64m,Eden区只有21m左右,所以会频繁触发GC并打印GC日志,所以耗时会更长,当开启逃逸分析以后,将会栈上分配,代码会如注释中优化,并且不会触发GC,耗时更短。
public class EscapeAnalysisDemo { public static void main(String[] args) { long start = System.currentTimeMillis(); for (int i = 0; i < 10000000; i++) { allocation(); } System.out.println(String.format("耗时:%d ms", System.currentTimeMillis() - start)); } public static void allocation() { // pointer未发生逃逸,同步操作会被消除 synchronized (EscapeAnalysisDemo.class) { Pointer pointer = new Pointer(); } } public static class Pointer { // 会标量替换,对象拆分成员变量 int x = 1; int y = 2; } }
执行结果:
[GC (Allocation Failure) 16384K->768K(62976K), 0.0009711 secs] [GC (Allocation Failure) 17152K->816K(62976K), 0.0006978 secs] [GC (Allocation Failure) 17200K->784K(62976K), 0.0005584 secs] [GC (Allocation Failure) 17168K->752K(62976K), 0.0005481 secs] [GC (Allocation Failure) 17136K->784K(62976K), 0.0008749 secs] [GC (Allocation Failure) 17168K->736K(64512K), 0.0008245 secs] [GC (Allocation Failure) 20192K->672K(64512K), 0.0006088 secs] [GC (Allocation Failure) 20128K->672K(63488K), 0.0004090 secs] [GC (Allocation Failure) 19104K->672K(64000K), 0.0004984 secs] [GC (Allocation Failure) 19104K->672K(64000K), 0.0005061 secs] [GC (Allocation Failure) 19104K->672K(64000K), 0.0005664 secs] [GC (Allocation Failure) 19104K->672K(64000K), 0.0003976 secs] [GC (Allocation Failure) 19104K->672K(64000K), 0.0009005 secs] 耗时:109 ms Process finished with exit code 0
耗时:78 ms
Process finished with exit code 0
2、内存分配策略
如下代码;设置JVM参数:-Xms64m -Xmx64m -XX:+PrintGC -XX:PretenureSizeThreshold=2097152,PretenureSizeThreshold没有单位默认字节,2097152是2M,所以大于等于2M的对象会进入老年代。所以执行之后,老年代会有占用5M内存。
public class HeapAllocationDemo { public static void main(String[] args) throws InterruptedException { byte[] b1 = new byte[1024 * 1024 * 1]; // Eden区 byte[] b2 = new byte[1024 * 1024 * 2]; //2M对象 OldG区 byte[] b3 = new byte[1024 * 1024 * 3]; //3M对象 OldG区 Thread.sleep(1000000); } }