逃逸分析在 Java 中的应用与优化

开发
逃逸分析技术是JVM用于提高性能以及节省内存的手段,在JVM编译语境下也就是我们常说的JIT阶段,。

逃逸分析技术算是在JVM面试题偶有提及的一个考察点,当然如果你能够讲解JVM工作原理的时候提及这一点,这一定会增加面试官对你的好感,通过对本篇文章的阅读,你将能够从容的解决以下几个面试题:

  • 什么是逃逸分析技术?
  • 逃逸分析技术解决什么问题?带来什么好处?
  • 如何更好的理解或者运用逃逸分析技术?

什么是逃逸分析

逃逸分析技术是JVM用于提高性能以及节省内存的手段,在JVM编译语境下也就是我们常说的JIT阶段,关于逃逸分析的概念,引用《深入理解Java虚拟机》的说法:

逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。

只要编译阶段判定当前对象并没有发生逃逸,那么它就会采用栈上分配、标量替换、同步锁消除等手段提升程序执行性能和节省内存开销,具体场景还得查看是发生方法逃逸还是线程逃逸。

那么我们又该如何判断对象是否逃逸呢?我们不妨基于上述的判断条件来看看这个示例,假设我们现在有一个user类,我们通过UserService进行初始化,那么请问这段代码是否发生逃逸呢?

public class UserService {

    private User user;

    public void init() {
        user = new User();
        user.setId(RandomUtil.randomInt(10));
        user.setName(RandomUtil.randomString(3));
    }
}

答案当然是肯定的,因为这段代码方法内所创建的对象被外部的main函数所引用,也就是我们所说的方法逃逸。

再来看看这段代码,典型的在方法内创建然后被外部函数所引用,也就是所谓的方法逃逸:

public User createUser() {
        User user = new User();
        user.setId(RandomUtil.randomInt(10));
        user.setName(RandomUtil.randomString(3));
        return user;
    }

而这段stringBuffer 已经被其他线程实例所访问到,也就是典型的线程逃逸:

public static void main(String[] args) throws InterruptedException {
        StringBuffer stringBuffer = new StringBuffer();
        CountDownLatch countDownLatch = new CountDownLatch(3);
        //调用appendStr操作stringBuffer
        new Thread(() -> {
            appendStr(stringBuffer);
            countDownLatch.countDown();
        }).start();

        //循环拼接操作stringBuffer
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                stringBuffer.append("aaa");
            }
            countDownLatch.countDown();
        }).start();
        
        //循环拼接操作stringBuffer
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                stringBuffer.append("aaa");
            }
            countDownLatch.countDown();
        }).start();

        countDownLatch.await();
        System.out.println(stringBuffer);
    }

如何运用到逃逸分析技术

1.栈上分配(针对未逃逸或方法逃逸)

下面这段代码仅在方法内部完成对象创建或者打印,其对象并没有被外部方法所引用和暴露,对象就没有发生逃逸,对于没有发生逃逸的代码或者上文中方法逃逸的代码端,JIT会通过栈上分配减少内存占用和GC压力。

 Map<Integer, User> userMap = new HashMap<>();


    public int getUserAgeById(int id) {
       User user = new User();
        user.setId(RandomUtil.randomInt(10));
        user.setName(RandomUtil.randomString(3));
        //打印用户信息
        printUserInfo(user);
    }

2.分离对象或标量替换(针对未逃逸)

如果仅仅是操作未逃逸对象的某些简单运算,我们同样可以只在栈帧内使用这个对象,如此JVM就会将这个对象打散,将对象打散为无数个小的局部变量,实现标量替换。

如下所示,这段代码没有发生任何逃逸,JVM会避免创建Point ,而是通过栈上创建基本变量完成逻辑操作:

public static void main(String args[]) {
    alloc();
}
class Point {
    private int x;
    private int y;
}
private static void alloc() {
    Point point = new Point(1,2);
    System.out.println("point.x" + point.x + ";point.y" + point.y);
}

进而直接标量替换,直接在栈上分配x和y的值,完成输出打印。

private static void alloc() {
    int x = 1;
    int y = 2;
    System.out.println("point.x = " + x + "; point.y=" + y);
}

