1 JVM逃逸分析
- 背景:
- 在 Java 虚拟机中,对象是在 Java 堆中分配内存的,这是一个普遍的常识。但是,有一种特殊情况,那就是如果经过逃逸分析(Escape Analysis)后发现,一个对象并没有逃逸出方法的话,那么就可能被优化成栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。
- 逃逸分析可以做标量替换、栈上分配、锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。
2 何为逃逸分析
- 逃逸分析一种数据分析算法,基于此算法可以有效减少 Java 对象在堆内存中的分配。Hotspot 虚拟机的编译器能够分析出一个新对象的引用范围,然后决定是否要将这个对象分配到堆上。
✓ 当一个对象在方法中被定义后,对象只在方法内部使用,则认为没有发生逃逸。
✓ 当一个对象在方法中被定义后,它被外部方法所引用,则认为发生逃逸。
建议:开发中能在方法内部应用对象的,就尽量控制在内部
在方法外创建,方法内引用也是不会发生逃逸的,会分配在堆里面局部变量分配在栈中。
3 代码优化
使用逃逸分析,编译器可以对代码做如下优化:
▪ 栈上分配:将堆分配转化为栈分配。如果一个对象在方法内创建,要使指向该对象的引用不会发生逃逸,对象可能是栈上分配的候选。
▪ 同步省略:又称之为同步锁消除,如果一个对象被发现只有一个线程被访问到,那么对于这个对象的操作可以不考虑同步。
我们知道线程同步是靠牺牲性能来保证数据的正确性,这个过程的代价会非常高。程序的并发行和性能都会降低。JVM 的 JIT 编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程应用?假如是,那么 JIT 编译器在编译这个同步块的时候就会取消对这部分代码上加的锁。这个取消同步的过程就叫同步省略,也叫锁消除。
▪ 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在 CPU 寄存器中。
所谓的标量(scalar)一般指的是一个无法再分解成更小数据的数据。例如,Java 中的原始数据类型就是标量。相对的,那些还可以分解的数据叫做聚合量(Aggregate),Java中的对象就是聚合量,因为他可以分解成其他聚合量和标量。在 JIT 阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过 JIT 优化,就会把这个对象分解成若干个变量来代替。这个过程就是标量替换。
package com.java.jvm;
/**
* 标量替换测试 (-XX:+EliminateAllocations)
* -Xmx128m -Xms128m -XX:+DoEscapeAnalysis
-XX:+PrintGC -XX:-EliminateAllocations
*/
public class ObjectScalarReplaceTests {
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");
}
private static void alloc() {
Point point = new Point(1,2);
}
static class Point {
private int x;
private int y;
public Point(int x,int y){
this.x=x;
this.y=y;
}}}
//对于上面代码,假如开启了标量替换,那么 alloc 方法的内容就会变为如下形式:
private static void alloc() {
int x=10;
int y=20;
}
alloc 方法内部的 Point 对象是一个聚合量,这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个标量了。
标量替换有什么好处:就是可以大大减少堆内存的占用。因为一旦不需要创建对象了,那么就不再需要分配堆内存了。标量替换为栈上分配提供了很好的基础。