概述
前端编译器与后端编译器
根据完成任务不同,可以将编译器的组成部分划分为前端(Front End)与后端(Back End)。
前端主要指与源语言有关但与目标机无关的部分,包括词法分析、语法分析、语义分析与中间表示生成。
后端主要指与目标机有关的部分,包括代码优化和目标代码生成等。
Java编译器
Java体系中的三种编译方式:
- 前端编译器
主要是把.java文件转变成.class文件。主要种类有:Sun的Javac、Eclipse JDT的增量式编译器(ECJ)。 - JIT编译器
虚拟机的后端运行期编译器(JIT编译器:Just In Time Compiler),把「字节码」变成「机器码」,主要有:Hotspot VM的C1、C2编译器。 - AOT编译器(Ahead Of Time Compiler)
静态提前编译,在编译期直接把*.java变成本地机器码,这样代码的执行效率更高。
前端编译器工作流程
概述
图片出自:Java虚拟机 :Java字节码的编译生成和运行优化

词法分析、语法分析
这个过程以源代码为输入,输出一个抽象语法树。最终生成的语法树是以所有token为节点的树结构,对语法树遍历即可得到输入源代码的所有信息。
填充符号表
将解析的到的相关信息存储到符号表中。更细节的资料可以查看:
注解处理
在 JDK 1.5 之后,Java 语言提供了对注解(Annotation)的支持,这些注解与普通 Java 代码一样,是在运行期间发挥作用的。在 JDK 1.6 中实现了 JSR-269 规范(JSR-269:Pluggable Annotations Processing API(插入式注解处理 API)),提供了一组插入式注解处理器的标准 API 在编译期间对注解进行处理,我们可以把它看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,编译器将回到解析及填充符号表的过程重新处理,知道所有插入式注解处理器都没有再对语法树进行修改为止。
语义分析
语法分析之后,编译器获得了程序代码的抽象语法树表示,语法树能表示一个结构正确的源程序的抽象,但无法保证源程序是符合逻辑的。而语义分析的主要任务是对结构上正确的源程序进行上下文有关性质的审查,如进行类型审查。
Javac 的编译过程中,语义分析过程分为标注检查以及数据及控制流分析两个步骤。
1、标注检查
标注检查步骤检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配等。在标注检查时还会进行常量折叠,如 int a = 1 + 2; 会被折叠为 int a = 3;
2、数据及控制流分析
数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检测出诸如程序局部变量是在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。
3、解语法糖
即去除代码中的语法糖,还原代码。
字节码生成
字节码生成是 Javac 编译过程的最后一个阶段,不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码写到磁盘中,编译器还进行了少量的代码添加和转换工作。
如果用户代码中没有提供任何构造函数,那编译器将会添加实例构造器init()方法。除了生成构造器以外,还有其他的一些代码替换工作,如把字符串的 + 操作替换为 StringBuffer 或 StringBuilder(取决于目标代码的版本是否大于或等于 JDK 1.5)的 append() 操作等。
语法糖泛型实现原理
这一节不会介绍泛型语法层面的使用,只介绍编译器为了实现泛型的语法糖做了哪些处理。
类型擦除
编译器在将java文件转换成class文件时,会将泛型信息替换为对应的原始类型进行语法检查,同事为了防止泛型与原始类型匹配的混乱,会在class文件中保留相应的匹配信息。
如下面的java代码:
1 | public class Test<T extends Runnable, V> { |
编译成class文件后,为:

注意到T被替换成了Runnable,而V被替换成了Object。
PS:上面的Signature是属性表中的一个属性,用于支持有泛型情况下泛型签名的记录。
插入类型检查
类型被擦除以后,可能会引起一个问题,参考如下代码:
1 | public class Test<T> { |
由于类型擦除,test.get() 实际返回的应该是一个Object对象,但是我们需要的是一个String对象。为了解决这个问题,编译器会在这个位置插入类型检查的指令。参考编译后的class文件:

可以发现取到的Object对象通过调用checkcast指令进行了类型检查,如果目标类型确实是String则通过检查,否则就抛出ClassCastException异常。
生成桥方法
同样也是因为类型擦除的原因,Java方法override在实现上也做了特殊处理。对于下面这个类:
1 | public class Test<T> { |
因为类型擦除的原因,T会变成Object,因此方法override的时候,子类的test方法的参数应该是Object,而不是具体的类型。但是我们在实际编写代码的时候,写的其实是具体类型,原因就在于编译器帮我们生成了一个桥方法,这个方法是以Object为参数的,它才是真正实现override的那个方法。具体看下面的代码:
1 | public class Test<T> { |
反编译Son类之后,可以看到实际的字节码为:

注意到方法1的参数为Object,方法2的参数为String。同时需要关注的是方法1的具体实现,可以发现方法1做的事情就是做类型检查并调用方法2,然后方法二再调用父类方法。换言之,整体的逻辑就是方法1override父类方法,然后具体的实现逻辑由方法2代理。
另外一个细节值得一提的是,因为编译器会帮我们生成参数为Object的方法,因此如果我们自己在子类中添加Object为参数的方法,就会报方法重复的错误。不过一个比较hack的方法可以避免报错,就是给这个方法的返回参数改变一个值,因为Java语法是以方法参数作为方法签名的,但是在class文件格式中,则是以方法参数与方法返回值作为方法签名的。
泛型的有界类型
对于下面的代码:
1 | public class Test<T extends String> { |
由于约束了泛型的边界,因此T擦除后最终会变成设定的边界值,具体如下:

final关键字与闭包
闭包
定义一个闭包的要点如下:
- 一个依赖于外部环境的
自由变量的函数.- 这个函数能够访问外部环境的
自由变量.
更加具体的介绍可以参考 Java中的闭包之争
实现原理
在Java中,可以通过将外部的自由变量设置为final,实现闭包的效果(虽然是阉割版的,不能修改自由变量的值)。那么具体是如何实现的呢?先来看下面的代码:
1 | public class Test { |
上面的代码是典型的闭包的应用,查看编译出来的class文件(只看匿名内部类):

可以看到在1处,编译器帮我们给匿名内部类添加了一个int类型的final字段,同时2处的构造方法也同样添加了一个int参数,至于3、4则是对该字段的写入与读取。
可以发现,实现闭包的关键在于编译器给内部类添加了一个放置外部自由变量值拷贝的字段。这样内部函数实际上访问的就是这个copy值。
从内存模型的角度讲,这样的实现将方法栈上的局部变量复制到了堆上,使之生命周期变长,为闭包提供了实现基础。
JVM的后端编译器
JVM编译优化的主要工作都在这一层,相关的内容非常多,这里只简单的介绍JIT相关的技术。
JIT概述
JIT即just in time,动态编译或即时编译。因为JVM是通过解释器解释执行指令的,因此效率比较低,为了提高效率,会将部分热点代码编译成native代码来代替解释执行,提高效率。
JIT工作过程
当 JIT 编译启用时(默认是启用的),JVM 读入.class 文件解释后,将其发给 JIT 编译器。JIT 编译器根据其是否为热点代码,选择性地将字节码编译成本机机器代码,下图展示了该过程。

热点代码
上面提到JIT只会编译热点代码,所谓的热点代码,包括被多次调用的方法以及被多次执行的循环体。相应的,为了检测热点代码,就有了热点判定算法,现在主流的实现有以下两种:
- 基于采样的热点探测:采用这种方法的虚拟机会周期性地检查各个线程的栈顶如果发现某个(或某些)方法经常出现在栈顶,那这个方法就是“热点方法”。
- 基于计数器的热点探测:采用这种方法的虚拟机会为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果次数超过一定的阈值就认为它是“热点方法”。
用图来描述计数器算法,如下:

静态提前编译(Ahead Of Time,AOT编译)
JIT编译器工作于VM运行时,而AOT编译器则在VM启动前完成工作。它提前将源代码编译为本地机器码,更直接地提高了代码的运行效率。
这一节主要介绍AOT在Android虚拟机中的应用。
- Android 4.x(Interpreter + JIT)
- 原理:平时代码走解释器,但热点trace会执行JIT进行即时编译
- 优点:占用内存少
- 缺点:耗电(退出App下次启动还会重复编译),卡顿(JIT编译时)
- Android 5.0/5.1/6.0(interpreter + AOT)
- 原理: 在AOT模式下,App在安装过程时, 就会完成所有编译。
- 优点: 性能好
- 缺点: App安装时间长,占用存储空间多。
- Android 7.0/7.1的ART引入了全新的Hybrid模式(Interpreter + JIT + AOT)
- 原理:
- App在安装时不编译, 所以安装速度快。
- 在运行App时, 先走解释器, 然后热点函数会被识别,并被JIT进行编译, 存储在jit code cache, 并产生profile文件(记录热点函数信息)。
- 等手机进入charging和idle状态下, 系统会每隔一段时间扫描App目录下profile文件,并执行AOT编译(Google官方称之为profile-guided compilation)。
- 不论是jit编译的binary code, 还是AOT编译的binary code, 它们之间的性能差别不大, 因为它们使用同一个optimizing compiler进行编译。
- 优点: App安装速度快,占用存储少(只编译热点函数)。
- 缺点: 前几次运行会较慢, 只有用户操作得次数越多,jit 和AOT编译后, 性能才会跟上来。