写在文章开头
你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,这里面会有笔者精心挑选的并发、JVM、MySQL数据库专栏,也有笔者日常分享的硬核技术小文。
逃逸分析技术算是在JVM 面试题偶有提及的一个考察点,当然如果你能够讲解JVM工作原理的时候提及这一点,这一定会增加面试官对你的好感,本文主题内容如下:
通过对本篇文章的阅读,你将能够从容的解决以下几个面试题:
- 什么是逃逸分析技术?
- 逃逸分析技术解决什么问题?带来什么好处?
- 如何更好的理解或者运用逃逸分析技术?
什么是逃逸分析
逃逸分析技术是JVM
用于提高性能以及节省内存的手段,在JVM
编译语境下也就是我们常说的JIT阶段,逃逸分析技术通过以下两个条件判断该对象是否是逃逸:
- 该对象是否分配在堆上(
static
关键字或者成员变量)。 - 该对象是否会传给未知代码,比如
return
到外部给别的类使用。
只要编译阶段判定当前对象并没有发生逃逸,那么它就会采用栈上分配 、标量替换 、同步锁消除等手段提升程序执行性能和节省内存开销。
那么我们又该如何判断对象是否逃逸呢?我们不妨基于上述的判断条件来看看这个示例,假设我们现在有一个user类:
arduino
@Data
public class User {
private int id;
private String name;
}
我们通过UserService进行初始化,那么请问这段代码是否发生逃逸呢?
csharp
public class UserService {
private User user;
public void init() {
user = new User();
user.setId(RandomUtil.randomInt(10));
user.setName(RandomUtil.randomString(3));
}
}
答案当然是肯定的,因为这段代码会被外部的其他任意线程操作。
再来看看这段代码,典型的return语句,很明显的外部线程可以直接操作这个对象,所以这个对象也发生了逃逸,所以针对这几种情况JIT都无法对其进行优化。
sql
public User createUser() {
User user = new User();
user.setId(RandomUtil.randomInt(10));
user.setName(RandomUtil.randomString(3));
return user;
}
如何运用到逃逸分析技术
栈上分配
一般来说,JIT即时编译技术中的栈上分配和标量替换基本都是同时出现的,按照上文所述,假如上述代码所返回的user对象仅仅是获取当前用户的年龄,那么我们就可以直接在方法内完成逻辑计算并直接返回,这样对象就没有发生逃逸,如此对象便可直接在栈帧上进行分配,有效减小JVM垃圾回收的压力。
scss
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);
}
分离对象或标量替换
如果仅仅是操作未逃逸对象的某些简单运算,我们同样可以只在栈帧内使用这个对象,如此JVM
就会将这个对象打散,将对象打散为无数个小的局部变量,实现标量替换,如下所示,这段代码没有发生逃逸,则JVM
会避免创建Point
。
arduino
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的值,完成输出打印。
csharp
private static void alloc() {
int x = 1;
int y = 2;
System.out.println("point.x = " + x + "; point.y=" + y);
}
同步锁消除
这一点就比较有趣了,我们都知道使用StringBuffer可以保证线程安全,因为其操作函数都有带synchronized关键字,那么请问这段代码会上锁吗?
java
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
。
小结
合理的在栈帧上解决问题可以避免对象逃逸,从而让JIT 尽可能的去进行优化,这一点我想应该是一个Java程序员对于代码的极致追求了。
我是sharkchili ,CSDN Java 领域博客专家 ,开源项目---JavaGuide contributor ,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili ,同时我的公众号也有我精心整理的并发编程 、JVM 、MySQL数据库个人专栏导航。
参考资料
逃逸分析,yyds!!:mdnice.com/writing/0e0...
本文使用 markdown.com.cn 排版