阅读 52

JVM系列之逃逸分析

一、前言

思考:对象是否都是创建在堆中?

答案是否定的,Java对象的创建一般情况是在堆内存中,当堆内存空间不足时,就会触发GC回收,GC次数多了就会影响到程序的性能;
因此逃逸分析就是为了缓解这一问题而诞生的。

二、什么是逃逸分析

因为堆内存空间总是有限的,随着对象的积累,gc的次数会越来越频繁,这显然会影响到程序的整体性能;

逃逸分析的目的是分析新创建对象的使用范围,并决定对象是否可以分配在Java堆上;

内存逃逸现象:
当一个对象不仅在方法中被调用,还在其他地方被引用了,当这个方法结束时,gc无法就立即回收这个对象。

三、逃逸状态

  1. 全局逃逸(GlobalEscape):一个对象的作用范围逃出了当前方法或者当前线程

全局逃逸的场景:

  • 一个对象的引用是复制给了一个类变量
  • 存储在一个已经逃逸的对象当中
  • 这个对象的引用作为方法的返回值返回给了调用方法
  1. 参数逃逸(ArgEscape):一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这种状态可以通过分析被调方法的二进制代码确定

  2. 没有逃逸(NoEscape):顾名思义,就是没有逃逸

  • 开启逃逸分析(JDK1.8 默认开启):-XX:+DoEscapeAnalysis
  • 关闭逃逸分析:-XX:-DoEscapeAnalysis
  • 显示分析结果:-XX:+PrintEscapeAnalysis

四、逃逸分析的作用

示例代码:

/**
 * 测试逃逸分析
 *
 * @author 小白
 * @date 2021/4/3 14:31
 */
public class Test {
  public static void main(String[] args) throws InterruptedException {
    long t1 = System.currentTimeMillis();
    for (int i = 0; i < 100000000; i++) {
      getObject();
    }
    long t2 = System.currentTimeMillis();
    System.out.println(t2 - t1);
    TimeUnit.MINUTES.sleep(10);
  }

  private static void getObject() {
    Object obj = new Object();
  }
}

开启逃逸分析:

开启逃逸.png

image.png

关闭逃逸分析:

image.png

image.png

分析:

开启逃逸分析时,jvm中大概产生11万左右的对象,代码耗时3ms,关闭逃逸分析时,大概产生60w左右的对象,代码耗时428ms,比较来看,看起逃逸分析是有助于虚拟机性能提升的。

1.标量替换

基础类型和对象的引用可以理解为标量(不能被进一步分解);
对象就是聚合量,能被进一步分解。

用其成员变量(分散的标量)代替该对象整体,从而不需要连续的存储空间,就可以将对象的部分甚至全部都保存在CPU寄存器内,这就叫做标量替换。

如果一个对象没有发生逃逸,那就不用创建这个对象,虚拟机只会在栈或者寄存器上创建它会使用到的成员标量,节省了内存空间,从而提升了应用程序性能。

java -XX:+DoEscapeAnalysis -XX:+EliminateAllocations Test
代码执行时间:3 ms

java -XX:+DoEscapeAnalysis -XX:-EliminateAllocations Test
代码执行时间:415 ms

java -XX:-DoEscapeAnalysis -XX:+EliminateAllocations Test
代码执行时间:392 ms

java -XX:-DoEscapeAnalysis -XX:-EliminateAllocations Test
代码执行时间:387 ms

分析

开启和关闭标量替换对于程序性能的影响很大,同时也验证了标量替换功能生效的前提是逃逸分析已经开启,否则没有意义。

  • 开启标量替换:-XX:+EliminateAllocations
  • 关闭标量替换:-XX:-EliminateAllocations
  • 显示标量替换详情:-XX:+PrintEliminateAllocations

2. 锁消除

示例代码:

  private static void getObject() {
    Object obj = new Object();
    synchronized (obj) {
      obj.toString();
    }
  }

java -XX:+DoEscapeAnalysis -XX:+EliminateLocks Test
代码执行时间:4 ms

java -XX:+DoEscapeAnalysis -XX:-EliminateLocks Test
代码执行时间:388 ms

分析

开启和关闭锁消除对于程序性能的影响也是很大。

  • 开启锁消除(JDK1.8 默认开启):-XX:+EliminateLocks
  • 关闭锁消除:-XX:-EliminateLocks

3.栈内存分配

将原本分配在堆内存上的对象转而分配在栈内存上,减少堆内存的占用,从而减少 GC 的次数

Oracle HotSpot 虚拟机并没有这么做

五、非逃逸分析优化

  1. 锁粗化
    当连续获取同一个对象的锁时,HotSpot虚拟机会去检查多个锁区域是否能合并成一个更大的锁区域。这种聚合被称作锁粗化,它能够减少加锁和解锁的消耗。

  2. 嵌套锁
    同步块可能会一个嵌套一个,进而两个块使用同一个对象的监视器锁来进行同步也是很有可能的。这种情况我们称之为嵌套锁,HotSpot虚拟机是可以识别出来并删除掉内部块中的锁的。当一个线程进入外部块时就已经获取到锁了,因此当它尝试进入内部块时,肯定也仍持有这个锁,所以这个时候删除锁是可行的。

总结

开发中尽量使用局部变量的,就不要使用在方法外定义。

作者:简楼

原文链接:https://www.jianshu.com/p/6866a3699be3

文章分类
后端
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