写在最前,本篇文章大部分来源于b站尚硅谷JVM全套教程的提炼,并附带自己的理解。主要是为了帮助自己理解,和用于复习。如果同时还能对其他人有所裨益,那就更好不过了。如果有谬误的地方,还请不吝指出。
逃逸分析
逃逸分析是通过分析对象的动态作用域,当一个对象在方法内被定义后,它可能被外部方法引用,比如:作为调用参数传递,称为方法逃逸;如果被外部线程访问,则称为线程逃逸。
不同的逃逸程度,也对应着不同的优化方法。
逃逸分析默认开启,可以通过指令-XX:-DoEscapeAnalysis
关闭(改成+号就为开启)
栈上分配
如果对象不逃逸出线程,就可能在栈上分配。
对象所占内存随栈帧出栈而销毁。即当方法结束,对象就自动销毁了。垃圾回收的压力会大大减小。
同步省略
如果一个对象只能被一个线程访问,那么对这个对象的操作不考虑同步。即可以安全地消除同步措施。
即使显示地注明synchronized关键字,编译器也会将其消除掉。
注意,字节码文件还是会有同步的指令,只是在运行的时候被消除了。
标量替换
有的对象不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存(堆),而是存在CPU寄存器(栈)中。
标量指无法再分解为更小数据的数据,基本数据类型就是标量。其它可以分解的则称为聚合量。
java对象为聚合量,因为其可以分解为其它的聚合量和标量。
如果一个对象能证明不会被方法以外的代码访问,并且这个对象可以被拆分,那么程序执行时,不会创建这个对象,而是用若干个成员变量(分解为原始类型)来代替。这是栈上分配的特殊情况。
指令:XX:-EliminateAllocations
此处感觉尚硅谷讲的还有些模糊,于是翻出了深入理解Java虚拟机,下面展示一下优化过程。
public int test(int x){
int xx = x + 2;
Point p = new Point(xx, 42);
return p.getX();
}
将Point类的构造函数和getX()方法进行内联优化:
public int test(int x){
int xx = x + 2;
Point p = point_memory_alloc();
p.x=xx;
p.y=42;
return p.x;
}
经过逃逸分析,发现test()中的Point对象实例不会逃逸,这样就可以进行标量替换,将内部的x和y置换,分解为局部变量,从而避免Point对象实例被创建:
public int test(int x){
int xx = x + 2;
int px=xx;
int py=42;
return px;
}
通过数据流分析,发现py的值对方法不会造成影响,于是将无效代码消除,得到最终优化结果:
public int test(int x){
return x+2;
}
在实际应用程序中,实施逃逸分析可能出现效果不稳定的情况,或者分析耗时但却无法有效判别而导致性能下降。
直到JDK7才成为服务端编译器默认开启的选项。
注意,HotSpot虚拟机中没有提供栈上分配的优化。也就是说,我们所见到的优化基本都由标量替换所提供。
关于这一点我其实还是有一点疑惑,于是我查找到了一个可以说是比较准确的答案:When can Hotspot allocate objects on the stack?(需要翻墙)
总结一下:
- 栈分配是有条件的:
Hotspot can stack-allocate an object instance:
- if all its uses are inlined
- it is never assigned to any static or object fields, only to local variables
- at each point in the program, which local variables contain references to the object must be JIT-time determinable, and not depend on any unpredictable conditional control flow.
- If the object is an array, its size must be known at JIT time and indexing into it must use JIT-time constants.
- HotSpot实际上并没有使用栈上分配,其实是用标量替换来达到了栈上分配的效果
与书中的结论是一致的。
注意:虽然可以说标量替换是栈上分配的一个特例,但这两者的实际用法还是有区别的。
标量替换仅在不需要创建指向栈上分配对象的指针时才有效。 例如 C++ 或 Go 中的某些形式的栈上分配能够在栈上分配完整的对象,然后将指向它们的引用或指针传递给被调用的函数。
而且标量替换也要比栈上分配的使用条件更为严格。
因此,如果需要将对象引用传递给非内联方法,即使该引用不会逃逸出被调用的方法,Hotspot 也会始终在堆上分配这样的对象。
笔者还不太理解这篇文章,有余力的同学可以前往这篇文章再看看。