首页 > 技术文章 > 技术文章精译 >

Java8的JVM对于逃逸对象的再捕获

更新时间:2018-10-16 | 阅读量(523)

### 背景 在上一篇文章中,我们介绍了逃逸分析,并且介绍了通过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 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/
叩丁狼学员采访 叩丁狼学员采访
叩丁狼头条 叩丁狼头条
叩丁狼在线课程 叩丁狼在线课程