JVM运行期优化及逃逸分析实战

2020-04-07  本文已影响0人  qiaoflin

运行期优化

楼主最近在网上看到一篇写关于JVM运行期优化的博客,经过整理,现在分享给大家:

image.png

我们知道,Java 是解释执行的,可是解释执行毕竟还是有点慢的,这也使得 Java 一直被认为是效率低下的语言……,不过随着即时编译技术的发展,Java 的运行速度得到了很大的提升,在本篇文章中,我们将会对 Java 的运行期优化,也就是即时编译 (Just In Time, JIT) 时进行的优化进行详细的讲解,我们先来看看什么是即时编译。

即时编译

什么是即时编译?

HotSpot 虚拟机内的即时编译器运作过程

我们主要通过以下 5 个问题来了解 HotSpot 虚拟机的即时编译器。

为什么要使用解释器与编译器并存的架构?

为什么虚拟机要实现两个不同的 JIT 编译器?

什么是虚拟机的分层编译?

分层编译就是根据编译器编译、优化的规模与耗时,划分出不同的编译层次,在代码运行的过程中,可以动态的选择将某一部分代码片段提升一个编译层次或者降低一个编译层次。

C1 与 C2 编译器会同时工作,许多代码可能会被多次编译。

目的: 在程序的启动响应时间和运行效率间达到平衡。

编译层次的划分:

如何判断热点代码,触发编译?

什么是热点代码?

我们发现,判断热点代码的一个要点就是: 多次执行 。那么虚拟机是如何知道一个方法或者一个循环体被多次执行的呢?

什么是 “多次” 执行?

HotSpot 中每个方法的 2 个计数器

HotSpot 热点代码探测流程

基于计数器的热点代码探测流程.png

热点代码编译的过程?

虚拟机在代码编译未完成时会按照解释方式继续执行,编译动作在后台的编译线程执行。

禁止后台编译:-XX: -BackgroundCompilation,打开后这个开关参数后,交编译请求的线程会等待编译完成,然后执行编译器输出的本地代码。

经典优化技术介绍

Content:

公共子表达式消除【语言无关】

如果一个表达式 E 已经计算过了,并且从先前的计算到现在,E 中所有变量值都没有发生变化,则 E 为公共子表达式,无需再次计算,直接用之前的结果替换。

数组范围检查消除【语言相关】

在循环中使用循环变量访问数组,如果可以判断循环变量的范围在数组的索引范围内,则可以消除整个循环的数组范围检查

方法内联【最重要】

目的是:去除方法调用的成本(如建立栈帧等),并为其他优化建立良好的基础,所以一般将方法内两放在优化序列最前端,因为它对其他优化有帮助。

类型继承关系分析(Class Hierarchy Analysis,CHA)

用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类、子类是否为抽象类等。

逃逸分析【最前沿】

基本行为

分析对象的作用域,看它有没有能在当前作用域之外使用:

对于不会逃逸到方法或线程外的对象能进行优化

虚拟机参数

一个优化的例子

原始代码:

static class B {
    int value;
    final int get() {
        return value;
    }
}

public void foo() {
    y = b.get();
    // ...do stuff...
    z = b.get();
    sum = y + z;
}

第一步优化: 方法内联(一般放在优化序列最前端,因为对其他优化有帮助)

目的:

public void foo() {
    y = b.value;
    // ...do stuff...
    z = b.value;
    sum = y + z;
}

第二步优化: 公共子表达式消除

public void foo() {
    y = b.value;
    // ...do stuff...  // 因为这部分并没有改变 b.value 的值
                       // 如果把 b.value 看成一个表达式,就是公共表达式消除
    z = y;             // 把这一步的 b.value 替换成 y
    sum = y + z;
}

第三步优化: 复写传播

public void foo() {
    y = b.value;
    // ...do stuff...
    y = y;             // z 变量与以相同,完全没有必要使用一个新的额外变量
                       // 所以将 z 替换为 y
    sum = y + z;
}

第四步优化: 无用代码消除

无用代码:

  • 永远不会执行的代码
  • 完全没有意义的代码
public void foo() {
    y = b.value;
    // ...do stuff...
    // y = y; 这句没有意义的,去除
    sum = y + y;
}


逃逸分析实战

实验准备
  1. 强制开启逃逸分析(JVM默认开启太医)
    -XX:+DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m
  2. 关闭逃逸分析
    -XX:-DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m
代码实例
public class OnStackTest {
    public static void alloc() {
        byte[] b = new byte[2];
        b[0] = 1;
    }

    public static void main(String[] args) {
        long b = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            alloc();
        }
        long e = System.currentTimeMillis();
        System.out.println(e - b);
    }
}
实验结果
  1. 开启逃逸的运行结果


    开启逃逸分析
  2. 关闭逃逸的运行结果


    关闭逃逸分享
实验分析

分析一下,这里是将2个字节的数据循环分配1千万次,开启逃逸的运行时间为28milisecond, 而未开启则为2726, 为未开启的将近1/100.
差异效果还是非常明显的…..

实验总结

栈上的空间一般而言是非常小的,只能存放若干变化和小的数据结构,大容量的存储结构是做不到。这里的例子是一个极端的千万次级的循环,突出了通过逃逸分析,让其直接从栈上分配,从而极大降低了GC的次数,提升了程序整体的执行效能。
所以,逃逸分析的效果只能在特定场景下,满足高频和高数量的容量比较小的变量分配结构,才可以生效。

转载于

  1. https://github.com/TangBean/understanding-the-jvm/blob/master/Ch4-Java%E7%A8%8B%E5%BA%8F%E8%BF%90%E8%A1%8C%E4%BC%98%E5%8C%96/00-Java%E8%BF%90%E8%A1%8C%E6%9C%9F%E4%BC%98%E5%8C%96.md#%E9%80%83%E9%80%B8%E5%88%86%E6%9E%90%E6%9C%80%E5%89%8D%E6%B2%BF
  2. https://blog.csdn.net/blueheart20/article/details/76167489
上一篇下一篇

猜你喜欢

热点阅读