天天看点

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

翻译:吴嘉俊,叩丁狼高级讲师。

背景

在上一篇文章中,我们介绍了逃逸分析,并且介绍了通过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/

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