(8)逃逸可以在编译期进行(硬核,未完待续)

JVM会在运行期对变量进行逃逸分析,实施优化,会有时间消耗却未必性能提升,为何不在编译期进行逃逸分析?判断变量有没有被返回或者作为参数传到被调方法,栈上分配或者标量替换都会很轻松的。求教。

“编译期”诸如javac、ECJ之类的Java源码编译器运行的时候(静态编译);不是JVM里的JIT编译器运行时(动态编译)

一、现成工具在静态编译器做逃逸分析:

WALA为例,有两块简单的逃逸分析实现:

TrivialMethodEscape- 方法内逃逸分析

SimpleThreadEscapeAnalysis- 线程内逃逸分析

Soot为例,

Thread escape analysis for Java programs based on Soot

Field Flow Sensitive Pointer and Escape Analysis for Java Using Heap Array SSA

A Two-Phase Escape Analysis for Parallel Java Programs

二、为没现成的产品在编译器时做逃逸分析和相关优化,或者为啥javac不做这种优化?

javac本来就几乎啥优化都不做,优化都扔给JVM了,不是啥技术原因,Sun / Oracle的公司策略不在这方面投资源而已。

有现成的产品做这种事情啊,只不过是针对Android的DexGuard。在ProGuard的介绍页面上写到:

Some notable optimizations that aren't supported yet:

Moving constant expressions out of loops.

Optimizations that require escape analysis (DexGuarddoes).

三、Android有优化器做逃逸分析,而针对普通Java就没呢?

Java也有啊。Excelsior JET比HotSpot VM早得多就实现了逃逸分析及相关优化,而且是静态编译时做的而不是运行时(JIT)做。Excelsior JET是一个AOT(Ahead-of-Time)编译器和运行时系统。

四、这么好的思路为啥没有更多产品用呢?技术难点在哪?

难点:Java分离编译(separate compilation)和动态类加载(dynamic class loading)/动态链接(dynamic linking)。不知道运行时会加载并链接上什么代码;具体原因不是前面回答所说的“反射”“运行时字节码增强(runtime bytecode instrumentation)”。

Java的把每个引用类型编译为Class文件(可被重新编译),运行时可被动态加载:

可单独编译,也可单独重编译,生成出Foo.class与Bar.class两个Class文件。运行时可以单独被JVM加载,每个ClassLoader实例都可以加载一次同一个Class文件可能会在同一个JVM实例里被加载多次并被看作不同的Class

静态编译Foo.java时,无法假设运行时的Bar实现跟现在Bar.java一样不能跨类型边界(编译后变成Class文件边界)做优化。同一Class文件内静态确定还是可以优化的。

五、Java面向对象特性“副作用”:

小方法(大量)之间调用多(很可能是对虚方法的调用)

虚方法:Java非私有实例方法,与它的子类不会在同一个Class文件里,这样即便类的a方法调用该类b方法未必有效分析和优化

可不可以Foo.foo()静态优化內联Foo.bar()并消除掉无用的new Object(),优化成return null?

不可以!原因:考虑上动态加载基于类基础的多态特性的话,不知道会不会在运行时有这么一个子类

被加载进来。假如有:这个foo()显然不会返回null。

Java有很多小方法、很多虚方法调用、难以静态分析。逃逸分析需要在比较大块的代码上工作才比较有效:编译器要能够看到更多的代码,以便更准确的判断对象有没有逃逸。

小块代码上分析,很多时候都只能得到“对象逃逸了”的判断,没效果。Foo.foo()如果能内联Foo.bar()就可以判断new Object()没逃逸,那标量替换、消除对象分配之类的都可以做;反之,局限在Foo.foo()自身内部的话,就只能保守判断new Object()有逃逸,于是啥优化也做不了。

这些特性使得对Java程序做高质量的静态分析变得异常困难

要么可以选择做缩头乌龟,不做什么静态分析,等运行时各种类都加载进来之后再激进的假设那就是当前已经加载的类就代表了“整个程序”,以“closed world”假设做激进优化,但留下“逃生门在遇到与现有假设冲突的新的类加载时抛弃优化,退回到安全的非优化状态。这是“主流”Java系统的做法——javac + HotSpot VM的组合就是如此。运行时虚方法內联(virtual method inlining)就是这种例子。这样就可以跨越Class边界做优化,跟C/C++程序的LTO(link-time optimization)一样,不过C/C++程序真在运行时做LTO的很少,这方面反而是Java“更胜一筹”…呃,C/C++写的一个动态链接库通常也有大量代码可以放在一起优化,对LTO的需求本来就远没有Java高。

要么可以抛弃Java的分离编译+动态加载特性,简化原始问题 ,这样就什么静态分析和优化都能做了。上面提到的DexGuard、Excelsior JET都走这个路线。

Java的分离编译+动态加载是个让JVM实现者非常非常蛋疼的特性。我天天工作都忍不住得骂它一通。

六、但是抛弃了它,那还能是Java么?

Android表示:我们从来就不说自己是全套Java,只是用了“Java编程语言”。

Java程序在Android上的典型部署方式是整个应用打包成一个apk文件,多个dex文件(跟Java的JAR包类似)存代码。不同的是dex文件作为整体可以看作“动态链接库”,可做跨类型分析、优化,做內联、逃逸分析之类都ok;JAR只是一堆Class文件zip起来,不能动态加载到什么做任何强假设,跟分散的Class文件没区别,优化边界还是在Class文件上

