技术分析

  • 大家都知道Eclipse已经实现了自己的编译器,命名为 Eclipse编译器for Java (ECJ)。

ECJ 是 Eclipse Compiler for Java 的缩写,是 JavaTM 认可的 Java 编译工具(类似 javac)。可以单独下载使用。

  • IDEA所支持的编译器,也有几种:javac(Java原生编译器)、ECJ(支持使用Eclipse编译器)、ACJ编译器(不太清楚),其中默认使用的是Javac,同时也推荐使用Javac。

有兴趣可以看看ECJ编译器的相关使用以及独立使用ECJ

大家的误解

首先,很多小伙伴们都跟我说过Javac和JIT还有AOT它们都是什么有啥区别啊?其实无论是ECJ之类的Java源码编译器运行的时候,也都是就是静态编译(前端编译器),而不是JVM里的JIT(主要面向与优化!),此处之前的文章介绍过JIT和AOT编译器,所以此处不做过多赘述!

主流的使用方式

  • “主流”Java系统的做法——javac + HotSpot VM的组合就是如此。

  • 运行时虚方法內联(virtual method inlining)就是这种例子。这样就可以跨越Class边界做优化,跟C/C++程序的LTO(link-time optimization)一样,不过C/C++程序真在运行时做LTO的很少,这方面反而是Java“更胜一筹”…呃,C/C++写的一个动态链接库通常也有大量代码可以放在一起优化,对LTO的需求本来就远没有Java高。

静态编译阶段

首先要确定概念:“编译期”肯定是指诸如Javac、ECJ之类的Java源码编译器运行的时候,也就是静态编译;而不是JVM里的JIT编译器运行的时候,也就是动态编译!

动态编译器之优化!

之前介绍过了逃逸分析属于动态编译器情况下对代码进行相关的逃逸分析优化技术,主要针对于动态编译时候做的优化,为什么不可以在静态编译器进行优化,这样性能不会很高吗?

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

例如,方法内逃逸分析:TrivialMethodEscape

javac优化能力分析

  • 你肯定会有一个疑问?那为啥没见到啥现成的产品在编译器时做逃逸分析和相关优化,或者为啥javac不做这种优化?

  • 回答:目前Javac几乎啥优化都不做,优化的操作和能力都交接了JVM(动态编译器)实现了,并非是技术原因(技术无法实现?),主要是Sun / Oracle的公司压根就没有考虑在Javac的时候进行代码优化操作。

不过即使是这样,仍然有现成的产品做这种事情啊,只不过是针对Android,大家可以参考DexGuard

  • 但是也还是有一些值得注意的优化尚未得到支持:

    • 例如将一些常量值提取到循环之外!
    • 以及一些相关的逃逸分析技术考虑,具体可以参考其相关官方文档!
  • Java也有静态编译优化技术,例如,[Excelsior JET]http://www.tucows.com/preview/371869/Excelsior-JET-For-Windows)比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文件边界)做优化。

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

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

    • 一个风格良好的面向对象程序通常会有大量很小的方法,方法之间的调用非常多,而且很可能是对虚方法的调用(invokevirtual),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有很多小方法、很多虚方法调用、难以静态分析。

  • 而逃逸分析恰恰需要在比较大块的代码上工作才比较有效:JIT编译器要能够看到更多的代码,以便更准确的判断对象有没有逃逸。

  • 只保守的在小块代码上分析的话,很多时候都只能得到“对象逃逸了”的判断,就没啥效果了。

拿上面的Foo / Bar例子说,Foo.foo()如果能内联Foo.bar()就可以判断new Object()没逃逸,那标量替换、消除对象分配之类的都可以做;反之,局限在Foo.foo()自身内部的话,就只能保守判断new Object()有逃逸,于是啥优化也做不了。

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

  • 运行时各种类都加载进来之后再激进的假设那就是当前已经加载的类就代表了“整个程序”,以“closed world”假设做激进优化,但留下“逃生门在遇到与现有假设冲突的新的类加载时抛弃优化,退回到安全的非优化状态。

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

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

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

反射和运行时字节码增强它们不是主要问题。

反射

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

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

  • 修改类的基类,或修改类实现的接口
  • 添加或删除成员(成员方法或字段都算)
  • 修改现有成员的类型(例如修改成员变量的声明类型,或者修改成员方法的signature之类)

参数无法静态确定的反射调用是没办法靠静态分析得知调用目标的。

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

运行时字节码增强
  • 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看作达成你的字节码生成目的的整体看作一个逻辑上编译系统。

