深入理解JVM 之 逃逸分析

深入理解JVM 之 逃逸分析

逃逸分析的理论学习及实验

准备

逃逸分析的知识:参考 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明11.4.3 逃逸分析部分
逃逸分析的算法:Escape Analysis for Java
此篇论文略深奥,暂时作为辅助作用,此篇不详细讲论文的内容

逃逸分析的基本概念

什么是逃逸分析
当一个对象指针被多个方法或线程引用时称这个指针发生逃逸。而用来分析这个逃逸的方法,则称为逃逸分析。因此逃逸分析是目前Java虚拟机中比较前沿的优化技术,不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。

什么时候进行逃逸分析
在编译阶段确立逃逸,注意并不是在运行时

逃逸分析的状态

  1. GlobalEscape(全局逃逸或线程逃逸):
    即一个对象的作用范围逃出了当前方法或者当前线程,有以下几种场景:
  • 对象是一个静态变量
  • 对象是一个已经发生逃逸的对象
  • 对象作为当前方法的返回值
  1. ArgEscape(参数逃逸或方法逃逸):
    即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态是通过被调方法的字节码确定的。

  2. NoEscape(没有逃逸):
    即方法中的对象没有发生逃逸。

几种不同程度的逃逸状态

public class EscapeStatus {

    public static Object globalVariableObject;

    public Object instanceObject;

    public void globalVariableEscape(){
        globalVariableObject = new Object();  // 静态变量,外部线程可见,发生逃逸 GlobalEscape
    }

    public void instanceObjectEscape(){
        instanceObject = new Object();  // 赋值给堆中实例字段,外部线程可见,发生逃逸 GlobalEscape
    }

    public Object returnObjectEscape(){
        return new Object();   // 返回实例,外部线程可见,发生逃逸 GlobalEscape
    }

    public void argEscape(){
        Object argEscape = new Object();
        callArgEscape(argEscape);   // 一个对象被作为方法参数传递,发生参数逃逸 ArgEscape
    }

    public void callArgEscape(Object obj){

    }

    public void noEscape(){
        Object noEscape = new Object();   // 仅创建线程可见,对象无逃逸 NoEscape
    }
}

全局逃逸GlobalEscape

public class GlobalEscape {
    static class User {
        private int id;
        private String name;
    }

    private static User user;

    public static void foo() {
        user = new User();
        user.id = 1;
        user.name = "Alice";
    }

    public static void main(String[] args) {
        foo();
    }
}

未逃逸NoEscape

public class NoEscape {
    class User {
        private int id;
        private String name;
    }

    public void foo() {
        User user = new User();
        user.id = 1;
        user.name = "Alice";
    }

    public static void main(String[] args) {
        NoEscape pos = new NoEscape();
        pos.foo();
    }
}

根据逃逸分析的不同状态,可以怎样优化

  • 栈上分配(Stack Allocations)
  • 标量替代(Scalar Replacement)
  • 锁消除(Synchronization Elimination)

相关参数:

-XX:+PrintGC :打印GC日志
-XX:+DoEscapeAnalysis :启用逃逸分析(默认打开)
-XX:+EliminateAllocations :标量替换(默认打开)
-XX:+EliminateLocks :锁消除(默认打开)
-XX:+PrintEliminateAllocations查看标量替换情况

JVM五大分区:

  1. 程序计数器
    每个线程拥有一个,记录指令位置。大小是固定的,不会出现oom。线程数越多,使用内存越大?
  2. 虚拟机栈
    每个方法都会生成一个栈帧。存储局部变量表,方法返回地址
  3. 本地方法栈
    与虚拟机栈一致,存储native方法

  4. 几乎所有的对象实例在这里生成,会发生GC
  5. 方法区
    存储类信息、静态变量、常量 会发生oom

栈上分配

什么是栈上分配:
几乎所有的对象实例,都是在堆上分配的,但存在部分例外,栈上分配就是这种除了堆上分配的例外。

栈上分配有什么好处:
不需要GC介入去回收这个对象,出栈即释放资源,可以提高性能,原理:由于我们GC每次回收对象的时候,都会触发Stop The World(停止世界),这时候所有线程都停止了,然后我们的GC去进行垃圾回收,如果对象频繁创建在我们的堆中,也就意味这我们也要频繁的暂停所有线程,这对于用户无非是非常影响体验的,栈上分配就是为了减少垃圾回收的次数

