JVM学习03节-逃逸分析(JIT编译优化)

逃逸分析

1. 简述

首先了解即时编译器(Just-in-time Compilation,JIT)的优化技术(大概小一百个吧),只学习逃逸分析即可。在《JVM运行时数据区域》章节中我们说过所有对象实例分配在堆上也不是那么“绝对”了,其实就是逃逸分析的优化。

2. 逃逸分析 Escape Analysis

逃逸分析的基本行为就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。

方法逃逸的几种方式如下:

public class Test {
    public static Object obj;
    
    public void globalVariableEscape() {  
        // 给全局变量赋值,发生逃逸
        obj = new Object();
    }
    
    //sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,
    //这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。
    //如果想要StringBuffer sb不逃出方法,可以这样写: return sb.toString();
    public static StringBuffer craeteStringBuffer(String s1, String s2) {
        StringBuffer sb = new StringBuffer();
        sb.append(s1);
        sb.append(s2);
        return sb;
    }
    
    public void instanceEscape() {  
        // 实例引用发生逃逸
        test(this); 
    }
}

逃逸分析的配置

  • -XX:+DoEscapeAnalysis开启逃逸分析(jdk1.7后默认开启)
  • -XX:-DoEscapeAnalysis 关闭逃逸分析

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

  • 栈上分配
  • 同步消除
  • 标量替换

2.1 栈上分配

如果能够通过逃逸分析确定某些对象不会逃出方法之外,那就可以让这个对象在栈上分配内存,这样该对象所占用的内存空间就可以随栈帧出栈而销毁,就减轻了垃圾回收的压力。

private static void alloc() {
	Point point = new Point(1, 2);
	int sum = point.x + point.y;
    log.debug("sum:{}", sum);
}

point 和 sum 并没有发生逃逸,JIT 在优化的时候,就会把变量和对象分配在栈上,方法执行完后自动销毁,而不需要垃圾回收的介入,从而提高系统性能。

2.2 同步消除(锁消除)

如果确定一个对象不会逃逸出线程,无法被其它线程访问到,那该对象的读写就不会存在竞争,对这个变量的同步措施就可以消除掉。单线程中是没有锁竞争。(锁和锁块内的对象不会逃逸出线程就可以把这个同步块取消)

 private static void alloc() {
     Point point = new Point(1, 2);
     synchronized (point) {
         int sum = point.x + point.y;
         log.debug("sum:{}", sum);
     }
 }

代码中对 point 这个对象进行加锁,但是 point 对象的生命周期只在 alloc() 方法中,并不会被其他线程所访问到,所以在 JIT 编译阶段就会被优化掉。优化成:

private static void alloc() {
	Point point = new Point(1, 2);
	int sum = point.x + point.y;
    log.debug("sum:{}", sum);
}

2.3 标量替换

Java虚拟机中的原始数据类型(int,long等数值类型以及reference类型等)都不能再进一步分解,它们就可以称为标量。相对的,如果一个数据可以继续分解,那它称为聚合量,Java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象是可分解的,那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。拆散后的变量便可以被单独分析与优化,可以各自分别在栈帧或寄存器上分配空间,原本的对象就无需整体分配空间了。

@Slf4j
public class EscapeAnalysisTest {

    private static void alloc() {
        Point point = new Point(1, 2);
        int sum = point.x + point.y;
        log.debug("sum:{}", sum);
    }

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

@AllArgsConstructor
class Point {
    public int x;
    public int y;
}

以上代码中,point 对象并没有逃逸出 alloc() 方法,并且 point 对象是可以拆解成标量的。那么,JIT 就会不会直接创建 point 对象,而是直接使用两个标量int x ,int y来替代Point对象。以上代码,经过标量替换后,就会变成:

@Slf4j
public class EscapeAnalysisTest {

    private static void alloc() {
        int x = x;
        int y = y;
        int sum = x + y;
        log.debug("sum:{}", sum);
    }

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

2. 逃逸分析测试

在测试的过程需要开启GC日志,通过-XX:+PrintGCDetails设置。多个设置用空格隔开(idea)

3.1 栈上分配测试

-XX:+DoEscapeAnalysis 开启逃逸分析(jdk1.7 后默认开启)

-XX:-DoEscapeAnalysis 关闭逃逸分析

代码如下:

@Slf4j
public class EscapeAnalysisTest {

    private static void alloc() {
        Point point = new Point(1, 2);
        int sum = point.x + point.y;
        log.debug("sum:{}", sum);   
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        log.info("耗时:{}", System.currentTimeMillis() - start);
    }
}

@AllArgsConstructor
class Point {
    public int x;
    public int y;
}

开启逃逸分析,执行的时间为32毫秒,日志如下

[2019-04-29 16:49:49 909][main][EscapeAnalysisTest:30]耗时:32

关闭逃逸分析,执行的时间为327毫秒,并且伴随的大量的GC日志信息。日志如下

[GC (Allocation Failure) [PSYoungGen: 31744K->2443K(36864K)] 31744K->2451K(121856K), 0.0067768 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 34187K->1528K(36864K)] 34195K->1544K(121856K), 0.0018649 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 33272K->1528K(36864K)] 33288K->1544K(121856K), 0.0013963 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 33272K->1528K(68608K)] 33288K->1552K(153600K), 0.0015636 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 65016K->1528K(68608K)] 65040K->1552K(153600K), 0.0041579 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[2019-04-29 16:53:37 980][main][EscapeAnalysisTest:30]耗时:327

