深入理解java虚拟机-第四部分 程序编译与代码优化
第十章、前端编译与优化
对于效率的追求永不止步。
10.1 概述
- 前端编译器:JDK的Javac,Eclipse JDT中的增量编译器(ECJ)
- 即时编译器(常称JIT编译器,Just In Time Compiler):Hotspot中的C1,C2编译器,Graal编译器
- 提前编译器(常称AOT编译器,Ahead Of Time Compiler):JDK的Jaotc、GNU Compiler for java (GCJ)、Excelsior JET
10.2 Javac编译器
Javac由Java语言编写
10.2.1 Javac的源码与调试
TODO 待完善

10.3 Javac语法糖的味道
语法糖是前端编译器的"小把戏"。要能够通过语法糖看到背后代码真实的面目。
10.3.1 泛型
泛型的本质是参数类型化(Parameterized Type)或 参数化多态的应用,即可以将操作的数据类型指定为方法签名中的特殊参数,能够应用在类、接口、方法,增强了编程语言的类型系统和抽象能力。
10.3.1.1 Java与C#的泛型
具现化、特化、偏特化 都源于C++模版语法中的概念
Java 类型擦除式泛型(Type Erasure Generics):泛型只在源码中存在,在字节码中,泛型被替换为裸类型(Raw Type),并在相应的地方插入强制转型代码。
C# 具现化式泛型(Reified Generics):泛型在 程序源码、编译后的中间语言表示、运行时期的CLR 都是切实存在的。List<int>`List
Java泛型限制:无法对泛型进行实例判断,无法使用泛型创建对象,无法使用泛型创建数组。
10.3.1.2 泛型的历史背景
《Java语言规范》:二进制向后兼容性。
实现思路:1. 平行的添加一套泛型版本的新类型 2. 直接把已有的类型泛型化
10.3.1.3 类型擦除
裸类型(Raw Type):裸类型应被视为所有该类型泛型化实例的共同父类(Super Type)
擦除导致:1. 对原始数据(Primitive Type)类型支持比较麻烦。2.运行期无法获取泛型类型信息。
由于Java泛型的引入,JCP组织对《Java虚拟机规范》做出了相应修改,引入诸如:Signature、LoaclVaribaleTypeTable等新属性,解决伴随泛型而来的参数类型是识别问题。Signature存储字节码层面的特征签名,参数包含了参数化类型信息
10.3.1.4 值类型与未来的泛型
Valhalla 项目:改进语言缺陷(泛型便是其中之一),Java值类型被称为"内联类型",计划通过新的关键字 inline来定义,字节码层面有与原生类型对应的Q开头的新操作吗(如:iload对应qload)
会提供"值类型(Value Type)":值类型可以与引用类型一样,具有构造方法、字段、方法等。它在赋值时通常是整体复制,容易实现分配在方法调用栈上。
10.3.2 自动装箱、拆箱与循环遍历
自动装箱(基本类型包装类的 valueOf() 方法 ),自动拆箱(基本类型包装类的 xxValue()方法),循环遍历(for-each,迭代器实现(需要实现Iterable接口))
10.3.3 条件编译
Java语言条件编译:使用条件为常量的if语句,如果使用常量与其他有判断能力的语句将会报错
Java常见语法糖:泛型、自动装箱、自动拆箱、循坏遍历、变长参数、条件编译、内部类、枚举类、断言语句、数值字面量、对枚举和String的switch语句、try语句中定义和关闭资源、Lambda表达式(,不算单纯的语法糖,前端编译器做了大量转换工作),等等,可通过Javac源码以及反编译窥探奥秘
10.4 插入式注解处理器
TODO 待完成
第十一章、后端编译与优化
11.1 概述
把字节码看作程序语言的中间表示形式(Intermediate Represention,IR)的话,那么编译器将class文件转换为基础设置(硬件指令集、操作系统)相关的二进制机器码,都可以视为编译过程的后端。
提前编译(Ahead Of Time,AOT)、即时编译器(Just In Time,JIT)
11.2 即时编译器
主流的商用虚拟机(HotSpot,OpenJ9),最初通过解释(Interpreter)执行,当虚拟机发现某个方法或代码块运行特别频繁时,这些代码被认定为"热点代码(Hot Spot Code)",为了提高热点代码的运行效率,在运行时,把这些代码编译为本地代码,并以各种手段进行代码优化,运行时完成这个编译过程的后端编译器被称为即时编译器。
11.2.1 解释器与编译器
主流商用Java虚拟机都包含解释器与编译器
解释器:省去编译时间,立即运行
编译器:随着时间把越来越多的代码编译成本地代码,获得更高执行效率。
解释器与编译器交互

