JVM系列之二:编译过程
0. 相关知识
计算机语言的执行方式分为编译型和解释型两种:
编译型语言:指使用专门的编译器,针对特定平台(操作系统)将某种高级语言源代码一次性翻译成可以被该平台硬件执行的机器码(包括机器指令和操作数),并包装成改平台所能识别的可执行性程序的格式,这个转换过程称为编译(Compile)。编译结束后,可能需要对编译好的目标代码进行链接。优点是运行效率高,组装的时候可以实现低层次的代码复用;缺点是不能跨平台。
解释型语言:指使用专门的解释器,对源程序逐行解释成特定的平台上的机器码并立即执行的语言。优点是跨平台比较容易,只需要提供特定平台上的解释器即可,每个平台上的解释器负责将源程序解释成特定平台的机器指令。缺点是程序执行效率低,每次执行程序都需要进行一次编译。
JAVA:先编译后解释(java源文件--javac编译--.class文件/与平台无关的字节码--JVM解释执行--特定平台的机器码)。javac编译器不需要面向任何具体的平台,只需要面向JVM,不同平台上JVM是不同的,但这些不同的JVM都提供了相同的接口,从而保证了Java的跨平台性。
编译型语言:C、C++、Objective-C、Pascal。(提高速度,复用)
半编译型语言:Visual Basic
解释型语言:Ruby、Python(减少内存,跨平台)
1. Java的编译和执行
编译包括两种情况:
1,源码编译成字节码
2,字节码编译成本地机器码(符合本地系统专属的指令)
解释执行也包括两种情况:
1,源码解释执行
2,字节码解释执行
编译和解释执行的区别是:是否产生中间本地机器码。
即时编译生成机器相关的中间码,可重复执行缓存效率高。
解释执行直接执行字节码,重复执行需要重复解释。
2. 编译原理
在执行前先对程序源码进行词法解析和语法解析处理,把源码转化为抽象语法树。
其中绿色的模块可以选择性实现。
- 上图中间的那条分支是解释执行的过程(即一条字节码一条字节码地解释执行,如JavaScript),
- 而下面的那条分支就是传统编译原理中从源代码到目标机器代码的生成过程。
对于一门具体语言的实现来说:
- 词法和语法分析乃至后面的优化器和目标代码生成器都可以选择独立于执行引擎,形成一个完整意义的编译器去实现,这类代表是C/C++语言。
- 也可以把抽象语法树或指令流之前的步骤实现一个半独立的编译器,这类代表是Java语言。
- 又或者可以把这些步骤和执行引擎全部集中在一起实现,如大多数的JavaScript执行器。
3. 三个编译器
JVM的编译器可以分为三个编译器:
(1)前端编译器:把.java转变为.class的过程。如Sun的Javac、Eclipse JDT中的增量式编译器(ECJ)。
(2)后端编译器,它在程序运行期间将字节码转变成机器码(现在的Java程序在运行时基本都是解释执行加编译执行), 如HotSpot虚拟机自带的JIT(Just In Time Compiler)编译器(分Client端和Server端)。
(3)AOT编译器:静态提前编译器(AOT,Ahead Of Time Compiler)直接把*.java文件编译成本地机器代码,如GCJ、Excelsior JET等,这类编译器我们应该比较少遇到。
4.编译期-Javac编译过程
.java
文件是由Java源码编译器(上述所说的javac.exe)来完成,流程图如下所示:
Java源码编译由以下三个过程组成:
- 分析和输入到符号表
- 注解处理
- 语义分析和生成class文件
1、解析与填充符号表过程 1)、词法、语法分析 词法分析将源代码的字符流转变为标记集合,单个字符是程序编写过程的最小元素,而标记则是编译过程的最小元素,javac中由com.sun.tools.javac.parser.Scanner类实现 语法分析是根据token序列构造抽象语法树的过程。抽象语法树(AST)是一种用来描述程序代码语法结构的树形表示方式,语法树种的每一个节点都代表着程序代码中的语法结构,javac中,语法分析过程由com.sun.tools.javac.tree.parser.Parser类实现,这个阶段产生出的抽象语法树由com.sun.tools.javac.tree.JCTree类表示 2)、填充符号表 enterTree()方法,符号表是由一组符号地址和符号信息构成的表格,符号表中登记的信息在编译的不同阶段都要用到。在语义分析中,符号表所登记的内容将用于语义检查和产生中间代码,在目标代码生成阶段,当对符号进行地址分配时,符号表是地址分配的依据。javac源码中由com.sun.tools.javac.comp.Enter类实现 2、插入式注解处理器的注解处理过程 注解在运行期间发挥作用,通过插入式注解处理器标准API中可以读取、修改、添加抽象语法树种的任意元素,若在处理注解期间对语法树进行修改,编译器将回到解析即填充符号表的过程重新处理,直到所有插入式注解处理器都没有再对语法树进行修改为止,每一次循环称为一个round。javac源码中插入式注解处理器的初始化过程是在initProrcessAnnotation()方法中完成的,而它的执行过程则是在processAnnotation()方法中完成。 3、分析与字节码生成过程 1)、标注检查 attribute()方法,标注检查步骤检查的内容包括诸如变量使用前是否已经被声明、变量与赋值之间的数据类型是否够匹配以及常量折叠。javac中实现类是com.sun.tools.javac.comp.Attr类和com.sun.tools.javac.comp.Check类 2)、数据及控制流分析 flow()方法,对程序上下文逻辑更进一步的验证,他可以检查出诸如程序局部变量在使用前是否赋值、方法的每条路径是否都有返回值、是否所有的受检查异常都被正确处理了问题 局部变量在常量池中没有CONSTANT_Fieldref_info的符号引用,自然没有访问标志的信息,甚至可能连名称都不会保存下来 将局部变量声明为final,对运行期是没有影响的,变量的不变性仅仅由编译器在编译期间保障 3)、解语法糖 也称糖衣语法,指在计算机中添加某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用,通常来说,使用语法唐能够增加程序的可读性,从而减少程序代码出错的机会。java中最常用的是泛型、变长参数、自动装箱/拆箱等 4)、字节码生成 javac编译的最后一个阶段,javac源码里面由com.sun.tools.javac.jvm.Gen类来完成,这个阶段不仅仅把前面各个步骤所生成的信息转化成字节码写到磁盘中,编译器还进行少量的代码添加转换工作 保证一定是按先执行父类的实例构造器,然后初始化变量,最好执行语句块的顺序进行
5. 运行期-JIT编译
5.1 解释器和编译器
Java程序最初是仅仅通过解释器解释执行的,即对字节码逐条解释执行,这种方式的执行速度相对会比较慢,尤其当某个方法或代码块运行的特别频繁时,这种方式的执行效率就显得很低。
于是后来在虚拟机中引入了JIT编译器(即时编译器),当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为“Hot Spot Code”(热点代码),为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,完成这项任务的正是JIT编译器。
PS:区别是:即时编译生成机器相关的中间码,可重复执行缓存效率高。解释执行直接执行字节码,重复执行需要重复解释。
现在主流的商用虚拟机(如Sun HotSpot、IBM J9)中几乎都同时包含解释器(减少内存)和编译器(提高速度)
解释器与编译器
1. 当程序需要迅速启动和执行的时候,
解释器可以首先发挥作用,省去编译的时间,立即执行,
在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后可以获得更高的执行效率。
当程序运行环境中内存资源限制较大,可以使用解释执行节约内存,反之可以使用编译执行来提升效率, 2. 解释器还可以作为编译器激进优化时的一个“逃生门”,让编译器根据概率选择一些大多数时候都能提升运行速度的优化手段,
当激进优化的假设不成立,如加载了新类后类型继承结构出现变化、出现“罕见陷阱”时可以通过逆优化退回到解释状态继续执行,
部分没有解释器的虚拟机中采用不进行激进优化的C1编译器担任“逃生门”的角色。 3. HotSpot中内置两个即时编译器C1编译器和C2编译器。
可以通过“-client”和”server”参数去强制指定虚拟机运行在Client模式或Server模式。
用Client Compiler获取更高的编译速度,C1
用Server Compiler来获取更好的编译质量。 C2
4. 解释器与编译器混搭配使用的方式在虚拟机中称为”混合模式”,
“-Xint”强制虚拟机运行于“解释模式”,
“-Xcomp”强制虚拟机运行于“编译模式”,但是解释器仍然要在编译无法进行的情况下介入执行过程,
可以通过“-version”命令输出结果显示3中模式。 分层编译
第0层:程序解释执行,解释器不开启性能监控,可触发第1层编译 第1层:也称为C1编译,将字节码编译为本地代码,进行简单、可靠的优化,如有必要将加入性能监控的逻辑 第2层:也称C2编译,将字节码编译为本地代码,也会开启一些编译耗时较长的优化,升值会根据性能监控信息进行一些不可靠的激进优化。
用Client Compiler获取更高的编译速度,C1
用Server Compiler来获取更好的编译质量。 C2 编译过程
1. 虚拟机在代码编译器未完成之前,都仍然将按照解释方式继续执行,而编译动作则在后台的编译线程中进行。
2. 也可以禁止后台编译,禁止后,一旦达到JIT的编译条件,执行线程向虚拟机提交编译请求后将会一直等待,直到编译过程完成后在开始执行编译器输出的本地代码。
Client Compiler过程
它是一个简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段。
- 在第一个阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(High-Level Intermediate Representaion,HIR)。HIR使用静态单分配(Static Single Assignment,SSA)的形式来代表代码值,这可以使得一些在HIR的构造过程之中和之后进行的优化动作更容易实现。在此之前编译器会在字节码上完成一部分基础优化,如方法内联、常量传播等优化将会在字节码被构造成HIR之前完成。
- 在第二个阶段,一个平台相关的后端从HIR中产生低级中间代码表示(Low-Level Intermediate Representation,LIR),而在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等,以便让HIR达到更高效的代码表示形式。
- 最后阶段是在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窥孔(Peephole)优化,然后产生机器代码。
Server Compiler则是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器,
1)他会执行所有经典的优化动作,如:无用代码消除,循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序等,
2)还会实施一项与java语言特性密切相关的优化技术,如范围消除、控制检查消除。
3)Server Compiler的寄存器分配器是全局图着色分配器,从即时编译器来看他无疑是比较缓慢的,
但他的编译速度依然远远超过传统静态优化编译器,而相对于Client Compiler编译输出的代码质量有所提高,可以减少本地代码执行时间,
5.2 编译对象与触发条件—热点代码
1. 编译对象
在运行过程中会被即时编译器编译的“热点代码”有两类
- 被多次调用的方法:由于方法调用触发的编译,因此编译器理所当然地会以整个方法作为编译对象,这种编译也是虚拟机中标准的JIT编译方式。
- 被多次调用的循环体:尽管编译动作是由循环体所触发,但编译器依然会以整个方法作为编译对象,称为“栈上替换–简称OSR编译”
- ps: OSR是一种在运行时替换正在运行的函数/方法的栈帧的技术。
2. 触发条件:热点探测
判断一段代码是不是热点代码,是不是需要触发即时编译,这样的行为称为热点探测(Hot Spot Detection),其实进行热点探测并不一定要知道方法具体被调用了多少次,目前主要的热点探测判定方式有两种。
1. 基于采样的热点探测:虚拟机会周期性地检查各个线程的栈顶,如果发现某些方法经常出现在栈顶,那这段方法代码就是“热点代码”。
- 好处是:实现简单高效,还可以很容易地获取方法调用关系,
- 缺点是:很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而扰乱热点探测。
2. 基于计数器的热点探测(HotSpot虚拟机)虚拟机会为每个方法,甚至是代码块建立计数器,统计方法的执行次数,如果执行次数超过一定的阀值,就认为它是“热点方法”。
- 缺点:这种统计方法实现复杂一些,需要为每个方法建立并维护计数器,而且不能直接获取到方法的调用关系
- 好处:统计结果相对更加精确严谨。
- 两个:方法调用计数器,回边计数器。
3. 基于计数器的热点探测方法的两个计数器
Hotspot中,在确定虚拟机运行参数的前提下,这两个计数器都有一个确定的阀值,当计数器的值超过了阀值,就会触发JIT编译。
1. 方法调用计数器 用来统计方法调用的次数,在默认设置下,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对的执行频率,即一段时间内方法被调用的次数。 方法调用计数器默认情况下CLient模式下是1500次,Server模式下是10000次,可以通过-XX:CompileThreshold来设置 一段时间内还是未超过阈值,方法的调用计数器就会被减少一半,这种方法叫做计算器热度的衰减,这段时间称为次方法统计的半衰周期,进行衰减的动作是在虚拟机进行垃圾收集时顺便进行的。可以用参数-XX:-UseCounterDecay来关闭热度衰减。也可以通过参数设置半衰周期的时间。 2. 回边计数器 回边计数器,统计一个方法中循环体代码执行的次数准确地说,应该是回边的次数,因为并非所有的循环都是回边),在字节码中遇到控制流向后跳转的指令称为“回边”,建立回边计数器统计的目的就是为了触发OSR(On-Stack Replacement)编译。 回边计数器没有技术热度衰减的过程,因此这个计数器统计的就是该方法循环执行的绝对次数,当计数器溢出的时候,他还会把方法计数器的值也调整到溢出状态,这样下次在进入该方法的时候会执行标准编译过程。 回边计数器阈值计算公式: Client:方法调用计数器阈值 X OSR比率/100其中OnStackReplacePercentage默认值是933,若都取默认值CLient模式虚拟机的回边计数器阈值为13995. Server:方法调用计数器阈值X(OSR比率-解释器监控比率)/100 其中OnStackReplacePercentage默认值是140,InterpreterProfilePercentage默认值是33,若都去默认值,阈值为10700
5.3 即时编译和解释执行的执行顺序
- 触发了JIT编译后,在默认设置下,执行引擎并不会同步等待编译请求完成,
- 而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成为止(编译工作在后台线程中进行)。
- 当编译工作完成后,下一次调用该方法或代码时,就会使用已编译的版本。
- 方法调用计数器
6. 编译优化
6.1 编译优化-语法糖(早期Javac阶段)
语法糖可以看做是编译器实现的一些“小把戏”,这些“小把戏”可能会使得效率“大提升”。
Java中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。虚拟机并不支持这些语法,它们在编译阶段就被还原回了简单的基础语法结构,这个过程成为解语法糖。
泛型、自动装箱、自动拆箱、遍历循环、变长参数、条件编译、内部类、枚举类、断言语句、对枚举、字符串的switch,try与居住定义和关闭自由。
6.1.1 泛型与类擦除
Java语言在JDK1.5之后引入的泛型实际上只在程序源码中存在,在编译后的字节码文件中,泛型会被替换为原来的原生类型(Raw Type,也称为裸类型)了。这个过程也被称为:泛型擦除。并且在相应的地方插入了强制转型代码,因此对于运行期的Java语言来说,ArrayList<String>和ArrayList<Integer>就是同一个类。所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型被称为伪泛型。
泛型类、泛型接口、泛型方法,C#中List与List就是两个不同的类型,他们在系统运行期生成,有自己的需方发表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。
java中泛型不一样,他只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型,并在相应得地方插入了强制转换代码,对于运行期的java来说ArrayList与ArrayList就是同一个类,java语言中的泛型实现方法称为类型擦除,基于这种方法的叫伪泛型
在Class文件格式中,只要描述符不是完全一致的两个方法就可以共存,也就是说两个方法如果有相同的名称和特征签名,但返回值不同,那他们也是可以合法地共存于一个class文件中
Signature是解决伴随泛型而来的参数类型的识别问题中最重要的一项属性,它的作用就是存储一个方法在字节码层面的特征签名,这个属性中保存的参数类型并不是原生类型,而是包括了参数化类型的信息、
擦除法所谓的擦除,仅仅是对方法的Code属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能够通过反射手段取得参数化类型的根本依据。
Map<Integer,String> map = new HashMap<Integer,String>(); map.put(1,"No.1"); map.put(2,"No.2"); System.out.println(map.get(1)); System.out.println(map.get(2)); //将这段Java代码编译成Class文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都变回了原生类型,如下面的代码所示: Map map = new HashMap(); map.put(1,"No.1"); map.put(2,"No.2"); System.out.println((String)map.get(1)); System.out.println((String)map.get(2)); //为了更详细地说明类型擦除,再看如下代码: import java.util.List; public class FanxingTest{ public void method(List<String> list){ System.out.println("List String"); } public void method(List<Integer> list){ System.out.println("List Int"); } } //用Javac编译器编译这段代码时,报出了如下错误: FanxingTest.java:3: 名称冲突:method(java.util.List<java.lang.String>) 和 method (java.util.List<java.lang.Integer>) 具有相同疑符 public void method(List<String> list){ ^ FanxingTest.java:6: 名称冲突:method(java.util.List<java.lang.Integer>) 和 metho d(java.util.List<java.lang.String>) 具有相同疑符 public void method(List<Integer> list){ ^ //这是因为泛型List<String>和List<Integer>编译后都被擦除了,变成了一样的原生类型List
有了泛型这颗语法糖以后:
- 代码更加简洁【不用强制转换】
- 程序更加健壮【只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常】
- 可读性和稳定性【在编写集合的时候,就限定了类型】
6.1.2 条件编译
java编译器并非一个个的编译Java文件,而是将所有编译单元的语法树顶级节点输入到待处理列表后再进行编译,因此各个文之间能够互相提供符号信息。
Java语言使用条件为常量的if语句,此代码中的if语句不同于其他Java代码,它在编译阶段就会被运行,生成的字节码之中只包含条件正确的部分。
java中根据布尔常量值的真假,编译器会把分支中不成立的代码块擦除掉。这一工作将在编译器解除语法糖阶段完成。
6.1.3 自动装箱、拆箱与遍历循环
自动装箱、拆箱在编译之后就被转换成了相应的包装和还原方法,如Integer.valueOf()与Integer,intValue()方法,而遍历循环则把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历类实现Iterable接口的原因。
包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,以及它们equals()方法不处理数据转型的关系。
6.2 运行期优化
Java 程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。 为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,简称JIT编译器)。
当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释执行节约内存,反之可以使用编译执行来提升效率。
解释器(节约内存)+JIT编译器(热点代码提升效率)。
6.3 即时编译器优化技术
首先需要明确的是,这些代码优化变换是建立在代码的某种中间表示或机器码之上,绝不是建立在Java源码之上的。
对代码的所有优化措施都集中在即时编译器中,JIT 即时编译器产生的本地代码会比Javac产生的字节码更加优秀
常用优化技术:
(1)方法内联(Method Inlining)一是去除方法调用的成本(如建立栈帧等),二是为其他优化建立良好的基础。
方法内联就是把被调用方函数代码”复制”到调用方函数中,来减少因函数调用开销的技术(栈帧)。
背景:函数调用的压栈和出栈过程,因此,函数调用需要有一定的时间开销和空间开销,
当一个方法体不大,但又频繁被调用时,这个时间和空间开销会相对变得很大,变得非常不划算,同时降低了程序的性能。
根据二八原则,80%的性能消耗其实是发生在20%的代码上,
对热点代码的针对性优化可以提升整体系统的性能。
代码:
privateintadd4(int x1, int x2, int x3, int x4) {
return add2(x1, x2) + add2(x3, x4);
}
privateintadd2(int x1, int x2) {
return x1 + x2;
}
内联后,运行一段时间后JVM会把add2方法去掉,并把你的代码翻译成:
privateintadd4(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}
触发条件:
JVM会自动的识别热点方法,并对它们使用方法内联优化。那么一段代码需要执行多少次才会触发JIT优化呢?通常这个值由-XX:CompileThreshold参数进行设置:
1、使用client编译器时,默认为1500;
2、使用server编译器时,默认为10000;
但是一个方法就算被JVM标注成为热点方法,JVM仍然不一定会对它做方法内联优化。其中有个比较常见的原因就是这个方法体太大了,分为两种情况。
-
如果方法是经常执行的,默认情况下,方法大小小于325字节的都会进行内联(可以通过
-XX:MaxFreqInlineSize=N
来设置这个大小) -
如果方法不是经常执行的,默认情况下,方法大小小于35字节才会进行内联(可以通过
-XX:MaxInlineSize=N
来设置这个大小)
(2)冗余访问消除(Redundant Loads Elimination)
static class B{ int value; final int get(){ return value; } } public void foo(){ y=b.get(); //……do stuff…… z=b.get();//z=y sum=y+z;//sum=y+y } 假设代码中的“dostuff……”所代表的操作不会改变b.value的值,那就可以把“z=b.value”替换为“z=y”, 因为上一句“y=b.value”已经保证了变量y与b.value是一致的,这样就可以不再去访问对象b的局部变量了。
(3)复写传播(Copy Propagation)
在这段程序的逻辑中并没有必要使用一个额外的变量“z”,它与变量“y”是完全相等的,因此可以使用“y”来代替“z”。
(4)无用代码消除(Dead Code Elimination)
无用代码可能是永远不会被执行的代码,也可能是完全没有意义的代码,因此,它又形象地称为“Dead Code”。
(5)公共子表达式消除
1. 如果一个表达式E已经计算过,并且从先前到现在E中所有变量的值没有发生变化,那E的这次出现就成为了公共子表达式,
2. 对这种表达式没必要再花时间对它进行计算,只需要直接用前面计算过的表达式结构结果代替即可,
3. 如果这种优化仅限于程序的基本块内,便称为局部公共子表达式消除,如果覆盖了多个基本块则称为全局公共子表达式消除。
(6)数组边界检查消除(Array Bounds Checking Elimination)
如果有一个数组foo[],在Java语言中访问数组元素foo[i]的时候系统将会自动进行上下界的范围检查,即检查i必须满足i>=0&&i<foo.length这个条件,
否则将抛出一个运行时异常:java.lang.ArrayIndexOutOfBoundsException。
这对软件开发者来说是一件很好的事情,即使程序员没有专门编写防御代码,也可以避免大部分的溢出攻击。
但是对于虚拟机的执行子系统来说,每次数组元素的读写都带有一次隐含的条件判定操作,对于拥有大量数组访问的程序代码,这无疑也是一种性能负担。
数组边界检查优化这种尽可能把运行期检查提到编译期完成的思路之外,另外还有一种避免思路——隐式异常处理
与语言相关的其他消除操作还有不少,如自动装箱消除(Autobox Elimination)、安全点消除(Safepoint Elimination)、消除反射(Dereflection)等
(7)逃逸分析
1. 分析对象动态作用域,当一个方法被定以后,它可能被外部方法所引用,称为方法逃逸,甚至还有可能被外部线程访问到,称为线程逃逸。 2. 若能证明一个对象不会逃逸到方法或线程之外,这可以通过栈上分配、同步消除、标量替换来进行优化。 3. 栈上分配:(线程私有)在一般应用中不会逃逸的局部对象所占比例很大,若能栈上分配就会随着方法的结束而自动销毁了,垃圾回收系统的压力将会小很多。 4. 标量替换:标量,指的是 jvm中描述数据最基本的单位。 列如原始数据类型等。当确定一个对象不会逃逸后,那么就要分配他到栈空间上,然而栈空间是有限的,为了进一节省栈空间,就需要将 对象(聚合量) 拆散为标量。 这样 在jvm不会在栈中创建对象而是仅仅创建对象的成员变量。这样就节省了空间,因为没有对象头以及对齐填充的空间浪费。
5. 同步消除:同样基于逃逸分析,当加锁的变量不会发生逃逸,是线程私有的,那么完全没有必要加锁。 在jit编译时期就可以将同步锁去掉,以减少加锁与解锁造成的资源开销。
摘录链接
- https://www.jianshu.com/p/904b15a8281f
- 【深入Java虚拟机】之七:Javac编译与JIT编译
- 【深入Java虚拟机】之六:Java语法糖
- 学习JVM是如何从入门到放弃的?
- java编译器优化和运行期优化
- OSR(On-Stack Replacement)是怎样的机制?
- 面向JIT编程-方法内联
- JIT——即时编译的原理
- JIT晚期(运行期)