从原理聊 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发现方法中这个循环过热,那么仅仅编译这个循环体就好了,执行引擎也会在进入下一个循环时跳转到新编译的代码中去。
作者:京东科技 康志兴
来源:京东云开发者社区 转载请注明来源
从原理聊 JVM(五):JVM 的编译过程和优化手段的更多相关文章
- 插件开发遇到的坑------final 型变量,编译过程被优化
android 插件开发遇到的坑 今天遇到一个坑,pdf 插件,调用了主工程的一个静态final 字符串,但是主工程里面已经没有这个字符串了,却没有崩溃. 后来同事说,因为字符串可能已经直接被写死了. ...
- 深入理解JVM(五)JVM优化策略
5.2一些案例: 1.高性能硬件部署策略: (1)背景:某公司升级了硬件(CPU升级为4核,内存增加为16G),发现不定期出现网页失去响应. (2)原因:①内存增加之后,项目中有在内存中处理文件的大对 ...
- 从原理聊JVM(一):染色标记和垃圾回收算法
作者:京东科技 康志兴 1 JVM运行时内存划分 1.1 运行时数据区域 • 方法区 属于共享内存区域,存储已被虚拟机加载的类信息.常量.静态变量.即时编译器编译后的代码等数据.运行时常量池,属于方法 ...
- 从原理聊JVM(二):从串行收集器到分区收集开创者G1
作者:京东科技 康志兴 1 前言 随着Java的进化过程,涌现出各种不同的垃圾回收器,从串行执行到并行执行,从高吞吐到低延迟,终极目标就是让开发人员专注于程序的代码书写而无需关注内存管理. JDK早期 ...
- 从原理聊JVM(三):详解现代垃圾回收器Shenandoah和ZGC
作者:京东科技 康志兴 Shenandoah Shenandoah一词来自于印第安语,十九世纪四十年代有一首著名的航海歌曲在水手中广为流传,讲述一位年轻富商爱上印第安酋长Shenandoah的女儿的故 ...
- JVM系列五:JVM监测&工具
JVM系列五:JVM监测&工具[整理中] http://www.cnblogs.com/redcreen/archive/2011/05/09/2040977.html 前几篇篇文章介绍了介 ...
- JVM运行原理及Stack和Heap的实现过程
Java语言写的源程序通过Java编译器,编译成与平台无关的‘字节码程序’(.class文件,也就是0,1二进制程序),然后在OS之上的Java解释器中解释执行,而JVM是java的核心和基础,在ja ...
- 《Java虚拟机原理图解》4.JVM机器指令集
0. 前言 Java虚拟机和真实的计算机一样,执行的都是二进制的机器码:而我们将.java 源码编译成.class 文件,class文件便是Java虚拟机可以认识的二进制机器码,Java可以识别cla ...
- JVM(五):探究类加载过程-上
JVM(五):探究类加载过程-上 本文我们来研究一个Java字节码文件(Class文件)是如何加载入内存中的,在這個过程中涉及类加载过程中的加载,验证,准备,解析(连接),初始化,使用,销毁过程,并探 ...
- JVM系列之二:编译过程
1. Java的编译和执行 编译包括两种情况: 1,源码编译成字节码2,字节码编译成本地机器码(符合本地系统专属的指令) 解释执行也包括两种情况: 1,源码解释执行2,字节码解释执行 解释和编译执行的 ...
随机推荐
- ERRORS: app1.Book.photo: (fields.E210) Cannot use ImageField because Pillow is not installed.
报错: (env) E:\pyAPP\mybook>python manage.py makemigrations SystemCheckError: System check identifi ...
- django--循环调用的解决办法
不要导入该APP的包,而是加上双引号写入"App.xxx"
- linux 引导过程和服务控制
目录 一.引导分区 二.服务控制 三.运行级别 四.systemd初始化 五.模拟错误 一.引导分区 原理:引导分区是指在开机启动到进入系统这之间的过程 引导分区的过程:1.开机自检 自检顺序:BIO ...
- Springboot 开启异步任务Async,邮件发送任务,定时任务
异步任务 1.主启动类开启异步注解 2.service目录下开启异步任务注解 @Service public class AsyncService { @Async//异步任务注解的标志 public ...
- 使用a标签下载**.txt文件, 而不是直接打开
今天有个使用a标签下载一个 .txt 文件,但是使用了不少方法,在点击下载的时候总是会直接打开被下载的文件,但是下载其他格式的文件就不会:也在网上找了不少资料 一.尝试href + download方 ...
- 一文教会你用Apache SeaTunnel Zeta离线把数据从MySQL同步到StarRocks
在上一篇文章中,我们介绍了如何下载安装部署SeaTunnel Zeta服务(3分钟部署SeaTunnel Zeta单节点Standalone模式环境),接下来我们介绍一下SeaTunnel支持的第一个 ...
- odoo开发教程十:Actions
actions定义了系统对于用户的操作的响应:登录.按钮.选择项目等. 一:窗口action(ir.actions.act_window ) 最常用的action类型,用于将model的数据展示出来. ...
- Java工具类Result<T>
枚举类:ResultCodeEnum /** * 统一返回结果状态信息类 * */ @Getter public enum ResultCodeEnum { SUCCESS(200,"成功& ...
- C++面试八股文:C和C++有哪些区别?
某日小二参加XXX科技公司的C++高级工程师开发岗位1面: 面试官:请问C和C++的区别有哪些? 小二:C++是C的超集. 面试官:还有吗? 小二:... 面试官:面试结束,回去等消息吧. 小二:淦. ...
- 9.3. Hibernate框架
Hibernate是一个开源的持久层框架,它可以帮助我们将Java对象映射到数据库表中,并实现对象的持久化操作.Hibernate提供了丰富的API,可以方便地进行CRUD(增删改查)操作,而无需手动 ...