Java-JVM-逃逸分析
摘要
逃逸分析其实并不是新概念,早在1999年就有论文提出了该技术。但在Java中算是新颖而前言的优化技术,从JDK1.6才开始引入该技术。本文会简单说说他是怎么操作的。
0x01 逃逸分析
1.1 辅助优化
首先我们要认识到,逃逸分析并不是直接优化的技术,而是作为其他优化的依据。
1.2 分析对象动态作用域
1.2.1 方法逃逸
- 定义
一个对象在方法中被定义,但却被方法以外的其他代码使用。 - 场景
如传参等可能导致此情况发生。 - 例子:
1.2.2 线程逃逸
- 定义
一个对象由某个线程在方法中被定义,但却被其他线程访问。 - 场景
如类变量、公用的或有get、set方法的实例变量等
1.2.3 逃逸分析过程
- 数据流敏感的若干复杂分析,以确定程序各分支执行对目标对象影响,耗时较长。
1.3 JVM配置
逃逸分析具体配置项如下:
- 开启逃逸分析(JDK8中,逃逸分析默认开启。)
-XX:+DoEscapeAnalysis
- 关闭逃逸分析
-XX:-DoEscapeAnalysis
- 逃逸分析结果展示
-XX:+PrintEscapeAnalysis
0x02 对象优化
如果对象不会发生前述方法逃逸和线程逃逸情况(即完全不可能被别的方法和线程访问到的对象),JVM可做以下优化:
2.1 栈上分配
- 普通对象在堆中分配,各线程共享。但有GC消耗。
- 当确定对象不会发生方法逃逸时,可在线程栈上分配对象。此时对象生命周期和方法相同,随栈帧出栈时即可销毁,不需要GC了。
2.2 同步消除
2.2.1 基本概念
- 线程同步有性能消耗
- 锁消除:当确定对象不会发生线程逃逸时,可消除该对象不必要的同步操作(永不会竞争)。具体来说,JVM在编译器运行时会扫描代码,当检查到那些不可能存在共享区竞争,但却有互斥同步的代码,直接将这样的多此一举的同步消除
- 锁粗化:JVM针对那些反复在一段代码中对同一对象加锁的情况,将同步锁放在最外层包住这里面的多次同步锁,同时取消内部的同步锁
2.2.2 JVM配置
(JDK8中,同步消除默认开启。)
-XX:+EliminateLocks
2.3 标量替换
2.3.1 基本概念
- 标量
标量指无法分解的数据,如java中的基本数据类型及引用类型 - 聚合量
可以分解的,成为聚合量,如对象 - 标量替换
如果一个可拆分对象不会发生逃逸,那在程序执行时并不创建他,而是根据情况在线程栈上只创建用到的成员标量 - 好处
- 原始对象标量替换后,往往可以只创建所需标量,节约了空间和时间。
- 此外,JVM可将这类成员变量放在栈上,乃至移动到高速寄存器中进行读写,大大提升读写效率。
- 还可以进一步优化。
截止JDK8,栈上直接分配对象并未实现,而是将对象标量替换后在栈上分配。
2.3.2 JVM配置
- 开启标量替换(JDK8中,逃逸分析默认开启。)
-XX:+EliminateAllocations
- 查看标量替换详情
-XX:+PrintEliminateAllocations
0x03 实验
3.1 标量替换
Java代码如下
public class EscapeDemo
{
class Person
{
private String name;
private int age;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
public int getAge()
{
return age;
}
public void setAge(int age)
{
this.age = age;
}
}
public void escapeTest(){
for(int i = 0 ; i < 1000000 ; i++){
Person person = new Person();
person.setAge(i);
}
}
public static void main(String[] args)
throws IOException
{
long startTime = System.currentTimeMillis();
new EscapeDemo().escapeTest();
long endTime = System.currentTimeMillis();
System.out.println("elapsed time = "+ (endTime - startTime));
System.in.read();
}
}
可以看到,这是段简单的代码,就是在escapeTest
方法中反复创建Person
对象100万次。
3.1.1 不使用逃逸分析
命令如下:
java -Xmx1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EscapeDemo
结果如下:
elapsed time = 22
$ jmap -histo 18938
num #instances #bytes class name
----------------------------------------------
1: 1000000 24000000 demos.jvm.escape.EscapeDemo$Person
此时耗时较慢,22ms,并实打实的构建了100万个对象。
3.1.2 不使用标量替换
命令如下:
java -Xmx1G -XX:-EliminateAllocations -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EscapeDemo
结果如下:
elapsed time = 23
$ jmap -histo 18935
num #instances #bytes class name
----------------------------------------------
1: 1000000 24000000 demos.jvm.escape.EscapeDemo$Person
此时情况和不开启逃逸分析时情况差不多,耗时较慢,23ms,并实打实的构建了100万个对象。
3.1.3 使用逃逸分析和标量替换
命令如下(JDK8默认开启逃逸分析和标量替换):
java -Xmx1G -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EscapeDemo
结果如下:
elapsed time = 7
$ jmap -histo 1839
num #instances #bytes class name
----------------------------------------------
1: 193418 4642032 demos.jvm.escape.EscapeDemo$Person
使用了逃逸分析和标量替换后,程序运行仅耗时7毫秒,是前面不开启时的三分之一还少,且EscapeDemo
只分配了13万多个对象。
因为此时,JVM逃逸分析EscapeDemo
只会在escapeTest
方法中执行,不会发生方法逃逸和线程逃逸,所以可以对部分聚合量EscapeDemo
进行标量替换,将拆分后的标量在栈上分配,减少直接在堆上分配的对象数量。
3.2 同步消除
3.2.1 锁粗化
- Java代码如下:
/**
* Created by chengc on 2018/12/23.
* 锁粗化
*/
public class EliminateLocks2
{
public StringBuilder concatStr2(String ... strs){
StringBuilder sb = new StringBuilder();
for (String string : strs) {
synchronized (sb) {
sb.append(string + " ");
}
}
return sb;
}
public static void main(String[] args)
throws IOException
{
long startTime = System.currentTimeMillis();
EliminateLocks2 eliminateLocks = new EliminateLocks2();
for(int i = 0 ; i < 1000000 ; i++){
StringBuilder sb = eliminateLocks.concatStr2("A", "B", "C");
String result = sb.toString();
}
long endTime = System.currentTimeMillis();
System.out.println("elapsed time = "+ (endTime - startTime));
System.in.read();
}
}
- 不开启同步消除
$ java -Xmx1G -XX:-EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks2
[GC (Allocation Failure) [PSYoungGen: 65536K->432K(76288K)] 65536K->440K(251392K), 0.0007763 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65968K->448K(76288K)] 65976K->464K(251392K), 0.0010218 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->352K(76288K)] 66000K->376K(251392K), 0.0008053 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
elapsed time = 174
- 开启同步消除
$ java -Xmx1G -XX:+EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks2
[GC (Allocation Failure) [PSYoungGen: 65536K->448K(76288K)] 65536K->456K(251392K), 0.0008636 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->448K(76288K)] 65992K->464K(251392K), 0.0007134 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->432K(76288K)] 66000K->448K(251392K), 0.0007321 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
elapsed time = 146
- 结果分析
- 无锁消除
StringBuilder
类的实例sb
对象在concatStr2
方法中被创建,并在最后被返回到方法外部,且会被调用方赋值给变量result
。所以此时sb
对象会发生方法逃逸,不能进行同步锁消除优化。 - 锁粗化
for循环内部对sb
对象进行synchronized
修饰,反复对sb
对象使用同步锁。此时会进行锁粗化优化,运行时间由174ms缩短为146ms。
3.2.2 锁消除
- Java代码如下:
/**
* Created by chengc on 2018/12/23.
* 锁消除
*/
public class EliminateLocks3
{
public String concatStr3(String ... strs){
StringBuilder sb = new StringBuilder();
synchronized (sb){
for (String string : strs) {
sb.append(string+" ");
}
}
return sb.toString();
}
public static void main(String[] args)
throws IOException
{
long startTime = System.currentTimeMillis();
EliminateLocks3 eliminateLocks = new EliminateLocks3();
for(int i = 0 ; i < 1000000 ; i++){
String result = eliminateLocks.concatStr3("A", "B", "C");
}
long endTime = System.currentTimeMillis();
System.out.println("elapsed time = "+ (endTime - startTime));
System.in.read();
}
}
- 不开启同步消除
$ java -Xmx1G -XX:-EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks3
[GC (Allocation Failure) [PSYoungGen: 65536K->416K(76288K)] 65536K->424K(251392K), 0.0007494 secs] [Times: user=0.01 sys=0.01, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65952K->448K(76288K)] 65960K->464K(251392K), 0.0010933 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->416K(76288K)] 66000K->432K(251392K), 0.0005035 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
elapsed time = 146
- 开启同步消除
$ java -Xmx1G -XX:+EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks3
[GC (Allocation Failure) [PSYoungGen: 65536K->416K(76288K)] 65536K->424K(251392K), 0.0007823 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65952K->448K(76288K)] 65960K->464K(251392K), 0.0009626 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->368K(76288K)] 66000K->384K(251392K), 0.0005893 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
elapsed time = 118
- 结果分析
- 锁消除
StringBuilder
类的实例sb
对象在concatStr3
方法中被创建,但在最后被返回到方法外部时最终返回的是转换后的String
对象而不是sb
本身。所以此时sb
对象不会发生方法逃逸,可以进行同步锁消除优化。 - 无锁粗化
for循环外部对sb
对象进行synchronized
修饰,此时不会进行锁粗化优化,运行时间由146ms缩短为118ms。
3.2.3 锁粗化+锁消除
Java代码如下:
public class EliminateLocks
{
public String concatStr1(String ... strs){
StringBuilder sb = new StringBuilder();
for (String string : strs) {
synchronized (sb) {
sb.append(string + " ");
}
}
return sb.toString();
}
public static void main(String[] args)
throws IOException
{
long startTime = System.currentTimeMillis();
EliminateLocks eliminateLocks = new EliminateLocks();
for(int i = 0 ; i < 1000000 ; i++){
String result = eliminateLocks.concatStr1("A", "B", "C");
}
long endTime = System.currentTimeMillis();
System.out.println("elapsed time = "+ (endTime - startTime));
System.in.read();
}
}
注意,这次concatStr1
方法返回的是toString
,而且synchronized
放在了for循环内部。
- 不开启同步消除
$ java -Xmx1G -XX:-EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks
[GC (Allocation Failure) [PSYoungGen: 65536K->448K(76288K)] 65536K->456K(251392K), 0.0011907 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65984K->432K(76288K)] 65992K->448K(251392K), 0.0009904 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65968K->464K(76288K)] 65984K->480K(251392K), 0.0007127 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
elapsed time = 195
- 开启同步消除
$ java -Xmx1G -XX:+EliminateLocks -XX:+PrintGCDetails -cp /Users/chengc/cc/work/projects/javaDemos/src/main/java/ demos.jvm.escape.EliminateLocks
[GC (Allocation Failure) [PSYoungGen: 65536K->432K(76288K)] 65536K->440K(251392K), 0.0007056 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65968K->416K(76288K)] 65976K->432K(251392K), 0.0007054 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
[GC (Allocation Failure) [PSYoungGen: 65952K->416K(76288K)] 65968K->432K(251392K), 0.0005523 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
elapsed time = 128
- 结果分析
- 锁消除
StringBuilder
类的实例sb
对象在concatStr1
方法中被创建,但在最后被返回到方法外部时最终返回的是转换后的String
对象而不是sb
本身。所以此时sb
对象不会发生方法逃逸,可以进行同步锁消除优化。 - 锁粗化
for循环内部对sb
对象进行synchronized
修饰,反复对sb
对象使用同步锁。此时会进行锁粗化优化,运行时间由195ms缩短为128ms。
0x04 现存问题
现在最大的问题就是无法保证逃逸分析的收益大于进行此操作带来的性能消耗。所以JVM目前采用不准确、时间较短算法进行逃逸分析,以权衡收益和开销。
0xFF 参考文档
《深入理解Java虚拟机》