只用一个dex文件部署模型下,Android应用可以在静态编译时做全程序(whole program)分析与优化,这是Java比不上的。“可以做”跟“已经做了”又是两码事。反正Dalvik VM原本带的dexopt做的优化就不怎么样;新的ART还在发展中,做的编译优化也还不多;至于新的Jack和Jill编译器/优化器,这个或许可以期待一下。

七、那像Excelsior JET那样标榜自己实现了标准Java,但又做很多静态编译优化,这又是怎么回事?

其实Java标准只是说要整个系统看起来维持动态类加载的表象,并没有说所有程序都一定要用动态类加载。假如有一个Java应用,它不关心通过动态链接带来的灵活性,而是在开发时就可以保证所有用到的类全都能静态准备好,而且不在运行时“灵活”的实用ClassLoader,那它完全可以找一个能对这种场景优化的Java系统来执行它。

Excelsior JET(编译技术增强的Java虚拟机)就是针对这样的场景优化的。用户在使用JET把Java程序编译成native code时,可以指定编译模式是“我声明我的应用肯定不会用某些动态特性”,JET就会相应的尝试激进的做静态全局编译优化。

八、那要用到动态类加载的Java程序怎么办?

Excelsior JET的运行时系统里其实也包含了一个JIT编译器,所以真的有动态类加载也的话也不惧,兵来将挡而已。激进的静态优化可以依赖运行时可以回退到重新JIT编译来保证安全性。

跟Excelsior JET类似的系统还有一些,最出名的可能是GCJ,不过我觉得它没Excelsior做得完善。根据GCJ的todo列表,很明显它还没实现逃逸分析和相关优化。

国内的话,复旦大学有过一个基于Open64的Java静态编译器项目,叫做Opencj。请参考论文:Opencj: A research Java static compiler based on Open64

根据论文的描述它也有做逃逸分析,但只关注了线程级逃逸来做同步削除的优化,而没有关注方法级逃逸来做标量替换。

没接触过Opencj的具体代码不太肯定它的具体实现现在是啥状况。

=================================================================

前面提到反射和运行时字节码增强,这里简单说说为啥它们不是主要问题。

反射:Java中,反射只能用来查看类的结构信息,而不能改变类的结构信息;反射可以读写实例的状态,但无法改变实例的类型。

九、怎样算是可以修改类的结构信息?

修改类的基类,或修改类实现的接口

添加或删除成员(成员方法或字段都算)

修改现有成员的类型(例如修改成员变量的声明类型,或者修改成员方法的signature之类)

这些Java的反射都不能做。有能通过反射做这些事情的语言,但Java不能。

诚然,参数无法静态确定的反射调用是没办法靠静态分析得知调用目标的。但这对静态分析的干扰程度其实跟普通的虚方法也差不了多少,反正都是目标无法确定,只能做保守分析;加入启发算法来猜测的话,普通虚方法比反射可能好猜一些,但也仅限于猜。

运行时字节码增强:在Java程序运行的过程中修改程序逻辑的能力。从Java提供这一功能的方法就可以一窥其目的:这个能力主要不是给普通Java程序使用,而是给profiler / debugger用的。要使用Java运行时字节码增强,要么得用Java agent来使用java.lang.instrument包里的功能,要么得用JVMTI接口写C/C++代码实现个JVM agent;普通的、不使用agent的Java程序是用不了这种功能的。讨论Java程序是否能在某场景下优化的话题,一般没必要考虑对运行时字节码增强的支持。

即便要支持,主流JVM通过JIT编译器可以重复多次优化编译代码,优化的代码可以被抛弃退回到非优化形式执行,从而既可以激进的做优化、又可以安全的支持这些动态功能;像Excelsior JET这种主要以AOT方式编译Java代码的,为了能提供完善的Java支持还是可选在运行时带有JIT编译器。

评论中有同学提到Javassist,这就是典型的运行时Java字节码增强的应用。运行时用ASM库也是如此。

与之相对,字节码增强也可以在运行之前做,通常叫做“weaving”。所有在运行之前对字节码做的修改都应该看作笼统的“编译时”的一部分——如果用javac编译也是你指定的,接着用啥post weaving也是你指定的,那你不能怪javac不知道后面还会有程序修改字节码,而应该把javac和post weaver看作达成你的字节码生成目的的整体看作一个逻辑上编译系统。

逃逸分析与栈上分配

逃逸分析的基本行为就是分析对象动态作用域:当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。如果想要StringBuffer sb不逃出方法,可以写成:return sb.toString();

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

推荐阅读更多精彩内容

  • 前言 记录个人在2017年08月的学习和总结,不定期更新 2017-08-02 有序的Map HashMap是无序...
    Kevin_ZGJ阅读 406评论 0 0
  • 部分的商用虚拟机中,Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会...
    胡二囧阅读 824评论 0 1
  • 《深入理解Java虚拟机》笔记_第一遍 先取看完这本书(JVM)后必须掌握的部分。 第一部分 走近 Java 从传...
    xiaogmail阅读 5,093评论 1 34
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,103评论 1 32
  • 注:此文是我在读完周志明老师的深入理解Java虚拟机之后总结的一篇文章,请阅读此书获取更加详细的信息. 在这篇文章...
    AlstonWilliams阅读 995评论 0 1