哪些对象可以栈上分配:

  • 小对象(一般几十个byte),在没有逃逸的情况下,可以直接分配在栈上
  • 大对象或者逃逸对象无法在栈上分配 (大对象多大?需要验证)

栈上分配需要有一定的前提:

  • 开启逃逸分析 (-XX:+DoEscapeAnalysis)
    逃逸分析的作用就是分析对象的作用域是否会逃逸出方法之外,在server虚拟机模式下才可以开启(jdk1.6默认开启)
  • 开启标量替换 (-XX:+EliminateAllocations)
    标量替换的作用是允许将对象根据属性打散后分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。默认该配置为开启

什么逃逸状态下可进行栈上分配优化:
NoEscape、ArgEscape

GlobalEscape 状态下能否进行栈上分配优化:

// -Xmx10m -Xms10m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
// 逃逸分析出是GlobalEscape的逃逸状态,无法进行栈上分配优化。是否开启逃逸分析不会影响结果。
public class EscapeTest2 {
    static class User {
        private int id;
        private String name;
    }

    private static User user;

    public static void foo() {
        user = new User();
        user.id = 1;
        user.name = "Alice";
    }
    public static void main(String[] args) {
        long beginTime = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            foo();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("time: " + (endTime - beginTime) + "ms");
    }

}

参数为-Xmx10m -Xms10m -XX:+PrintGC -XX:-DoEscapeAnalysis -XX:+EliminateAllocations 关闭逃逸分析
结果为

[GC (Allocation Failure)  4709K->2661K(9728K), 0.0003478 secs]
[GC (Allocation Failure)  4709K->2661K(9728K), 0.0003017 secs]
此处省略n个gc信息
[GC (Allocation Failure)  4709K->2661K(9728K), 0.0003630 secs]
time: 1098ms

参数为-Xmx10m -Xms10m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:+EliminateAllocations 开启逃逸分析
结果为

[GC (Allocation Failure)  5065K->3017K(9728K), 0.0003543 secs]
[GC (Allocation Failure)  5065K->3017K(9728K), 0.0003543 secs]
此处省略n个gc信息
[GC (Allocation Failure)  5065K->3017K(9728K), 0.0003322 secs]
time: 1106ms

以上结果可以说明:逃逸分析出是GlobalEscape的逃逸状态,无法进行栈上分配优化。是否开启逃逸分析不会影响结果。

NoEscape 状态下是否开启逃逸分析对结果的影响:

// -Xmx10m -Xms10m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
// 逃逸分析出是NoEscape的逃逸状态,可进行栈上分配优化。开启逃逸分析和标量替换会减少gc,提升性能。
public class EscapeTest1 {
    class User {
        public int id;
        public String name;
    }

    public void foo() {
        User user = new User();
        user.id = 1;
        user.name = "Alice";
    }

    public static void main(String[] args) {
        long beginTime = System.currentTimeMillis();
        EscapeTest1 escapeTest = new EscapeTest1();
        for (int i = 0; i < 100000000; i++) {
            escapeTest.foo();
        }
        long endTime = System.currentTimeMillis();
        System.out.println("time: " + (endTime - beginTime) + "ms");
    }

}

参数为-Xmx10m -Xms10m -XX:+PrintGC -XX:-DoEscapeAnalysis -XX:+EliminateAllocations 关闭逃逸分析
结果为

[GC (Allocation Failure)  4893K->2845K(9728K), 0.0003884 secs]
[GC (Allocation Failure)  4893K->2845K(9728K), 0.0003271 secs]
此处省略n个gc信息
[GC (Allocation Failure)  4893K->2845K(9728K), 0.0004020 secs]
[GC (Allocation Failure)  4893K->2845K(9728K), 0.0004676 secs]
time: 1049ms

参数为-Xmx10m -Xms10m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:+EliminateAllocations 开启逃逸分析
结果为

[GC (Allocation Failure)  2048K->716K(9728K), 0.0018283 secs]
[GC (Allocation Failure)  2764K->748K(9728K), 0.0005826 secs]
[GC (Allocation Failure)  2796K->748K(9728K), 0.0006814 secs]
time: 32ms

