Battle:你会TLAB,我会逃逸分析
“噔噔噔…”传来一阵敲门声,把我从美梦中惊醒了。
朦胧间听到有人在说话“阿Q,在家不?”
“来了来了”,推门一看,原来是“赵信”兄弟。
赵信:自称常山赵子龙,一把三爪长枪耍的虎虎生风,见人上去就是一枪,人送外号“菊花信”。
TLAB
- 尽管不是所有的对象实例都能够在
TLAB
中成功分配内存(因为它的空间比较小),但JVM
明确是将TLAB
作为内存分配的首选; - 一旦对象在
TLAB
空间分配内存失败时,JVM
就会尝试着通过使用加锁机制确保数据操作的原子性,从而直接在Eden
空间中分配内存。
参数设置
-XX:UseTLAB
:设置是否开启TLAB
空间;-XX:TLABWasteTargetPercent
:设置TLAB
空间所占Eden
空间的百分比大小,默认仅占1%
;
堆是分配对象的唯一选择吗?
- 如果经过逃逸分析(
Escape Analysis
)后发现,一个对象并没有逃逸出方法,那么就可能被优化为栈上分配。这样就无需在堆上分配内存,也无须进行垃圾回收了。这也是最常见的堆外存储技术。 - 基于
OpenJDK
深度定制的TaoBaoVM
,它创新的GCIH(GCinvisible heap)
实现了堆外分配。将生命周期较长的Java
对象从堆中移至堆外,并且GC
不能管理GCIH
内部的Java
对象,以此达到降低GC的回收频率和提升GC
的回收效率的目的。
举例一
public void method(){
User user = new User();
...
user = null;
}
user
对象在方法内部声明,且在内部置为null
,未被方法外的方法所引用,我们就说user
对象没有发生逃逸。
它可以分配到栈上,并随着方法的结束,栈空间也随之移除。
举例二
public static StringBuffer createStringBuffer(String s1,String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
虽然sb
对象在方法内部被定义,但是它又作为方法的返回对象,可被其它方法调用,我们就说sb
对象发生了逃逸。
要想不发生逃逸,可以改造为:
public static String createStringBuffer(String s1,String s2){
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
在JDK 6u23
版本之后,HotSpot
中默认开启了逃逸分析。
-XX:DoEscapeAnalysis
:显式开启逃逸分析-XX:+PrintEscapeAnalysis
:查看逃逸分析的筛选结果
栈上分配
/**
* 栈上分配测试
* -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails
*/
public class StackAllocation {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
alloc();
}
long end = System.currentTimeMillis();
System.out.println("花费的时间为: " + (end - start) + " ms");
//为了方便查看堆内存中对象个数,线程sleep
try {
Thread.sleep(1000000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
private static void alloc() {
//未发生逃逸
User user = new User();
}
static class User {
}
}
逃逸分析默认开启,也可以手动开启:-XX:+DoEscapeAnalysis
关闭逃逸分析
同步省略
我们都知道线程同步的代价是相当高的,同步的后果就是降低了并发性和性能。
JVM
为了提高性能,在动态编译同步块的时候,JIT
编译器可以借助逃逸分析来判断同步块所使用的锁对象是否只能够被一个线程访问。
如果符合条件,那么JIT
编译器在编译这个同步块的时候就会取消对这部分代码的同步。这个取消同步的过程就叫同步省略,也叫锁消除。
举例
public class SynchronizedTest {
public void method() {
Object code = new Object();
synchronized(code) {
System.out.println(code);
}
}
/**
*代码中对code这个对象进行加锁,
*但是code对象的生命周期只在method方法中
*并不会被其他线程所访问控制,
*所以在 JIT 编译阶段就会被优化掉。
*/
//优化为
public void method2() {
Object code = new Object();
System.out.println(code);
}
}
在解释执行时这里仍然会有锁,但是经过服务端编译器的即时编译之后,这段代码就会忽略所有的同步措施而直接执行。
标量替换
- 标量:不可被进一步分解的量,如
JAVA
的基本数据类型就是标量; - 聚合量:可以被进一步分解的量, 在
JAVA
中对象就是可以被进一步分解的聚合量。
聚合量可以分解成其它标量和聚合量。
标量替换,又名分离对象,即在JIT
阶段,如果经过逃逸分析,发现一个对象不会被外界访问的话,那么经过JIT
优化,就会把这个对象拆解成若干个其中包含的成员变量来替代。
举例
public class ScalarTest {
public static void main(String[] args) {
alloc();
}
public static void alloc(){
Point point = new Point(1,2);
}
}
class Point{
private int x;
private int y;
public Point(int x,int y){
this.x = x;
this.y = y;
}
}
//转化之后变为
public static void alloc(){
int x = 1;
int y = 2;
}
//Point这个聚合量经过逃逸分析后,发现他并没有逃逸,就被替换成两个标量了。
标量替换默认开启,你也可以通过参数手动设置
-XX:+EliminateAllocations
,开启之后允许将对象打散分配到栈上,GC
减少,执行速度提升。
常见的发生逃逸的场景
举例
public class EscapeAnalysis {
public EscapeAnalysis obj;
/*
为成员属性赋值,发生逃逸
*/
public void setObj(){
this.obj = new EscapeAnalysis();
}
//思考:如果当前的obj引用声明为static的?仍然会发生逃逸。
/*
方法返回EscapeAnalysis对象,发生逃逸
*/
public EscapeAnalysis getInstance(){
return obj == null? new EscapeAnalysis() : obj;
}
/*
引用成员变量的值,发生逃逸
*/
public void useEscapeAnalysis1(){
EscapeAnalysis e = getInstance();
//getInstance().xxx()同样会发生逃逸
}
/*
对象的作用域仅在当前方法中有效,没有发生逃逸
*/
public void useEscapeAnalysis(){
EscapeAnalysis e = new EscapeAnalysis();
}
}
逃逸分析并不成熟
1999
年就已经发表了关于逃逸分析的论文,但JDK1.6
中才有实现,而且这项技术到如今也不是十分成熟。
其根本原因就是无法保证逃逸分析的性能提升一定能高于它的消耗,因为逃逸分析自身也需要进行一系列复杂的分析,是需要耗时的。
一个极端的例子,就是经过逃逸分析之后,发现所有对象都逃逸了,那这个逃逸分析的过程就白白浪费掉了。
细心的小伙伴也应该能发现,我们在抽样器中的截图其实就是在堆中分配的对象。
以上就是今天的所有内容了,如果你有不同的意见或者更好的idea
,欢迎联系阿Q,添加阿Q可以加入技术交流群参与讨论呦!
- 点赞
- 收藏
- 关注作者
评论(0)