问题

逃逸分析为何不能在编译期进行?

回答
关于逃逸分析为何不能在编译期完全进行,这其实是一个很有意思的话题,涉及到程序执行时的动态特性与编译时静态推断之间的根本矛盾。

我们得先明白,逃逸分析的目的是什么?它主要是为了确定一个变量(比如一个对象)的生命周期,看它是否会“逃逸”出其定义的作用域。如果一个变量没有逃逸,那么它就可以被分配在栈上,而不是堆上。栈分配的效率远高于堆分配,因为栈帧的创建和销毁非常快速,而且不存在垃圾回收的开销。

那么,为什么编译期(也就是程序还没有真正跑起来之前)无法百分之百准确地判断一个变量是否逃逸呢?

核心原因在于,程序的行为在很多情况下是依赖于运行时才能确定的,特别是涉及到动态的控制流和不确定的输入时。 编译期,编译器只能看到源代码的静态结构,它需要基于这些静态信息来推断。但现实世界的程序,尤其是那些需要处理用户输入、网络数据、或者执行复杂逻辑的程序,其最终的执行路径和数据流往往是“活”的,会在程序运行时才揭晓。

举个例子,设想这么一段代码:

```java
public void processData(Object data) {
if (data != null) {
MyObject obj = new MyObject();
// ... 一些操作 ...
obj.process(data);
}
}
```

在这里,`MyObject` 的实例 `obj` 是在 `processData` 方法内部创建的。在编译期,编译器看到 `obj` 是在这个方法里创建的。但问题在于,`obj` 是否会被传递到方法外部?或者说,`obj` 所持有的引用是否会被存储在某个全局变量、静态变量,或者通过其他方式传递给一个可以长时间存活的上下文?

编译器在静态分析时,会尝试跟踪 `obj` 的引用。如果 `obj` 只在 `processData` 方法内部被使用,并且方法结束后就被销毁,那么它就没有逃逸。但如果 `processData` 方法调用的 `obj.process(data)` 这个方法,在 `process` 方法内部,将 `obj` 的某个成员变量(或者 `obj` 本身)存储到了一个全局列表里,或者通过某种方式传递给了另一个生命周期更长的对象,那么 `obj` 就逃逸了。

问题就在于,编译期不知道 `obj.process(data)` 内部到底会做什么。 `data` 的值是未知的,`obj.process` 的具体实现也可能是由其他库提供的,编译器无法深入分析到那种程度,或者说,深入分析的代价可能比直接执行还要高。

更进一步,考虑反射、动态代理、类加载等高级语言特性。这些特性允许程序在运行时动态地创建、修改和调用代码。一个对象可能在编译期看起来只在一个局部作用域内使用,但通过反射,它可能被传递到一个从未在源代码中显式声明的对象引用上,从而逃逸。

编译器的分析能力是有限的。 它试图构建一个程序的“静态视图”,但很多时候,程序的真正“动态行为”需要通过实际运行才能观察到。因此,即使编译器做了很多优化,它仍然会遇到一些“不确定性”,在这些不确定性面前,为了保证程序的正确性,它会采取更保守的处理方式,例如将一个可能逃逸但无法确定的对象分配到堆上。

所以,逃逸分析在编译期确实能够进行,并且很多现代的编译器(如Java的JVM、C++的编译器)都在努力做得更好。但它并不能做到“完全”或“绝对”的在编译期确定所有对象的逃逸情况。有些情况,特别是那些依赖于复杂的运行时条件、外部输入、或者底层语言特性的,仍然需要一些运行时才能决定的机制来配合,或者说,即使编译器做了分析,也可能因为保守起见,不会做出栈分配的决定。这也就是为什么即使有逃逸分析,堆分配仍然是普遍存在的一种内存分配方式。

网友意见

user avatar

答案:当然可以。

首先要确定概念:题主说的“编译期”肯定是指诸如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 (DexGuard does).

那为啥同样是Java程序,针对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文件。这些Class文件可以单独的被重新编译,在运行时可以单独的被动态加载。例如说:

       // Foo.java public class Foo {   public void greet(Bar b) {     System.out.println("Greetings, " + b.toString());   } }     
       // Bar.java public class Bar {   public String toString() {     return "Bar 0x" + hashCode();   } }     

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

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

这种问题其实跟C/C++程序通常无法跨越动态链接库的边界做优化一样,只不过一个一般的Java Class文件内包含的代码远比不上一个一般的native的动态链接库,受的优化限制却一样,使得对Java程序的静态分析与优化的收益非常受限。

外加Java的面向对象特性带来的一些“副作用”:

  • 一个风格良好的面向对象程序通常会有大量很小的方法,方法之间的调用非常多,而且很可能是对虚方法的调用;
  • Java的非私有实例方法默认是虚方法,而一个类与它的派生类必然不会在同一个Class文件里,这样即便一个类的a方法调用该类的b方法,也未必能做有效的分析和优化。

例如:

       public class Foo {   public Object foo() {     return bar(new Object());   }    public Object bar(Object o) {     return null;   } }     