以上结果可以说明:逃逸分析出是NoEscape的逃逸状态,可进行栈上分配优化。开启逃逸分析和标量替换会减少gc,提升性能。

如何确定大对象界限,使其无法在栈上分配:

// -Xmx5m -Xms5m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
// 数组在长度64以上时会被认为是大对象,无法进行栈上分配优化。
public class EscapeTest5 {

    public static void main(String[] args) {
        int[] intArray;
        long[] longArray;
        Integer[] integerArray;
        Test[] objectArray;
        
        long beginTime = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            intArray = new int[64]; //不开启逃逸分析速度7749ms,开启速度18ms
            //intArray = new int[65]; //不开启逃逸分析速度8144ms,开启速度8804ms
            //longArray = new long[64]; //不开启逃逸分析速度14763ms,开启速度27ms
            //longArray = new long[65]; //不开启逃逸分析速度15179ms,开启速度14764ms
            //integerArray = new Integer[64]; //不开启逃逸分析速度7695ms,开启速度18ms 
            //integerArray = new Integer[65]; //不开启逃逸分析速度8120ms,开启速度8027ms
            //objectArray = new Test[64]; //不开启逃逸分析速度8120ms,开启速度18ms
            //objectArray = new Test[65]; //不开启逃逸分析速度8120ms,开启速度8008ms
        }
        long endTime = System.currentTimeMillis();
        System.out.println("time: " + (endTime - beginTime) + "ms");
    }
    public static class Test{
        Integer a;
        String b;
    }
}

开启逃逸分析参数为:-Xmx5m -Xms5m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:+EliminateAllocations

实例化长度\类型intlongIntegerTest
6418ms27ms18ms18ms
658804ms14764ms8027ms8008ms

关闭逃逸分析参数为:-Xmx5m -Xms5m -XX:+PrintGC -XX:-DoEscapeAnalysis -XX:+EliminateAllocations

实例化长度\类型intlongIntegerTest
647749ms14763ms7695ms8120ms
658144ms15179ms8120ms8120ms

以上结果可以说明:数组在长度64以上时会被认为是大对象,无法进行栈上分配优化。
64是可以通过配置(感谢公司大佬帮忙找到)
HotSpot JVM上的一个默认限制是大于64个元素的数组不会进行逃逸分析优化。这个大小可以通过启动参数-XX:EliminateAllocationArraySizeLimit=n来进行控制,n是数组的大小。

int和Integer栈上分配的优化情况:


// -Xmx5m -Xms5m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
// int是直接分配在栈上的,基础数据类型不需要开启逃逸分析就分配在栈上;
// Integer在-128~127时,是缓存的对象,在范围外是重新实例化的对象,所以逃逸分析对重新实例化的对象有效
public class EscapeTest6 {
    public static void main(String[] args) {
        long beginTime = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            int x = 127; //不开启逃逸分析速度1ms,开启速度1ms
            //int x = 128; //不开启逃逸分析速度2ms,开启速度1ms
            //Integer x = 127; //不开启逃逸分析速度2ms,开启速度3ms
            //Integer x = 128; //不开启逃逸分析速度661ms,开启速度5ms
        }
        long endTime = System.currentTimeMillis();
        System.out.println("time: " + (endTime - beginTime) + "ms");
    }
}

以上对多种情况进行试验

  1. int是直接分配在栈上的,基础数据类型不需要开启逃逸分析就分配在栈上;
  2. Integer在-128~127时,是缓存的对象,在范围外是重新实例化的对象,所以逃逸分析对重新实例化的对象有效

标量替代(Scalar Replacement)

什么是标量:
标量是指不可分割的量,如java中基本数据类型和reference类型,相对的一个数据可以继续分解,称为聚合量

什么是标量替换:
如果把一个对象拆散,将其成员变量恢复到基本类型来访问就叫做标量替换

如何进行标量替换:
如果逃逸分析发现一个对象不会被外部访问,并且该对象可以被拆散,那么经过优化之后,并不直接生成该对象,而是在栈上创建若干个成员变量

