翻译:吴嘉俊,叩丁狼高级讲师。
背景
在上一篇文章中,我们介绍了逃逸分析,并且介绍了通过EA,JVM可以直接在栈上为未逃逸对象分配空间,而不需要在堆上分配空间。在文章发布之后,Caleb Cushing问了一个很有趣的问题:
如果一个逃逸对象被限定在调用者的范围之内,那么这个逃逸对象是否可以被EA优化?
我在这篇文章中给出了问题的答案。
一个例子
我们先创建一个如下的简单类:Person
public class Person {
private final String firstName;
private final String middleName;
private final String lastName;
public Person(String firstName, String middleName, String lastName) {
this.firstName = requireNonNull(firstName); // Cannot be null
this.middleName = middleName; // Can be null
this.lastName = requireNonNull(lastName); // Cannot be null
}
public String getFirstName() {
return firstName;
}
public Optional<String> getMiddleName() {
return Optional.ofNullable(middleName);
}
public String getLastName() {
return lastName;
}
}
假如我们调用Person::getMiddleName方法,很明显,Optional对象就是一个逃逸对象,因为它可以被任何调用这个方法的对象访问,所以返回的这个对象会被标记为全局逃逸对象,既然是逃逸对象,那么按理,应该会在堆里面为这个Optional对象分配空间。
但是,这真说不一定。JVM在某些情况下,确实可能把Optional对象直接在栈上分配,即使这个Optional对象会逃逸出getMiddleName方法。这可能么?
如何让一个全局逃逸对象仍然在栈上分配空间
事实在于,C2编译器在执行逃逸分析的时候,并不仅仅只是去分析某一个方法的调用,而是会在更大的内联代码块( chunks of code that is inlined)上去分析一个对象是否可以执行EA。内联是一种优化方案,代码会首先消除冗余调用,把代码执行“扁平化”操作,即多个层次的代码调用会被“扁平化”为一个指令序列。然后编译器在这个已经扁平化的代码块上再执行EA操作。所以,即使一个对象从一个方法中逃逸,如果在更大的内联代码块中没有逃逸,那也可以被优化。
下面给一个具体的例子来证明内联的逃逸对象是怎么被EA的
public class Main2 {
public static void main(String[] args) throws IOException {
Person p = new Person("Johan", "Sebastian", "Bach");
count(p);
System.gc();
System.out.println("Press any key to continue");
System.in.read();
long sum = count(p);
System.out.println(sum);
System.out.println("Press any key to continue2");
System.in.read();
sum = count(p);
System.out.println(sum);
System.out.println("Press any key to exit");
System.in.read();
}
private static long count(Person p) {
long count = 0;
for (int i = 0; i < 1_000_000; i++) {
if (p.getMiddleName().isPresent()) {
count++;
}
}
return count;
}
}
上面的代码创建了一个Person对象,并且大量的调用了Person对象的getMiddleName()方法。我们分成三步来测试。第一步在调用完count方法之后,立刻进行GC操作回收我们创建的对象。另外两步不会从堆里面回收任何数据,我们来看看每个步骤在堆上数据的区别。我们按照下面的参数运行代码:
-server
-XX:BCEATraceLevel=3
-XX:+PrintCompilation
-XX:+UnlockDiagnosticVMOptions
-XX:+PrintInlining
-verbose:gc
-XX:MaxInlineSize=256
-XX:FreqInlineSize=1024
-XX:MaxBCEAEstimateSize=1024
-XX:MaxInlineLevel=22
-XX:CompileThreshold=10
-Xmx4g
-Xms4g
在执行完第一步(GC之后),内存情况如下:
pemi$ jps | grep Main2
74886 Main2
num #instances #bytes class name
----------------------------------------------
1: 95 42952184 [I
2: 1062 101408 [C
3: 486 55384 java.lang.Class
4: 526 25944 [Ljava.lang.Object;
5: 13 25664 [B
6: 1040 24960 java.lang.String
7: 74 5328 java.lang.reflect.Field
后面两步内存情况分别如下:
pemi$ jmap -histo 74886 | head
num #instances #bytes class name
----------------------------------------------
1: 95 39019792 [I
2: 245760 3932160 java.util.Optional
3: 1063 101440 [C
4: 486 55384 java.lang.Class
5: 526 25944 [Ljava.lang.Object;
6: 13 25664 [B
7: 1041 24984 java.lang.String
pemi$ jmap -histo 74886 | head
num #instances #bytes class name
----------------------------------------------
1: 95 39019544 [I
2: 245760 3932160 java.util.Optional
3: 1064 101472 [C
4: 486 55384 java.lang.Class
5: 526 25944 [Ljava.lang.Object;
6: 13 25664 [B
7: 1042 25008 java.lang.String
可以明显的看到,在第二步和第三步之间,没有新的Optionals对象被创建,EA确实没有在堆上创建Optinal对象,即使它们从创建和返回它们的初始方法中逃逸。因为这个特性,所以,我们仍然能在代码级别做适当的抽象,而不对性能产生影响。
原文:https://www.voxxed.com/2016/01/java-8-jvm-can-re-capture-objects-escaped/