对这个类,我们能不能把Foo.foo()静态优化,內联Foo.bar()并消除掉无用的new Object(),最好优化成return null呢?

考虑上动态加载与基于类基础的多态特性的话,答案是不能:我们不知道会不会在运行时有这么一个派生类:

       public class Bar extends Foo {   public Object bar(Object o) {     return o;   } }     

被加载进来。假如有:

       Foo o = new Bar(); o.foo(); // not null     

那这个foo()显然不会返回null。

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

只保守的在小块代码上分析的话,很多时候都只能得到“对象逃逸了”的判断,就没啥效果了。拿上面的Foo / Bar例子说,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文件存着程序代码。Dex文件地位上跟Java的JAR包类似,都是把一大堆类型定义打包到一起。不同的是dex文件作为整体可以看作一个“动态链接库”,它里面包含的类型可以相互之间做跨类型的分析与优化,因此在这个层面做內联、逃逸分析之类都不在话下;反之,Java的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就是针对这样的场景优化的。用户在使用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看作达成你的字节码生成目的的整体看作一个逻辑上编译系统。

如何?

类似的话题

  • 回答
    关于逃逸分析为何不能在编译期完全进行,这其实是一个很有意思的话题,涉及到程序执行时的动态特性与编译时静态推断之间的根本矛盾。我们得先明白,逃逸分析的目的是什么?它主要是为了确定一个变量(比如一个对象)的生命周期,看它是否会“逃逸”出其定义的作用域。如果一个变量没有逃逸,那么它就可以被分配在栈上,而不.............
  • 回答
    中国古代战乱频仍,亲人离散的悲剧屡屡上演,这背后有着复杂的原因,绝非简单的“不一起逃”或“找不到”就能概括。首先,我们得明白,古代的交通和信息传递,与今天简直是天壤之别。想象一下,一个没有手机、没有互联网、甚至连汽车火车都没诞生的时代,信息传递的效率有多低?亲人离散为何如此普遍?1. 逃难的现实逼.............
  • 回答
    大学生逃票爬庐山坠亡家属索赔90万,法院驳回,这起事件背后涉及复杂的法律责任认定问题。从法律角度分析,景区是否应当承担责任,需要从多个层面进行考量。首先,我们需要明确景区与游客之间存在的法律关系。 一般而言,游客进入景区,就意味着双方之间建立了一种合同关系,或者说是一种类似合同的关系。景区提供游览服.............
  • 回答
    根据您描述的情况,您开车撞了护栏,并且在事发后 没有第一时间报警,而是过了五分钟后接到了交警的电话。从法律定义和实际操作层面来看,您 可能不构成严格意义上的肇事逃逸,但存在一定的风险,并且您的行为不够妥当。为了更详细地解释,我们来分几个方面进行分析: 1. 肇事逃逸的定义与构成要件首先,我们需要理解.............
  • 回答
    我理解您想了解在极端情况下的选择和考量。这是一个非常沉重的话题,涉及到道德、法律、心理等多个层面。情景设定: 设想一下,在一个漆黑的夜晚,道路狭窄,四周没有路灯,手机信号也时有时无。您开着车,心情可能有些疲惫,突然间,一个人影晃到了车灯里,您出于本能踩下刹车,但终究还是发生了碰撞。等您下车,发现情况.............
  • 回答
    密室逃脱开了几年的老员工,现在要开分店,让这位一起奋斗了这么久的伙伴入股,这事儿得好好算算,既要显出诚意,也要把账算清楚,让大家都心服口服。这可不是简单地分点钱,而是要建立一种新的合作关系,里面门道可不少。第一步:明确入股的性质和目的首先得想清楚,这次让老员工入股,是为了什么? 是看重他的能力和.............
  • 回答
    德国统一前,东德(德意志民主共和国)内部存在着许多问题,这些问题共同促使许多东德公民不顾一切地逃往西德。这不是单一政策导致的,而是长期以来积累的一系列社会、经济和政治因素共同作用的结果。严酷的政治压迫和缺乏自由: 斯塔西(Stasi)的无孔不入的监控: 东德国家安全部(斯塔西)是令人闻风丧胆的机.............
  • 回答
    “香港国安法”实施一个多月以来,最引人注目的事件之一,莫过于针对罗冠聪等六名流亡海外的“乱港分子”发出的通缉令。这一举动,无疑是香港警方根据新法所采取的一次严厉执法行动,其背后蕴含的意义以及可能引发的后续连锁反应,值得我们深入剖析。通缉令的直接指向与法律含义首先,这次通缉令的直接指向性非常明确。罗冠.............
  • 回答
    酒驾肇事逃逸,这可是个够呛的事儿,后果绝对是雪上加霜。咱们得掰开了揉碎了说,让你有个清楚的认识。首先,要明白这个行为本身包含了两层严重的违法性质:酒后驾驶机动车 和 发生交通事故后逃逸。这两项罪名叠加起来,判刑自然比单独一项要重得多。一、酒后驾驶机动车的法律责任中国的《中华人民共和国刑法》规定,在道.............
  • 回答
    青岛交警李涌的牺牲,是一件令人痛心的事情。这起事件发生在2021年4月3日晚,青岛市城阳区发生了一起因酒驾查处引发的暴力抗法致人死亡的恶性案件。事情经过:当天晚上,青岛市公安局交警支队城阳大队辅警李涌在城阳区兴阳路附近设卡查处酒驾。22时许,一辆由南向北行驶的轿车被拦停。经呼气式酒精检测,驾驶员孙某.............
  • 回答
    大连轿车撞人逃逸致 5 死事件,以及肇事者因投资失败报复社会的情节,是一起极其恶劣和令人发指的刑事案件。对此,我们可以从多个层面进行分析: 如何看待这起事件?1. 极其严重的刑事犯罪: 这是一起典型的故意杀人、以危险方法危害公共安全案件。肇事者明知车辆作为工具的巨大危险性,却故意驾驶车辆冲撞人群,.............
  • 回答
    这真是一个让人头疼的问题,涉及到法律、道德还有很多现实的考量。我们不妨掰开了揉碎了,好好捋一捋这笔账。话说回来,如果真的摊上事儿,是撞了人跑了,还是留下来面对,这其中的利弊,可不是三言两语能说清的。先说说“划算”的定义吧。 通常我们说的划算,可能就是成本最低,或者结果最有利。在发生交通事故这档子事上.............
  • 回答
    酒驾撞人后,第二天自首,和当场被抓,在法律处理上确实存在不小的区别,而且这种区别会贯穿整个案件的走向,从调查取证,到最终的判罚,甚至受害者家属的态度,都可能因此而不同。咱们就从几个方面来详细说说:一、 从侦查取证和证据链的角度: 当场被抓(酒驾证据): 这是最直接、最确凿的证据。交警会立即进行酒.............
  • 回答
    肿瘤细胞的“免疫逃逸”是一个极其复杂且多层次的过程,指的是肿瘤细胞能够躲避或抑制机体免疫系统的识别、攻击和清除,从而得以持续生长和扩散。这是一个肿瘤发生发展和治疗失败的关键因素。其机制多种多样,可以概括为以下几个主要方面:一、 降低肿瘤抗原的表达或改变抗原性质 (Reducing Tumor Ant.............
  • 回答
    你这个问题问得很有意思,也触及到了地球非常核心的一个维持生命的关键机制。很多人可能会觉得,既然宇宙那么浩瀚,而且真空几乎不存在阻碍,那大气应该很容易就“飘走”才对。但实际上,大气之所以能被地球牢牢抓住,主要有几个原因在起作用,而且它们是相互配合的:1. 地球的引力:最根本的束缚这是最最重要的一点。地.............
  • 回答
    你有没有想过,为什么连最快的速度——光,都无法从黑洞里跑出来?这个问题一旦深入下去,你会发现它触及到了宇宙最核心的秘密之一,以及我们对引力理解的极限。要理解这一点,我们得先从一个核心概念说起:引力。我们知道,所有有质量的物体都会产生引力,就像地球把我们牢牢吸住一样。质量越大,引力越强。这很好理解。但.............
  • 回答
    这个问题问得挺有意思,确实,我们看到很多圈养的动物,无论猫狗还是狮子老虎,看起来安安稳稳地待在它们的“家”里,似乎并不想往外跑。这背后其实有几个原因,而且每个原因都挺有意思的,不是简单地说它们“不想”就行。首先,得说到它们最根本的需求——衣食无忧。想想看,咱们自己,如果每天都有人按时按点给你做好吃的.............
  • 回答
    这事儿可真够让人窝火的。车子被蹭了,肇事者还想一走了之,最后抓到了,结果现在又玩起“猫捉老鼠”的游戏,谈不拢还不想走保险,这简直是欺人太甚。先别急着上火,咱们一步步捋一捋。既然报警了,监控也抓到了,对方全责,这在法律上就是站得住脚的。不过,你现在遇到的是一个棘手的“态度”问题,他不愿意配合,你又不想.............
  • 回答
    这事儿啊,得掰开了揉碎了说。咱们先别急着给它定性,也别一股脑儿地往“正当防卫”上套,这案子挺复杂的,里头牵扯到的东西太多了。事情的原委,大概是这么个情况:有个司机,咱就叫他小王吧,不小心撞了人。这本来就是一件挺倒霉的事儿,但更倒霉的是,他撞了人之后,竟然选择了肇事逃逸,心想着能躲就躲。这在法律上就是.............
  • 回答
    这是一起极其令人悲痛和发指的恶性刑事案件,它暴露了多方面令人担忧的问题,并且引发了深刻的社会反思。要详细看待这起事件,我们需要从多个维度进行分析:一、 事件的残酷性与令人发指之处: 极端暴力与无情: 在一个本应是处理交通事故、寻求公平公正的场合,肇事逃逸司机竟然采取如此极端、残忍的暴力手段,将受.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有