☕【Java技术指南】「编译器专题」深入分析探究“静态编译器”(JAVA\IDEA\ECJ编译器)是否可以实现代码优化?的更多相关文章

  1. ☕【Java技术指南】「编译器专题」重塑认识Java编译器的执行过程(常量优化机制)!

    问题概括 静态常量可以再编译器确定字面量,但常量并不一定在编译期就确定了, 也可以在运行时确定,所以Java针对某些情况制定了常量优化机制. 常量优化机制 给一个变量赋值,如果等于号的右边是常量的表达 ...

  2. ☕【Java技术指南】「Guava Collections」实战使用相关Guava不一般的集合框架

    Google Guava Collections 使用介绍 简介 Google Guava Collections 是一个对 Java Collections Framework 增强和扩展的一个开源 ...

  3. SpringBoot图文教程17—上手就会 RestTemplate 使用指南「Get Post」「设置请求头」

    有天上飞的概念,就要有落地的实现 概念十遍不如代码一遍,朋友,希望你把文中所有的代码案例都敲一遍 先赞后看,养成习惯 SpringBoot 图文教程系列文章目录 SpringBoot图文教程1-Spr ...

  4. Java已五年1—二本物理到前端实习生到Java程序员「回忆贴」

    关键词:郑州 二本 物理专业 先前端实习生 后Java程序员 更多文章收录在码云仓库:https://gitee.com/bingqilinpeishenme/Java-Tutorials 前言 没有 ...

  5. 「题目代码」P1044~P1048(Java)

    P1044 谭浩强C语言(第三版)习题5.8 import java.util.*; import java.io.*; import java.math.BigInteger; public cla ...

  6. 🏆【Java技术专区】「编译器专题」重塑认识Java编译器的执行过程(消除数组边界检查+公共子表达式)!

    前提概要 Java的class字节码并不是机器语言,要想让机器能够执行,还需要把字节码翻译成机器指令.这个过程是Java虚拟机做的,这个过程也叫编译.是更深层次的编译. 在编译原理中,把源代码翻译成机 ...

  7. ☕【Java技术指南】「并发原理专题」AQS的技术体系之CLH、MCS锁的原理及实现

    背景 SMP(Symmetric Multi-Processor) 对称多处理器结构,它是相对非对称多处理技术而言的.应用十分广泛的并行技术. 在这种架构中,一台计算机由多个CPU组成,并共享内存和其 ...

  8. ☕【Java技术指南】「OpenJDK专题」想不想编译属于你自己的JDK呢?(Windows10环境)

    Win10下编译OpenJDK8 编译环境 Windows10专业版64位: 编译前准备 Tip: 以下软件的安装和解压目录尽量不要包含中文或空格,不然可能会出现问题 安装 Visual Studio ...

  9. ☕【Java技术指南】「TestNG专题」单元测试框架之TestNG使用教程指南(上)

    TestNG介绍 TestNG是Java中的一个测试框架, 类似于JUnit 和NUnit, 功能都差不多, 只是功能更加强大,使用也更方便. 详细使用说明请参考官方链接:https://testng ...

随机推荐

  1. Longhorn 云原生容器分布式存储 - Air Gap 安装

    内容来源于官方 Longhorn 1.1.2 英文技术手册. 系列 Longhorn 是什么? Longhorn 云原生容器分布式存储 - 设计架构和概念 Longhorn 云原生容器分布式存储 - ...

  2. docker实现mysql主从复制

    目录 一.概述 二.创建master主库 三.创建Slave实例 四.主从配置 五.参考 一.概述 1.原理 master服务器将数据的改变记录二进制binlog日志,当master上的数据发生改变时 ...

  3. SpringBoot博客开发之异常处理

    异常处理: 背景: 最近在搭建属于自己的个人博客(码农小白的执念),自己搭建后端的时候首先考虑的是异常处理.个人也是一边学习一边做,难免有疏漏的地方,希望朋友们在不对的地方提醒下. 技术栈: spri ...

  4. Linux命令集锦之·字符截取命令

    时间:2018-11-15 记录:byzqy 字符截取命令: cut.printf.awk.sed cut $ cut [选项] 文件名 选项: -f 列号:提取第几列: -d 分隔符:按照指定分隔符 ...

  5. 谈谈redis缓存击穿透和缓存击穿的区别,雪崩效应

    面试经历 在很长的一段时间里,我以为缓存击穿和缓存穿透是一个东西,直到最近去腾讯面试,面试官问我缓存击穿和穿透的区别:我回答它俩是一样的,面试官马上抬起头用他那细长的单眼皮眼睛瞪着我说:"你 ...

  6. 2021 年 9 月 TIOBE 指数 C# 增长突破 1.2%

    TIOBE 编程社区指数是编程语言流行程度的指标.该指数每月更新一次.评级基于全球熟练工程师.课程和第三方供应商的数量.谷歌.必应.雅虎.维基百科.亚马逊.YouTube 和百度等流行搜索引擎用于计算 ...

  7. Pikachu靶场通关之XSS(跨站脚本)

    一.XSS(跨站脚本)概述 Cross-Site Scripting 简称为"CSS",为避免与前端叠成样式表的缩写"CSS"冲突,故又称XSS.一般XSS可以 ...

  8. J2EE分布式微服务云开发架构 Spring Cloud+Mybatis+ElementUI 前后端分离J2EE分布式微服务云开发架构 Spring Cloud+Mybatis+ElementUI 前后端分离

    ​ 鸿鹄云架构[系统管理平台]是一个大型企业.分布式.微服务.云架构的JavaEE体系快速研发平台,基于模块化.微服务化.原子化.热部署的设计思想,使用成熟领先的无商业限制的主流开源技术(Spring ...

  9. 利用协程爬网页,自动切换io 精典案例:

    首先Python提供的协程库gevent好像并不能知道那些程序使用了io  所以要加一个补丁,mondey,以下同步和异步各爬一次的案例 : , from urllib import requesti ...

  10. java.net.NoRouteToHostException: Cannot assign requested address

    今天压力测试时, 刚开始出现了很多异常, 都是 java.net.NoRouteToHostException: Cannot assign requested address.  经网上查资料, 是 ...