HotSpot内置三个即时编译器:"客户端编译器(Client Compiler,简称C1编译器)"、"服务端编译器(Server Compiler,简称C2编译器)"、"Graal编译器(JDK10出现,长期目标替换C2,实验状态)"
虚拟机编译模式:
-Xint 参数 强制解释模式(Interpreted Mode),
-XComp 参数 强制编译模式(Compiled Mode),
解释器与编译器混合,混合模式(Mixed Mode)
-X TieredCompiled 参数 分层编译(Tiered Compilation)
分层编译根据编译器编译、优化规模与耗时,分为:
- 第0层、纯解释执行,且解释器不开启性能监控功能(Profiling)
- 第1层、客户端编译器编译为本地代码,进行简单可靠的稳定优化,不开启性能监控功能。
- 第2层、仍使用客户端编译器执行,仅开启方法回边数统计等有限的的性能监控功能
- 第3层、仍使用客户端编译器执行,开启全部性能监控功能,除第二层外,还收集:如:虚方法调用版本,分支跳转等全部的统计信息
- 第4层、服务端编译器编译为本地代码,使用耗时编译更长的优化,根据性能监控,进行一些不可靠的性能优化。
分层编译后各编译器共同工作,热点代码可能会被多次编译
分层编译交互关系
![]()
11.2.2 编译对象与触发条件
被即时编译器编译的目标是"热点代码":
- 被多次调用的方法。
- 被多次执行的循环体
编译的目标都是整个方法体,循环体触发只是执行入口(从方法的第几条字节码开始执行)会稍有不同,编译时会传执行入口字节码序号(Byte Code Index,BCI),这种编译因为发生在执行方法过程中,也被称为"栈上替换(On Stack Replacement,OSR)"
"热点探测(Hot Spot Code Detection)"主流方法:
- 基于采样的热点探测(Sample Base Hot Spot Code Detection):周期检查各个线程栈顶,如果经常出现在栈顶,则为热点方法。方法实现简单高效,但是容易受线程阻塞、外界因素扰乱。
- 基于计数器的热点探测(Counter Base Hot Spot Code Detection):为每个方法甚至方法块建立计数器,统计执行次数,一旦超过一定阈值则认为是热点代码。实现麻烦但是统计结果相对严谨。
HotSpot虚拟机采用第二种计数探测,实现计数准备了两种计数器:方法调用计数器(Invocation Counter)和回边计数器(Back Edge Counter,回边:在循环边界往回跳转),计数器有明确阈值,阈值溢出,触发即时编译。
方法调用计数器:- XX:CompileThreshold 设置阈值。过一段时间仍为满足阈值要求,则计数减半,称为方法调用计数器的热度衰减(Counter Decay),这个时间称为方法统计的半衰期(Counter Half Life Time),可以使用 -XX: -UseCounterDecay 关闭热度衰减,-XX:CounterHalfLifeTime 设置半衰期时间,单位s。
方法调用计数器触发即时编译流程

