编译器优化技术 — 逃逸分析

一、基本原理与名词

逃逸分析是目前较前沿的优化技术,它不会进行代码的直接优化,而是为其他优化技术提供分析的技术。

原理

通过其对象动态作用域进行分析,从而得到逃逸程度。

方法逃逸

当一个对象在方法里面被定义后,它可能被外部方法所引用,作为调用参数传递到其他方法中。

线程逃逸

赋值到可以在其他线程中访问到的实例变量。

逃逸程度

逃逸程度从低到高分为三个级别:

  • 不逃逸:其他方法或线程都无法通过任何途径访问到这个对象。
  • 逃逸程度低:即方法逃逸,线程以内的逃逸。
  • 逃逸程度高:即线程逃逸,可逃逸至线程外。

二、逃逸优化

1. 栈上分配

当确定一个对象不会逃逸出线程之外,直接让对象在栈上进行内存分配 即可,对象占用的内存会随栈帧出栈而销毁。

在实际应用开发中,不逃逸和逃逸程度低的对象所占比例是很大的,大量对象随着方法结束会自动销毁,垃圾收集的压力会大大减小。但此方式 不支持线程逃逸

2. 标量替换

标量
一个数据已无法再分解成更小的数据来表示,那么就可以称它为标量。
例如:Java 虚拟机的原始数据类型(intlong 等数值类型及 reference 类型等),都无法进一步进行分解。

聚合量
一个数据可以继续分解,则为聚合量。
例如:Java 中的对象就是典型的聚合量。

标量替换
如果把一个对象(聚合量)拆散,根据程序访问情况,将用到的 成员变量恢复为原始类型(标量)来进行访问,此过程即为标量替换。

当确定一个对象不会被方法外部访问,并且这个对象可以被拆散,则程序执行时可能不会创建这个对象,由此带来两点好处:

  • 对象可直接分配和读写在栈上,而栈上的数据很大机会被分配至物理机器的高速寄存器中存储。
  • 为后续进一步优化创造条件。

但此方式要求更高,不允许对象逃逸出方法范围内

3. 同步消除

线程同步本身是一个相对耗时的过程,当确定一个变量不会逃逸出线程时(其他线程无法访问),此变量读写肯定不会有竞争,对这个变量进行的 同步措施可以安全消除

4. 工作过程示例

初始代码

class Point {
    private int x;
    private int y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

public int test(int x) {
    int xx = x + 2;
    Point p = new Point(xx, 42);
}

内联化
Point 的构造函数和 getX() 方法进行内联优化(参考内联相关博客)。

public int test(int x) {
    int xx = x + 2;
    Point p = point_memory_alloc();    // 在堆上分配 p 对象的表示(非真实代码)
    p.x = xx;    // ```Point``` 的构造函数内联后的表示(非真实代码)
    p.y = 42;
    return p.x;    // ```Point::getX()``` 被内联后的表示(非真实代码)
}

逃逸分析(标量替换)
整个 test() 方法的范围内 Point 对象实例不会发生任何程度的逃逸,故可进行标量替换优化。
把内部 x 和 y 直接置换出来,分解为 test() 方法内的局部变量,从而不用直接实例化 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;
}

三、实验(栈上分配验证)

测试代码
public static void main(String[] args) {
    long start = System.currentTimeMillis();
    for (int i = 0; i < 1000000; i++) {
        alloc();
    }
    // 查看执行时间
    long end = System.currentTimeMillis();
    System.out.println("cost " + (end - start) + " ms");
    // 为了方便查看堆内存中对象个数,线程 sleep
    try {
        Thread.sleep(600000);
    } catch (InterruptedException e1) {
        e1.printStackTrace();
    }
}

private static void alloc() {
    User user = new User();
}

static class User {

}
分析

代码内容很简单,使用 for 循环,创建 100 万个 User 对象。

其中,alloc 方法中定义了 User 对象,但是并没有在方法外部引用。故这个对象并不会逃逸到 alloc 外部。经过 JIT 的逃逸分析之后,就可以对其内存分配进行优化。

参数设定
  • 第一组
    指定以下 JVM 参数:
    -Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
    其中 -XX:-DoEscapeAnalysis 表示 关闭 逃逸分析。
  • 第二组
    指定以下 JVM 参数:
    -Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
    其中 -XX:+DoEscapeAnalysis 表示 开启 逃逸分析。
运行结果

分别使用两组参数运行代码。
在程序打印出 cost XX ms 后,代码运行结束之前,我们使用 jmap 命令,来查看下当前堆内存中有多少个 User 对象。

> jmap -histo 2809       // 其中 2809 为当前 JVM 进程 ID
  • 第一组
 num     #instances         #bytes  class name
----------------------------------------------
   1:           524       87282184  [I
   2:       1000000       16000000  StackAllocTest$User
   3:          6806        2093136  [B
   4:          8006        1320872  [C
   5:          4188         100512  java.lang.String
   6:           581          66304  java.lang.Class
  • 第二组
 num     #instances         #bytes  class name
----------------------------------------------
   1:           524      101944280  [I
   2:          6806        2093136  [B
   3:         83619        1337904  StackAllocTest$User
   4:          8006        1320872  [C
   5:          4188         100512  java.lang.String
   6:           581          66304  java.lang.Class
分析

从上面的 jmap 执行结果中我们可以看到。

  • 第一组
    堆中共创建了 100 万个 StackAllocTest$User 实例。
  • 第二组
    堆中共创建了 8.3 万多个 StackAllocTest$User 实例。
结论

在关闭逃避分析的情况下,虽然在 alloc 方法中创建的 User 对象并没有逃逸到方法外部,但是还是被分配在堆内存中。
故没有 JIT 编译器优化,没有逃逸分析技术,所有对象都分配到堆内存中。

在打开逃避分析的情况下,在堆内存中只有 8 万多个 StackAllocTest$User 对象。也就是说在经过 JIT 优化之后,堆内存中分配的对象数量,从 100 万降到了 8.3 万。

除以上通过 jmap 验证对象个数的方法以外,还可以尝试将堆内存调小,然后执行以上代码,根据GC的次数来分析。
也能发现,开启了逃逸分析之后,在运行期间,GC 次数会明显减少。因为很多堆上分配对象内存被优化至栈上分配,随着方法结束而自动销毁,导致 GC 次数减少。

四、总结

发展与现状

关于逃逸分析的论文在 1999 年就已经发表了,但直到 JDK 1.6,HotSpot 才初步支持逃逸分析实现,而且这项技术到如今也并不是十分成熟的,仍有很大的改进余地。

根本原因

无法保证逃逸分析的性能消耗一定能高于他的消耗。虽然经过逃逸分析可以做标量替换、栈上分配和同步消除。但是逃逸分析自身也是需要进行一系列复杂的分析的,这其实也是一个相对耗时的过程。

举一个极端的例子,经过逃逸分析之后,发现没有一个对象是不逃逸的,那这个逃逸分析的过程就白白浪费掉了。故目前虚拟机只能采用不那么准确,但时间压力相对小的算法。

虽然这项技术并不十分成熟,但是他也是即时编译器优化技术中一个十分重要的手段。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,142评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,298评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,068评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,081评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,099评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,071评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,990评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,832评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,274评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,488评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,649评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,378评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,979评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,625评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,643评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,545评论 2 352

推荐阅读更多精彩内容