通过上面开启和关闭逃逸分析:开启逃逸分析,对象没有分配在堆上,没有进行GC,而是把对象分配在栈上。关闭逃逸分析,对象全部分配在堆上,当堆中对象存满后,进行多次GC,导致执行时间大大延长。

即时编译器(Just-in-time Compilation,JIT)
1、使用client编译器时,默认执行为1500次才认为是热代码;
2、使用server编译器时,默认执行为10000次才认为是热代码;
上面的例子开启逃逸分析后,并不是所有的对象都直接在栈上分配,而是通过 JIT 分析此代码是热代码,才进行异步编译成本地机器码,并通过逃逸分析,把对象分配到栈上。(如果是server编译器:在前10000次循环和编译成本地机器码这段时间,对象都会在堆中分配对象,编译成本地机器码后才会在栈上分配)

3.2 同步消除测试

-XX:+EliminateLocks 开启锁消除
-XX:-EliminateLocks 关闭锁消除
锁消除基于分析逃逸基础之上,开启锁消除必须开启逃逸分析

把上面的代码稍微修改了下,加了一个同步块。使用数组替换 point 对象,数组长度大于64的是不会在栈上分配,我们都以堆上分配为例来测试锁消除带来的影响。测试代码如下:

@Slf4j
public class EscapeAnalysisTest {

    private static void alloc() {
        byte[] arr = new byte[65];
        synchronized (arr) {
            arr[0]=0;
        }
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        log.info("耗时:{}", System.currentTimeMillis() - start);
    }
}

开启同步消除,执行的时间为286毫秒,日志如下

[GC (Allocation Failure) [PSYoungGen: 31744K->2536K(36864K)] 31744K->2544K(121856K), 0.0041677 secs] [Times: user=0.05 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 34280K->1576K(36864K)] 34288K->1592K(121856K), 0.0014835 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 33320K->1512K(36864K)] 33336K->1528K(121856K), 0.0020325 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 33256K->1512K(68608K)] 33272K->1528K(153600K), 0.0043267 secs] [Times: user=0.03 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 65000K->1528K(68608K)] 65016K->1544K(153600K), 0.0020884 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 65016K->1544K(129536K)] 65032K->1560K(214528K), 0.0014352 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 128520K->0K(129536K)] 128536K->1510K(214528K), 0.0020099 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 126976K->0K(257024K)] 128486K->1510K(342016K), 0.0004067 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 253952K->0K(257024K)] 255462K->1510K(342016K), 0.0006921 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[2019-04-29 17:02:35 158][main][EscapeAnalysisTest:29]耗时:286

关闭同步消除,执行的时间为457毫秒,日志如下

[GC (Allocation Failure) [PSYoungGen: 31744K->2440K(36864K)] 31744K->2448K(121856K), 0.0031128 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 34184K->1544K(36864K)] 34192K->1560K(121856K), 0.0022394 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 33288K->1528K(36864K)] 33304K->1544K(121856K), 0.0021265 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 33272K->1528K(68608K)] 33288K->1544K(153600K), 0.0019755 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 65016K->1528K(68608K)] 65032K->1552K(153600K), 0.0015081 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 65016K->1544K(129536K)] 65040K->1568K(214528K), 0.0021424 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 128520K->0K(129536K)] 128544K->1513K(214528K), 0.0030146 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 126976K->0K(257024K)] 128489K->1513K(342016K), 0.0003678 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 253952K->0K(257024K)] 255465K->1513K(342016K), 0.0007280 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] 
[2019-04-29 17:02:53 396][main][EscapeAnalysisTest:29]耗时:457

通过上面开启和关闭同步消除:开启后要比关闭后性能提升很多。

3.3 标量替换测试

-XX:+EliminateAllocations 开启标量替换
-XX:-EliminateAllocations 关闭标量替换
标量替换基于分析逃逸基础之上,开启标量替换必须开启逃逸分析

测试代码如下:

@Slf4j
public class EscapeAnalysisTest {

    private static void alloc() {
        Point point = new Point(1, 2);
        int sum = point.x + point.y;
        log.debug("sum:{}", sum);
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        log.info("耗时:{}", System.currentTimeMillis() - start);
    }
}

@AllArgsConstructor
class Point {
    public int x;
    public int y;
}

开启标量替换,执行的时间为41毫秒,日志如下

[2019-04-29 17:13:24 218][main][EscapeAnalysisTest:28]耗时:41

关闭标量替换,执行的时间为121毫秒,并且伴随的大量的GC日志信息。日志如下

[GC (Allocation Failure) [PSYoungGen: 31744K->2456K(36864K)] 31744K->2464K(121856K), 0.0038336 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 34200K->1544K(36864K)] 34208K->1560K(121856K), 0.0019355 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 33288K->1544K(36864K)] 33304K->1560K(121856K), 0.0017686 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 33288K->1528K(68608K)] 33304K->1544K(153600K), 0.0015481 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 65016K->1512K(68608K)] 65032K->1528K(153600K), 0.0022685 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[2019-04-29 17:13:42 657][main][EscapeAnalysisTest:28]耗时:121

这次我们把标量替换功能关闭,我们发现对象又分配到堆里面了,并执行了多次GC。由此可以看出java中没有实现真正意义上的栈上分配,而是通过标量替换来实现栈上分配

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值