从原理聊JVM(五):JVM的编译过程和优化手段 | 京东云技术团队
一、前端编译
前端编译就是将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发现方法中这个循环过热,那么仅仅编译这个循环体就好了,执行引擎也会在进入下一个循环时跳转到新编译的代码中去。
作者:京东科技 康志兴
来源:京东云开发者社区 转载请注明来源

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
四层负载均衡的NAT模型与DR模型推导 | 京东物流技术团队
导读 本文首先讲述四层负载均衡技术的特点,然后通过提问的方式推导出四层负载均衡器的NAT模型和DR模型的工作原理。通过本文可以了解到四层负载均衡的技术特点、NAT模型和DR模型的工作原理、以及NAT模型和DR模型的优缺点。读者可以重点关注NAT模型到DR模型演进的原因(一种技术的诞生肯定是为了弥补现有技术的不足)。除此之外,读者可以多多关注一些基本的、底层的知识,比如内核空间、用户空间、计算机网络等。 为了叙述方便,文中将“四层负载均衡器” 简称为“FLB” (Four-tier Load Balancer)。 一、FLB在网络中的基本拓扑 FLB工作在OSI七层网络参考模型的第四层(传输控制层),FLB上必须具备两个IP地址,VIP和DIP。VIP是暴露给客户端的访问地址;DIP是FLB的分发IP,将数据包通过DIP所在的网卡发送给后端的真实提供服务的服务器(后面简称“RS”(Real Server)),如下图。 图1 FLB的基本网络拓扑图 其中CIP为客户端的ip,RIP为RS的ip。 二、四层负载均衡技术的特点 由于FLB工作在传输控制层,因此它对数据包的处理(转发)总是运行在...
- 下一篇
【深入浅出系列】之代码可读性 | 京东云技术团队
这是“深入浅出系列”文章的第一篇,主要记录和分享程序设计的一些思想和方法论,如果读者觉得所有受用,还请“一键三连”,这是对我最大的鼓励。 一、老生常谈,到底啥是可读性 一句话:见名知其义。有人说好的代码必然有清晰完整的注释,我不否认;也有人说代码即注释,是代码简洁之道的最高境界,我也不否认。但我都不完全接受,如果照搬前者,有人会在每个方法、每个循环、每个判断都添加大量注释,对于一个表达不严谨的coder来说,代码与汉字可能词不达意;而且,一旦代码逻辑发生变化,注释改不改?对于后者,英语水平可能也就是个半吊子,动词名词不区分,真能做到代码即注释的有多少人? 二、骂归骂,总归要硬着头皮干 先来举个简单例子: public StepExitEnum doExecute(StepContext stepContext) throws Exception { String targetFilePath = this.getOriginFilePath(stepContext.getJobContext());//获取目标路径 File targetDir = new File(targetF...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- 2048小游戏-低调大师作品
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS8编译安装MySQL8.0.19
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Red5直播服务器,属于Java语言的直播服务器