专业的编程技术博客社区

网站首页 > 博客文章 正文

JVM对于逃逸对象的再捕获(Escape Object Recapture)

baijin 2024-09-18 12:10:13 博客文章 3 ℃ 0 评论

翻译:叩丁狼教育吴嘉俊

背景

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

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表