回边计数器:统计方法中循环体执行次数。字节码中遇到控制流向后跳转的指令就称为"回边 Back Edge"。目的:触发栈上的替换编译。阈值设置参数-XX:BackEdgeThreshold 未启用。回边计数器没有热度衰减过程
回边计数器触发即时编译流程
11.2.3 编译过程
默认编译未完成前,仍按解释执行代码,可以使用 -XX:-BackgroundCompile来禁止后台编译,执行线程提交编译请求后阻塞等待直到编译完成。
客户端编译器相对于服务端编译器,是相对简单快速的三段式编译器,注重局部优化,放弃耗时较长的全局优化手段。
第一阶段:一个平台独立的前端将字节码构造成一种高级中间代码表示(High-Level Intermediate Representation,HIR)
第二阶段:一个平台相关的后端从HIR中产生低级中间代码表示(Low-Level Intermediate Representation,LIR)
第三阶段:平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在LIR上分配寄存器
客户端编译器编译流程
服务端编译器专门为服务端设计能够容忍很高优化复杂度的高级编译器,会执行大部分经典优化动作,如:无用代码消除(Dead Code Elimination)、循环展开(Loop Unrolling)、循环表示外提(Loop Expression Hoisting)、消除公共子表达式(Common SubExpression Elimination)、常量传播(Constant Propagation)、基本块重排(Basic Block Reordering)等。
11.2.4 实战:查看及分析即时编译结果
TODO 待完成
11.3 提前编译器
11.3.1 提前编译的优劣得失
提前编译有两条明显分支:
- 与传统C、C++类似在程序运行之前把程序代码编译成机器码的静态工作过程
- 把原本即时编译器在运行时要做的编译工作提前做好并保存下来
第一条:即时编译的弱点:占用程序运行时间和运算资源
第二条:给即时编译器做缓存加速,改善Java启动时间,以及预热达到最高性能的问题,这种编译被称为动态提前编译(Dynamic AOT)或即时编译缓存(JIT Caching),JDK9中出现的Jaotc引起广泛关注,CDS(Class Data Sharing)等。
提前编译与即时编译:
提前编译:不受时间和资源的压力,能够使用重负载的优化手段。
即时编译: - 性能分析制导优化(Profile-Guided Optimization,PGO),性能监控提供优化参考
- 激进预测性优化(Aggressive Speculative Optimization),很多即时编译器优化措施的基础
- 连接时优化(Like-Time Optimization,LTO)
11.3.2 实战:Jaotc的提前编译
Jaotc支持对Java文件和模块进行提前编译
TODO 待完成
11.4 编译器优化技术
11.4.1 优化技术概览
挑选代表优化技术进行分析:
- 最重要的优化技术之一:方法内联
- 最前沿的优化技术之一:逃逸分析
- 语言无关的经典优化技术之一:公共子表达式消除
- 语言无关的经典优化技术之一:数组边界检查消除
11.4.2 方法内联
方法内联:把目标方法代码"复制"到调用的方法中,避免发生真实的方法调用。
编译期解析:非虚方法(non-virtual method)包括invokeSpecial调用的 构造方法、私有方法、父类方法、invokeStatic调用静态方法和final方法。
其他虚方法(virtual method)需要等到运行时进行方法接受者的多态选择。
解决虚方法,Java虚拟机引入类型继承关系分析(Class Hierarchy Analysis,CHA)技术,整个应用程序范围内的类型分析技术。非虚方法可直接内联,虚方法仅有一个版本可守护内联。多个版本可使用内联缓存
11.4.3 逃逸分析
逃逸分析(Escape Analysis)是目前Java虚拟机比较前沿的技术,与类型继承关系分析一样,并不是直接优化代码的手段,而是为其他优化提供依据的分析技术。
原理:分析对象的动态作用域,当一个对象在方法中被定义后可能被外部方法所引用,称为方法逃逸(如作为参数传递),甚至可能被外部线程访问,称为线程逃逸(赋值给其他线程可访问的实例变量)。 不逃逸、方法逃逸、线程逃逸 逃逸程度由低到高。
如果一个对象不会逃逸到方法或线程外、或逃逸程度较低(只逃逸出方法,不逃逸线程)可采取不同程度优化:
- 栈上分配(Stack Allocation):堆中对象对于各个线程都是共享可见的,只要持有这个对象的引用就可以访问堆中对象数据。堆中对象管理耗费大量资源,栈上分配,随栈帧出站而销毁垃圾收集子系统压力减小,支持方法逃逸,不支持线程逃逸。
- 标量替换(Scalar Replacement):若一个数据无法再分为更小的数据来表示,Java虚拟机中的原始数据类型(int,long等数据之类和reference引用类型)被称为标量。如果一个数据可以继续分解被称为聚合量(Aggregate)如Java对象。如果把一个对象拆散,根据程序访问情况将其用到的成员变量恢复为原始类型来访问,这个过程称为标量替换。创建对象改为被这个方法访问的成员变量代替,让对象的成员变量在栈上分配和读写,还可以为后续优化提供条件,要求不逃逸。
- 同步消除(Synchronization Elimination):线程同步相对耗时,如果一个变量不会线程逃逸,则对这个变量的同步可以安全的消除。
Java 实验阶段的inline关键字用于定义Java的内联类型。
11.3.4 公共子表达式消除
公共子表达式消除是一项非常经典的、普遍适用于各类编译器的优化技术,含义:如果一个表达式E之前已经被计算过,并且从先前计算到现在E中所有变量的值都么有发生变化,那么E的这次出现就称为公共子表达式。对于这种表达式,没必要再次计算,只需要用前面计算过的结果替代E。如果这种优化仅局限于程序基本块内,称为局部公共子表达式消除(Local Common Subexpression Elimination),如果这种优化范围涵盖多个基本块,则称为全局公共子表达式消除(Global Common Subexpiression Elimination)。
11.4.5 数组边界检查消除
数组边界检查消除(Array Bounds Checking Elimination)即时编译器中一项语言相关的经典优化技术。Java是一门动他安全语言,Java语言数据元素访问自动进行上下界范围检查,否则抛出运行时异常ArrayIndexOutofBoundsException。
优化:编译器确认取值范围在[0,foo.lenght)之内,就可以去除上下界检查
11.5 实现:深入了解Graal编译器
15.1.1 历史背景
Graal使用Java语言编写,继承了HotSpot服务端编译器的优秀编译技术。JDK9时首次以Jaotc提前编译工具加入官方JDK,JDK10,Graal可替换服务端编译器,称为HotSpot分层编译中最顶层的即时编译器。
Graal与HotSpot解耦合:JPE243:Java虚拟机编译接口(Java-Level Compiler Interface,JVMCI)主要功能:
- 相应HotSpot的编译请求,并将请求分发给Java实现的即时编译器。
- 允许编译器访问HotSpot中与编译有关的的数据结构,包括:类、字段、方法以及性能监控数据等,并提供了一组这些数据在Java语言方面的抽象。
- 提供给HotSpot代码缓存(Code Cache)的Java端抽象表示,允许编译器部署完成的二进制机器码。
11.5.2 构建编译调试环境
TODO 待完成。


浙公网安备 33010602011771号