JVM 逃逸分析 笔记

public static StringBuffer craeteStringBuffer(String s1, String s2) {
   StringBuffer sb = new StringBuffer();
   sb.append(s1);
   sb.append(s2);
   return sb;
}

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

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

 

public static String createStringBuffer(String s1, String s2) {
   StringBuffer sb = new StringBuffer();
   sb.append(s1);
   sb.append(s2);
   return sb.toString();
}

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

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

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

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

上面的关于同步省略的内容,我在《深入理解多线程(五)—— Java虚拟机的锁优化技术》中有介绍过,即锁优化中的锁消除技术,依赖的也是逃逸分析技术。

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

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

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

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

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

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

对象的栈上内存分配

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

我们来看以下代码:

public static void main(String[] args) {
   long a1 = System.currentTimeMillis();
   for (int i = 0; i < 1000000; i++) {
       alloc();
   }
   // 查看执行时间
   long a2 = System.currentTimeMillis();
   System.out.println("cost " + (a2 - a1) + " ms");
   // 为了方便查看堆内存中对象个数,线程sleep
   try {
       Thread.sleep(100000);
   } catch (InterruptedException e1) {
       e1.printStackTrace();
   }
}
 
private static void alloc() {
   User user = new User();
}
 
static class User {
 
}

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

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

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

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

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

开启逃逸分析后:

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

 

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

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

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

总结

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

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

关于逃逸分析的论文在1999年就已经发表了,但直到JDK 1.6才有实现,而且这项技术到如今也并不是十分成熟的。

其根本原因就是无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配、和锁消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。

一个极端的例子,就是经过逃逸分析之后,发现没有一个对象是不逃逸的。那这个逃逸分析的过程就白白浪费掉了。

虽然这项技术并不十分成熟,但是他也是即时编译器优化技术中一个十分重要的手段。
 

变量在内部创建在内部消失  就是线程安全的

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章