标量替换需要有一定的前提:

  • 开启逃逸分析 (-XX:+DoEscapeAnalysis)
    逃逸分析的作用就是分析对象的作用域是否会逃逸出方法之外,在server虚拟机模式下才可以开启(jdk1.6默认开启)
  • 开启标量替换 (-XX:+EliminateAllocations)
    标量替换的作用是允许将对象根据属性打散后分配在栈上,比如若一个对象拥有两个字段,会将这两个字段视作局部变量进行分配。默认该配置为开启

什么逃逸状态下可进行标量替换优化:
NoEscape

锁消除(Synchronization Elimination)

什么是锁消除:
线程同步本身是一个相对耗时的过程,如果逃逸分析 能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉。

锁消除需要有一定的前提:

  • 开启逃逸分析 (-XX:+DoEscapeAnalysis)
    逃逸分析的作用就是分析对象的作用域是否会逃逸出方法之外,在server虚拟机模式下才可以开启(jdk1.6默认开启)
  • 开启锁消除(-XX:+EliminateLocks)
    线程同步本身是一个相对耗时的过程,如果逃逸分析 能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争, 对这个变量实施的同步措施也就可以安全地消除掉。默认该配置为开启

什么逃逸状态下可进行锁消除优化:
NoEscape、ArgEscape

怎样子的锁才能进行锁消除优化:

// -Xmx1m -Xms1m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:+EliminateAllocations -XX:+EliminateLocks
public class SynTest {
    public static void main(String[] args) {
        long beginTime = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            SynTest synTest = new SynTest();
            synTest.addSyn();// 带synchronized,开启逃逸分析23ms,不开启逃逸分析156675ms
            //synTest.add();// 不带synchronized,开启逃逸分析29ms,不开启逃逸分析27ms
        }
        long endTime = System.currentTimeMillis();
        System.out.println("time: " + (endTime - beginTime) + "ms");
    }

    public static void main1(String[] args) {
        long beginTime = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            SynTest synTest = new SynTest();
            for(int j = 0; j < 5; j++) {
                synTest.addSyn();// 带synchronized,开启逃逸分析15676ms,不开启逃逸分析12756ms
                //synTest.add();// 不带synchronized,开启逃逸分析3543ms,不开启逃逸分析4634ms
            }
        }
        long endTime = System.currentTimeMillis();
        System.out.println("time: " + (endTime - beginTime) + "ms");
    }

    public synchronized void addSyn() {
        int a = 0;
    }

    public void add() {
        int a = 0;
    }
}

结论:在循环内部实例化并调用带锁的方法,可以进行锁消除优化
(为什么循环外面就不可以了呢?)
锁消除对性能的影响:
java.lang.StringBuffer是一个使用同步方法的线程安全的类,非同步的java.lang.StringBuilder类来作为它的备选。这两个类都继承了包私有(注:简单来说就是没有修饰符的类)的java.lang.AbstractStringBuilder类,它们的length方法的实现也非常类似。

  @Override
  public int length() {
      return count;
  }
   @Override
   public synchronized int length() {
       return count;
   }
// -Xmx1m -Xms1m -XX:+PrintGC -XX:+DoEscapeAnalysis -XX:+EliminateAllocations -XX:+EliminateLocks
public class StringTest {
  public static void main(String[] args) {
      long beginTime = System.currentTimeMillis();
      for (int i = 0; i < 100000000; i++) {
          StringBuilder stringBuilder = new StringBuilder(); // 需在里面实例化
          stringBuilder.length();// 不带synchronized,开启锁消除1562ms,不开启锁消除1530ms
          /*StringBuffer stringBuffer = new StringBuffer();
          stringBuffer.length();// 带synchronized,开启锁消除1577ms,不开启锁消除3707ms*/
      }
      long endTime = System.currentTimeMillis();
      System.out.println("time: " + (endTime - beginTime) + "ms");
  }
}

结论: 开启锁消除对带锁的StringBuffer,性能上有显著的提升。

疑问

  1. 标量替换和栈上分配的区别?
  2. 大对象不能分配到栈上的界限是由谁决定的?
  3. 锁消除的锁是什么程度的锁,在前面的StringBuffer的例子中,若将实例化StringBuffer放在循环外,开启锁消除不会提升性能,这里是不是出现锁加粗?
  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值