3.同步锁消除(针对未逃逸线程)

这一点就比较有趣了,我们都知道使用StringBuffer可以保证线程安全,因为其操作函数都有带synchronized关键字,那么请问这段代码会上锁吗?

public void appendStr(int count) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < count; i++) {
            sb.append("no: " + i + " ");
        }
    }

答案是不会,因为我们当前操作的StringBuffer 对象并没有发生线程逃逸,它仅仅在函数内部进行字符串操作,所以针对appendStr内部逻辑,其直接将其优化为StringBuilder:

4.线程逃逸分析的更进一步

请问实例方法调用静态方法,StringBuffer作为变量传入,是否发生逃逸,直接创建一个main方法调用这段代码,方法是否发生逃逸?

public void appendStr(int count) {
        StringBuffer sb = new StringBuffer();
        loop(count, sb);
    }

    private static void loop(int count, StringBuffer sb) {
        for (int i = 0; i < count; i++) {
            sb.append("no: " + i + " ");
        }
    }

答案是发生了方法逃逸,但是没有发生线程逃逸,但我们的代码是单线程执行这段代码,即使StringBuffer 由外部传入,函数内部依然可以进行锁消除将其内部的拼接逻辑用StringBuilder进行字符串拼接:

再来看看这段代码,请问发生逃逸了吗?

 public void appendStr(int count) {
        StringBuffer sb = new StringBuffer();
        loop(count, sb);
    }

    private static String loop(int count, StringBuffer sb) {
        for (int i = 0; i < count; i++) {
            sb.append("no: " + i + " ");
        }
        return sb.toString();
    }

答案是没有发生线程逃逸,返回的字符串还是没有被外部线程操作,所以最终还是被转为StringBuilder:

而下面这段代码就是典型的逃逸,可以看到多线程场景下StringBuffer 被多线程共享和访问,此时JIT优化就会视为对象逃逸:

public static void main(String[] args) throws InterruptedException {
        StringBuffer stringBuffer = new StringBuffer();
        CountDownLatch countDownLatch = new CountDownLatch(3);
        //调用appendStr操作stringBuffer
        new Thread(() -> {
            appendStr(stringBuffer);
            countDownLatch.countDown();
        }).start();

        //循环拼接操作stringBuffer
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                stringBuffer.append("aaa");
            }
            countDownLatch.countDown();
        }).start();

        //循环拼接操作stringBuffer
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                stringBuffer.append("aaa");
            }
            countDownLatch.countDown();
        }).start();

        countDownLatch.await();
        System.out.println(stringBuffer);
    }

    public static void appendStr(StringBuffer stringBuffer) {
        for (int i = 0; i < 10; i++) {
            stringBuffer.append(i);
        }

    }

所以appendStr在判定线程逃逸之后,并没有将StringBuffer变为StringBuilder:

小结

合理的在栈帧上解决问题可以避免对象逃逸,从而让JIT尽可能的去进行优化,这一点我想应该是一个Java程序员对于代码的极致追求了。

责任编辑:赵宁宁 来源: 写代码的SharkChili
相关推荐

2018-07-09 15:11:14

Java逃逸JVM

2024-07-23 08:06:19

缓存技术策略

2020-07-21 14:19:18

JVM编程语言

2024-04-07 11:33:02

Go逃逸分析

2022-05-10 11:23:56

漏洞补洞过程入侵检测

2020-05-13 15:10:04

矩阵乘法深度学习人工智能-

2010-10-11 09:28:07

2023-04-25 08:01:23

JavaQuarkusKubernetes

2010-09-02 09:15:33

协议分析器Wi-Fi

2009-03-03 09:56:00

协议分析器WLAN

2020-08-14 10:00:34

Node前端应用

2024-03-04 08:00:00

Java开发

2010-02-23 10:25:29

2011-06-20 15:55:14

SEO

2012-03-27 14:04:54

JavaEnum

2021-10-14 10:22:19

逃逸JVM性能

2011-01-21 10:01:07

jQueryjavascriptweb

2023-10-26 06:55:17

风控系统应用

2009-06-11 13:52:25

协同软件Java

2009-05-05 12:00:32

虚拟化部署应用
点赞
收藏

51CTO技术栈公众号