一 点睛
使用逃逸分析,编译器可以对代码做如下优化。
栈上分配:将堆分配转化为栈分配。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会发生逃逸,对象可能是栈上分配的候选,而不是堆上分配。
同步省略:如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在 CPU 寄存器中。
二 栈上分配
JIT 编译器在编译期间根据逃逸分析的结果,发现如果一个对象并没有逃逸出方法的话,就可能被优化成栈上分配。分配完成后,继续在调用栈内执行,最后线程结束,栈空间被回收,局部变量对象也被回收。这样就无须进行垃圾回收了。
下面场景会发生逃逸,不会在栈上分配。
分别给成员变量赋值、方法返回值、实例引用传递参数。
三 实战
下面通过实战说明开启逃逸分析和未开启逃逸分析的各种参数设置情况。
1 代码
/**
* 栈上分配测试
* -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
*/
public class StackAllocation {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
// 查看执行时间
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
// 为了方便查看堆内存中对象个数,线程 sleep
try {
Thread.sleep(1000000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
User user = new User(); // 未发生逃逸,可以考虑将该对象分配到栈空间
}
}
class User {
}
2 设置 JVM 参数,未开启逃逸分析
-Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
控制台输出结果如下。
花费的时间为: 112 ms
然后查看内存的情况,发现有大量的 User 存储在堆中。
3 开启逃逸分析
-Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
然后查看运行时间,我们能够发现花费的时间快速减少
花费的时间为: 7 ms
再看内存情况,发现只有很少的 User 对象,说明 User 未发生逃逸,因为它存储在栈中,随着栈的销毁而消失。
4 关闭逃逸分析,并设置为 256m,会发生 GC
-Xmx256m -Xms256m -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
控制台输出如下
[GC (Allocation Failure) [PSYoungGen: 65536K->1083K(76288K)] 65536K->1091K(251392K), 0.0016055 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 66619K->968K(76288K)] 66627K->976K(251392K), 0.0008380 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
花费的时间为: 69 ms
5 开启逃逸分析,并设置为 256m,没发生GC,而且时间短
-Xmx256m -Xms256m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails
控制台输出如下
花费的时间为: 5 ms