一、前端编译
前端编译就是将 Java 源码文件编译成 Class 文件的过程,编译过程分为 4 步:
1 筹备
初始化插入式注解处理器(Annotation Processing Tool)。
2 解析与填充符号表
将源代码的字符流转变为标记(Token)汇合,结构出 形象语法树(AST)
。
形象语法树每个节点都代表着程序代码中的一个语法结构,蕴含包、类型、修饰符、运算符、接口、返回值、代码正文等内容。
编译器的后续行为都是基于形象语法树来进行。
符号表能够了解为一个 K - V 构造的汇合,存储了以下信息:
- 变量名和常量
- 过程和函数名称
- 文字常量和字符串
- 编译器生成的临时文件
- 源语言中的标签
编译器在运行过程中会通过符号表来不便查找所有标识。
3 注解处理器
注解处理器能够看做是一组编译器的插件,用来读写形象语法树中任意元素。
简略来说,注解处理器的作用就是让编译器对特定注解执行特定逻辑,个别用来生成代码,比方罕用的 lombok 和 mapstruct 都是基于此。
如果在这期间语法树被批改了,编译器将回到“解析与填充符号表”的过程重新处理,这个循环被称作“轮次(Round)”。
这是开发人员惟一能管制编译器行为的形式。
4 剖析与字节码生成
前置步骤能够胜利生成一个构造正确的语法树,语义剖析则是校验语法树是否合乎逻辑。
语义剖析又分为四步:
4.1 标注查看
标注查看次要用来检查表量是否被申明、变量与赋值是否匹配等等。
在这个阶段,还会进行被称作“常量折叠”的优化,比方 Java 代码int a = 1 + 2;
,理论编译后会被折叠为int a = 3
;
4.2 数据及控制流剖析
数据流剖析和控制流剖析是对程序上下文逻辑更进一步的验证,它能够查看出诸如程序局部变量在应用前是否有赋值、办法的每条门路是否都有返回值、是否所有的受查异样都被正确处理了等问题。
4.3 解语法糖
Java 中存在十分多的语法糖用来简化代码实现,比方主动的装箱拆箱、泛型、变长参数等等。这些语法糖会在编译器被还原为根底语法结构,这个过程被称为解语法糖。
4.4 字节码生成
这是 javac
编译过程的最终阶段,编译器会在这个阶段把后面生成的形象语法树、符号表生成为 class 文件,还进行了大量的代码增加和转换。
二、运行时编译
运行时编译的次要目标是为了将代码编译成本地代码,从而节俭解释执行的工夫。
然而 JVM 并不是启动后立即开始执行编译,而是为了执行效率先进行解释执行。等到程序运行过程中,依据热点探测,找出热点代码后,对其进行针对性的编译来逐步代替解释执行。所以 HotSpot JVM 采纳的是解释器和即时编译器并存的架构。
1 应用编译执行的机会
Sun JDK 次要依据办法上的一个计数器来计算是否超过阈值,如果超过则采纳编译执行的形式。
- 调用计数器
记录办法调用次数,在 client 模式下默认为 1500 次,在 server 模式下默认为 10000 次,可通过 -XX:CompileThreshold=10000
来设置
- 回边计数器
循环执行局部代码的执行次数,默认在 client 模式时为 933,在 server 模式下为 140,可通过 -XX:OnStackReplacePercentage=140
来设置
2 编译模式
在编译上,Sun JDK 提供两种模式:client compiler(-client)和 server compiler(-server)
2.1 Client compiler
又称 C1,较为轻量级,次要包含以下几方面:
2.1.1 办法内联
编译器所做最重要的优化是办法内联
遵循面向对象设计,属性拜访通常通过 setter/getter 办法而非间接调用,而此类办法调用的开销很大,特地是绝对办法的代码量而言。
当初的 JVM 通常都会用内联代码的形式执行这些办法,举个例子:
Order o = new Order();
o.setTotalAmount(o.getOrderAmount() * o.getCount());
而编译后的代码实质上执行的是:
Order o = new Order();
o.orderAmount = o.orderAmount * o.count;
内联默认是开启的,可通过 -XX:-Inline
敞开,然而因为它对性能影响微小,并不倡议敞开。
办法是否内联取决于它有多热以及它的大小。
2.1.2 去虚拟化
如发现类中的办法只提供了一个实现类,那么对于调用了此办法的代码,将进行办法内联
public interface Animal {void eat();
}
public class Cat implements Animal {
@Override
public void eat() {System.out.println("Cat eat !");
}
}
public class Demo {public void execute(Animal animal){animal.eat();
}
}
如果 JVM 中只有 Cat 类实现了 Animal 接口,execute()
办法被编译时,调演变成相似如下构造:
public void execute() {System.out.println("Cat eat !");
}
即 execute()
办法间接内联了 Cat
类中 eat()
办法的外部逻辑。
2.1.3 冗余打消
冗余打消指在编译时,依据运行状况进行代码折叠或者打消
例如:
private static final boolean isDebug = false;
public void execute() {if (isDebug) {log.debug("do execute.");
}
System.out.println("done");
}
在执行 C1 编译后,调演变成如下构造:
public void execute() {System.out.println("done");
}
这就是为什么,通常不倡议间接调用log.debug()
,而要先判断的起因。
2.2 Server complier
又称 C2,较 C1 更为重量级,C2 更多在于全局优化,而非代码块的优化。
逃逸剖析
逃逸剖析指的是依据运行状况来判断办法中变量是否会被办法内部读取,如果被内部读取,则认为是逃逸的。
如果通过命令-XX:+DoEscapeAnalysis
(默认为 true)开启逃逸剖析,server 编译器会执行较为激进的优化措施。
2.2.1 标量替换
Point point = new Point(1, 2);
System.out.println("point.x =" + point.x + "; point.y" + point.y);
当 point 对象在前面执行过程中未被应用到时,代码通过编译会演变为如下构造:
int x = 1, y = 2;
System.out.println("point.x =" + x + "; point.y" + y);
2.2.2 栈上调配
在下面的例子中,如果 point
没有逃逸,那么 C2 会抉择在栈上间接创立 point
对象,而非堆上。
在栈上调配的益处一方面是对象创立更加疾速,另一方面是回收时随着办法的完结,对象也被回收了。
2.2.3 同步削除
Point point = new Point(1, 2);
synchronized(point) {System.out.println("point.x =" + point.x);
}
通过剖析如果发现 point
未逃逸,则代码会在编译后变成如下构造:
Point point = new Point(1, 2);
System.out.println("point.x =" + point.x);
2.3 OSR(On Stack Replace,栈上替换)
OSR 和 C1、C2 次要不同在于,OSR 仅仅替换 循环代码体的入口,而 C1、C2 替换的是办法调用的入口。
因而在 OSR 编译后会呈现的景象是,办法的整段代码被编译了,但只有在循环代码体局部才执行编译后的机器码,而其余局部依然是解释执行形式。
如果对办法进行编译优化,等 JVM 在某个办法中发现这个办法很热,须要编译,那么只有下次调用这个办法能力享受到被优化后的代码,而本次调用仍旧应用优化前的代码。OSR 次要就是解决这个问题,比方 JVM 发现办法中这个循环过热,那么仅仅编译这个循环体就好了,执行引擎也会在进入下一个循环时跳转到新编译的代码中去。
作者:京东科技 康志兴
起源:京东云开发者社区 转载请注明起源