JVM——逃逸分析

爱被打了一巴掌 2022-04-24 08:30 498阅读 0赞

首先,为解释这个问题,需要的基本知识如下(如果对以下概念不太熟悉, 可以先Google下):

1.JVM内存结构,传送门
2.即时编译(JIT),传送门

逃逸分析

在编译期间,JIT会对代码做很多优化。其中有一部分优化的目的就是减少内存堆分配压力,其中一种重要的技术叫做逃逸分析。

逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中 同步负载内存堆分配压力跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

例如:

  1. public static StringBuffer newStringBuffer(String s1, String s2) {
  2. StringBuffer sb = new StringBuffer();
  3. sb.append(s1);
  4. sb.append(s2);
  5. return sb;
  6. }

StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

上述代码如果想要StringBuffer sb不逃出方法,可以这样写:

  1. public static String newStringBuffer(String s1, String s2) {
  2. StringBuffer sb = new StringBuffer();
  3. sb.append(s1);
  4. sb.append(s2);
  5. return sb.toString();
  6. }

不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法。

使用逃逸分析,编译器可以对代码做如下优化:

一、同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

二、将堆分配转化为栈分配:如果一个对象在子程序中被分配,如果指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

三、分离对象或标量替换。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

本文主要来介绍逃逸分析的第二个用途:将堆分配转化为栈分配

其实,以上三种优化中,栈上内存分配其实是依靠标量替换来实现的。由于不是本文重点,这里就不展开介绍了。如果大家感兴趣,我后面专门出一篇文章,全面介绍下逃逸分析。

在Java代码运行时,通过JVM参数可指定是否开启逃逸分析,

-XX:+DoEscapeAnalysis : 表示开启逃逸分析

-XX:-DoEscapeAnalysis : 表示关闭逃逸分析

从jdk 1.7开始已经默认开始逃逸分析,如需关闭,需要指定-XX:-DoEscapeAnalysis

对象的栈上内存分配

我们知道,在一般情况下,对象和数组元素的内存分配是在堆内存上进行的。但是随着JIT编译器的日渐成熟,很多优化使这种分配策略并不绝对。JIT编译器就可以在编译期间根据逃逸分析的结果,来决定是否可以将对象的内存分配从堆转化为栈。

我们来看以下代码:

  1. public static void main(String[] args) {
  2. long a1 = System.currentTimeMillis();
  3. for (int i = 0; i < 1000000; i++) {
  4. alloc();
  5. }
  6. // 查看执行时间
  7. long a2 = System.currentTimeMillis();
  8. System.out.println("cost " + (a2 - a1) + " ms");
  9. // 为了方便查看堆内存中对象个数,线程sleep
  10. try {
  11. Thread.sleep(100000);
  12. } catch (InterruptedException e1) {
  13. e1.printStackTrace();
  14. }
  15. }
  16. private static void alloc() {
  17. User user = new User();
  18. }
  19. static class User {
  20. }

其实代码内容很简单,就是使用for循环,在代码中创建100万个User对象。

我们在alloc方法中定义了User对象,但是并没有在方法外部引用他。也就是说,这个对象并不会逃逸到alloc外部。经过JIT的逃逸分析之后,就可以对其内存分配进行优化。

我们指定以下JVM参数并运行:

  1. -Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

在程序打印出 cost XX ms 后,代码运行结束之前,我们使用[jmap][1]命令,来查看下当前堆内存中有多少个User对象:

  1. ~ jps
  2. 2809 StackAllocTest
  3. 2810 Jps
  4. ~ jmap -histo 2809
  5. num #instances #bytes class name
  6. ----------------------------------------------
  7. 1: 524 87282184 [I
  8. 2: 1000000 16000000 StackAllocTest$User
  9. 3: 6806 2093136 [B
  10. 4: 8006 1320872 [C
  11. 5: 4188 100512 java.lang.String
  12. 6: 581 66304 java.lang.Class

从上面的jmap执行结果中我们可以看到,堆中共创建了100万个StackAllocTest$User实例。

在关闭逃避分析的情况下(-XX:-DoEscapeAnalysis),虽然在alloc方法中创建的User对象并没有逃逸到方法外部,但是还是被分配在堆内存中。也就说,如果没有JIT编译器优化,没有逃逸分析技术,正常情况下就应该是这样的。即所有对象都分配到堆内存中。

接下来,我们开启逃逸分析,再来执行下以上代码。

  1. -Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError

在程序打印出 cost XX ms 后,代码运行结束之前,我们使用jmap命令,来查看下当前堆内存中有多少个User对象:

  1. ~ jps
  2. 709
  3. 2858 Launcher
  4. 2859 StackAllocTest
  5. 2860 Jps
  6. ~ jmap -histo 2859
  7. num #instances #bytes class name
  8. ----------------------------------------------
  9. 1: 524 101944280 [I
  10. 2: 6806 2093136 [B
  11. 3: 83619 1337904 StackAllocTest$User
  12. 4: 8006 1320872 [C
  13. 5: 4188 100512 java.lang.String
  14. 6: 581 66304 java.lang.Class

从以上打印结果中可以发现,开启了逃逸分析之后(-XX:+DoEscapeAnalysis),在堆内存中只有8万多个StackAllocTest$User对象。也就是说在经过JIT优化之后,堆内存中分配的对象数量,从100万降到了8万。

除了以上通过jmap验证对象个数的方法以外,读者还可以尝试将堆内存调小,然后执行以上代码,根据GC的次数来分析,也能发现,开启了逃逸分析之后,在运行期间,GC次数会明显减少。正是因为很多堆上分配被优化成了栈上分配,所以GC次数有了明显的减少。

总结

所以,如果以后再有人问你:是不是所有的对象和数组都会在堆内存分配空间?

那么你可以告诉他:不一定,随着JIT编译器的发展,在编译期间,如果JIT经过逃逸分析,发现有些对象没有逃逸出方法,那么有可能堆内存分配会被优化成栈内存分配。但是这也并不是绝对的。就像我们前面看到的一样,在开启逃逸分析之后,也并不是所有User对象都没有在堆上分配。

转自Hollis

发表评论

表情:
评论列表 (有 0 条评论,498人围观)

还没有评论,来说两句吧...

相关阅读

    相关 JVM逃逸分析

      我们都知道Java中的对象默认都是分配到堆上,在调用栈中,只保存了对象的指针。当对象不再使用后,需要依靠GC来遍历引用树并回收内存。如果堆中对象数量太多,回收对象还有整理内

    相关 Java之JVM逃逸分析

    引言: 逃逸分析(Escape Analysis)是众多JVM技术中的一个使用不多的技术点,本文将通过一个实例来分析其使用场景。 概念 逃逸分析,是一种可以有效减少Ja

    相关 JVM逃逸分析

    摘要: 本文基于周志明著作的《深入了解Java虚拟机》主要介绍了逃逸分析的定义,以及逃逸分析的一些应用,方便复习 `逃逸分析`(Escape Analysis)是目前Jav