逃逸分析技术算是在JVM面试题偶有提及的一个考察点,当然如果你能够讲解JVM工作原理的时候提及这一点,这一定会增加面试官对你的好感,本文主题内容如下:
- 什么是逃逸分析技术?
- 逃逸分析技术解决什么问题?带来什么好处?
- 如何更好的理解或者运用逃逸分析技术?
一、什么是逃逸分析
逃逸分析技术是JVM用于提高性能以及节省内存的手段,在JVM编译语境下也就是我们常说的JIT阶段,逃逸分析技术通过以下两个条件判断该对象是否是逃逸:
- 该对象是否分配在堆上(static关键字或者成员变量)。
- 该对象是否会传给未知代码,比如return到外部给别的类使用。
只要编译阶段判定当前对象并没有发生逃逸,那么它就会采用栈上分配、标量替换、同步锁消除等手段提升程序执行性能和节省内存开销。
那么我们又该如何判断对象是否逃逸呢?我们不妨基于上述的判断条件来看看这个示例,假设我们现在有一个user类:
@Data
public class User {
private int id;
private String name;
}
我们通过UserService进行初始化,那么请问这段代码是否发生逃逸呢?
public class UserService {
private User user;
public void init() {
user = new User();
user.setId(RandomUtil.randomInt(10));
user.setName(RandomUtil.randomString(3));
}
}
答案当然是肯定的,因为这段代码会被外部的其他任意线程操作。
再来看看这段代码,典型的return语句,很明显的外部线程可以直接操作这个对象,所以这个对象也发生了逃逸,所以针对这几种情况JIT都无法对其进行优化。
public User createUser() {
User user = new User();
user.setId(RandomUtil.randomInt(10));
user.setName(RandomUtil.randomString(3));
return user;
}
二、如何运用到逃逸分析技术
1.栈上分配
一般来说,JIT即时编译技术中的栈上分配和标量替换基本都是同时出现的,按照上文所述,假如上述代码所返回的user对象仅仅是获取当前用户的年龄,那么我们就可以直接在方法内完成逻辑计算并直接返回,这样对象就没有发生逃逸,如此对象便可直接在栈帧上进行分配,有效减小JVM垃圾回收的压力。
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 对象并没有发生逃逸,它仅仅是根据外部传入的count完成拼接并打印结果而已,于是JIT就会进行锁消除的优化操作。如下字节码所示,优化后的StringBuffer被替换为StringBuilder。
三、逃逸分析更进一步
了解了逃逸分析止之后,我们不妨基于下面这些题目进行一下自测,如下代码,请问实例方法调用静态方法,StringBuffer作为变量传入,是否发生逃逸,最终执行代码是StringBuffer 还是StringBuilder?
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 + " ");
}
}
答案是未发生逃逸,因为对象并没有被外部线程操作,JIT感知到未发生逃逸,所以将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:
四、小结
合理的在栈帧上解决问题可以避免对象逃逸,从而让JIT尽可能的去进行优化,这一点我想应该是一个Java程序员对于代码的极致追求了。