前言
锁优化措施包括:锁消除和锁粗化。他们都是针对不同情况下的低效锁操作进行的优化,其中锁消除过程和逃逸分析密切相关。
什么是逃逸分析
逃逸分析的作用是分析对象的动态作用域,即通过判断方法中被创建的对象的作用是否被外部方法引用来决定是否将对象分配在堆上。
如下面代码所示:
public class EscapeAnalysis {
public static void noEscape(){
StringBuffer inside = new StringBuffer();
inside.append("没有逃逸");
System.out.println(inside.toString());
}
public static StringBuffer escape(){
StringBuffer outside = new StringBuffer();
outside.append("发生逃逸");
return outside;
}
public static void main(String[] args){
StringBuffer sb = escape();
System.out.println(sb.toString());
}
}
noEscape()方法中的对象inside没有发生逃逸,即inside对象的作用范围就是noEscape()这个方法体,其他线程并不能访问到inside对象,即inside对象是线程私有的。
escape()方法中的对象outside则发生了逃逸,因为inside对象被作为结果返回,即inside对象的作用范围已经超出了escape()方法体。试想一下,如果escape()方法体执行结束后就释放outside对象的内存,则main()方法中的sb对象将指向null。
逃逸分析的作用
锁解除:如果通过逃逸分析发现某个对象只能被一个线程访问,即没有发生逃逸(线程私有),那么就会解除对象上的同步锁。如方法noEscape()中inside对象的append()方法是同步方法,但由于inside对象没有发生逃逸, 所以append()方法的同步机制在JIT编译阶段就会被解除。
-
标量替换:首先要理解标量和聚合量的区别,标量是指无法在分解的数据,比如基本数据类型。聚合量是指还可以进一步分解的数据,比如对象就可以分解为变量和其他的聚合量。
当通过逃逸分析发现某个对象没有发生逃逸时,JIT优化就会把对象拆解成成员变量来代替。这就是标量替换。如下面代码所示:
public void split(){ Polymerization p = new Polymerization(1,2); System.out.println("Polymerization.x = " + p.x + ", Polymerization.y = " + p.y); } class Polymerization{ private int x; private int y; public Point(int x, int y){ this.x = x; this.y = y; } }
对象Polymerization没有发生逃逸,通过标量替换后的结果如下所示:
public void split(){ int x = 1; int y = 2 System.out.println("Polymerization.x = " + x + ", Polymerization.y = " + y); }
-
栈上分配
java中对象都是分配在堆内存中,但如果通过逃逸分析得到某个对象没有发生逃逸,即只有单个线程能够访问,该对象可能会被分配到栈上,这样对象就会随着线程的退出而被回收,同时也可以减小垃圾回收的压力。
锁粗化
锁解除对应着锁粗化,如下面代码所示:
public static StringBuffer escape(){
StringBuffer outside = new StringBuffer();
outside.append("发生逃逸1");
outside.append("发生逃逸2");
outside.append("发生逃逸3");
return outside;
}
StringBuffer是同步类,因此在append()方法添加字符串时需要同步机制,但是每次使用append方法都需要获取锁,退出后释放锁。这样频繁的获取锁和释放锁的操作会引起性能的损失。 如果虚拟机探测到有一串零碎操作都是对同一对象加锁,将会把加锁的范围扩展到整个操作序列的外部,也就是上面示例中将三个append()操作包含到一次加锁操作中。