谈即时编译优化-以异常堆栈丢失为例
前言
日照充足会让西瓜更甜,那拥有即时编译优化会让Java程序怎么样?本文会初步介绍JVM的即时编译优化特性,并且通过异常堆栈丢失这一常见的现象来进行举例
即时编译优化
Java程序在运行初期是通过解释器来执行,当发现某块代码运行特别频繁,就会将之判定为热点代码(Hot Spot Code), 虚拟机会将这部分代码编译成本地机器码,并对这些代码进行优化。这件事就是即时编译(Just In Time, JIT)优化, 做这件事的就是即时编译器。
解释器与编译器
目前主流虚拟机都采用解释器、编译器并存的架构。
- 解释器:程序执行初期,解释器执行的方式可以省去编译过程,节省时间
- 编译器:在渡过初期后,编译器把更多的代码编译成本地代码,提升执行效率,以空间换时间
因为编译器存在过度优化,基于假设优化等失败的优化结果,通过逆优化(Deoptimization)的方式,将程序的执行主动权从编译器交给解释器执行。可以把解释器看成是一个保守派,编译器是一个激进派,在JVM执行体系里,两者相辅相成,互相配合。
编译器种类
一般虚拟机都内置了两个或三个即时编译器,历史比较久远的C1, C2, 以及在JDK10才出现的Graal
-
C1:客户端编译器(Client Complier),执行时间较短,启动程序的时间较快。在一些物联网小型设备上可指定这种编译器,通过-client参数强制指定
-
C2:服务端编译器(Server Complier),执行时间较长,启动时间较长但可编译高度优化的代码,峰值性能更高。可通过-server参数强制指定
-
Graal:是一个实验性质的即时编译器,其最大的特点是该编译器用Java语言编写,更加模块化,也更容易开发与维护。充分预热后Java代码编译成二进制码后其执行性能并不亚于由C++编写的C2。可以通过参数 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 启用,并替换 C2
分层编译优化
虽然可以通过-Xint参数强制虚拟机处于"解释模式"此时编译器不工作,可以通过-Xcomp参数强制虚拟机处于"编译模式"此时解释器不工作,可以通过-client参数使C2不工作,也可以通过-server参数使C1不工作,但是并不推荐这样做,因为有分层编译优化这一特性。
编译器在编译代码的时候会占用程序运行时间,优化程度越高的代码编译时间会越长,甚至会需要解释器负责收集程序运行监控信息提供给编译器来编译优化程度更高的代码。所以为了在更短的时间内编译优化程度更高的代码,需要编译器之间的配合,也就是所谓的分层编译优化。一共有五层,分别是:
-
纯解释执行,解释器不开启收集程序运行监控信息
-
使用C1编译器进行简单可靠的优化,解释器不开启收集程序运行监控信息
-
仍然使用C1编译器优化,但是会针对方法调用次数和回边次数(循环代码调用次数)相关的统计
-
仍然使用C1编译器优化,统计信息才上一层的基础上会加上分支跳转、虚方法调用等全部统计信息,解释器火力全开
-
使用C2编译器优化,相比C1,C2会开启更多耗时更长的优化,还会根据解释器提供的程序运行信息进行一些更为激进的优化
在开启编译优化后,热点代码可能会被重复编译,C1编译器编译得更快,C2编译器编译质量更高,第0层模式解释器执行的时候也不用收集监控信息,第4层模式C2在进行耗时较长的编译较为忙碌时候,C1也能为C2承担一部分编译工作,交互关系如下图
- common是针对大部分代码的编译情况,trival method针对执行次数较少的代码
- trival method很少被执行所以没有被C2编译的必要,通过第4层模式的优化就足够了
- 在C1忙碌的时候,会直接由C2编译;C2忙碌的时候,在C1编译的路径也会更长
编译触发条件
上面提到即使编译是针对热点代码进行编译优化,那么什么是热点代码?
- 被多次调用的方法
- 被多次执行的循环代码体
这里的多次如何知道具体有多少次?有两种方法可以知道
- 基于采样的热点探测(Sample Based Hot Spot Code Detection): 虚拟机周期性地检查各个线程的调用栈顶,如果发现某个方法经常出现在栈顶,那么这个方法就是热点方法,这种方法简单高效但是精确度不高
- 基于计数器的热点探测(Counter Based Hot Spot Code Dection): 虚拟机为每个方法建立计数器,计数器超过一定阈值就是热点方法
目前HotSpot虚拟机使用的是第二种方法,虚拟机为每个方法都准备了两类计数器,方法调用计数器以及回边计数器(回边的意思是在循环的末尾边界往回跳转,可以理解为循环代码的一次执行)
讲到这里给大家举一个工作中经常见到的一个JIT优化案例:异常堆栈丢失
异常堆栈丢失
问题
众所周知在打印Java异常的时候,会将其堆栈信息一并输出,这些堆栈信息非常重要,有助于我们排查问题,像这样
20:10:50.491 [main] ERROR com.yangkw.ErrorTest java.lang.NullPointerException: null at com.yangkw.ErrorTest.error(ErrorTest.java:33) at com.yangkw.ErrorTest.main(ErrorTest.java:19)
但是在最近在观察系统的线上运行日志的时候,发现了很多不带堆栈的异常日志,让人摸不着头脑到底发生了什么,像这样
20:10:50.491 [main] ERROR com.yangkw.ErrorTest java.lang.NullPointerException: null
猜想
通过前面关于JIT编译触发条件的介绍,可以设想是抛出异常执行太频繁所以触发了JIT优化导致,于是我们可以写一个Demo来验证,堆栈完整的时候打印"full trace",堆栈丢失的时候打印"no trace"
public static void main(String[] args) throws InterruptedException { int count = 0; while (true) { try { count++; //统计调用次数 error(); } catch (Exception e) { if (e.getStackTrace().length == 0) { LOG.error("no trace count:{}", count, e); Thread.sleep(1000); //方便观察日志 } else { LOG.error("full trace count:{}", count, e); } } } } private static void error() { String nullMsg = null; nullMsg.toString(); }
下面是执行结果,可以看出程序是在执行到8405次(每次执行都会不同)的时候丢失了堆栈
验证
虽然8405次执行的时候丢失了堆栈,但是并不能说明是因为JIT优化导致的,于是我们可以加上参数-XX:+PrintCompilation 来打印即时编译情况。
可以看到,在10388次执行的时候是有堆栈信息的,在10389次执行的时候就丢失了堆栈信息,在这中间就发生了即使编译优化,针对这一现象官方术语称之为"fast throw"可以通过参数-XX:-OmitStackTraceInFastThrow关闭这一优化
在ORACLE官方文档有这么一段描述
The compiler in the server VM now provides correct stack backtraces for all "cold" built-in exceptions. For performance purposes, when such an exception is thrown a few times, the method may be recompiled. After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. To disable completely the use of preallocated exceptions, use this new flag: -XX:-OmitStackTraceInFastThrow.
堆栈丢失只是表面现象,JIT还对其做了以下优化:
- 创建需要抛出异常的实例
- 清空堆栈信息
- 将该实例缓存起来
- 之后再需要抛出的时候,将缓存实例抛出去
总结
- 解释器、C1编译器、C2编译器各有优劣,合理搭配,干活不累
- -XX:-OmitStackTraceInFastThrow 谨慎使用,如果关闭fast throw的优化应预防"日志风暴"使磁盘空间迅速被打满
- 做好历史日志的记录以及备份,笔者通过回查历史日志成功追回了异常的堆栈信息
- 日照充足的西瓜会更甜,拥有即时编译优化会让Java程序程序更灵性
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
微软改名部又惹祸,
几年前,微软发布了一个名为"GVFS"的项目,这是一个 Git 虚拟文件系统,全称为 Git Virtual File System,GVFS 是它的首字母缩写。然而这个名字却引起了巨大的争议,因为这与 GNOME 项目的虚拟文件系统 GVfs 发生了同名冲突,不仅导致用户在查找信息时容易出现混淆,还严重影响了后者的 SEO。最后的结果是微软在各种压力之下选择妥协,将 "GVFS" 项目的名字改为 "VFS For Git"。 现在,微软和 KDE 社区之间也出现了类似的情况。 微软在 Build 2020 大会上宣布了MAUI 项目(.NET Multi-platform App UI),此项目旨在在单一的代码库中提供跨平台的原生 UI,目标是希望.NET MAUI 成为.NET 6 的一部分并作为 Xamarin Forms 的进化版。微软还计划在未来将基于 Xamarin.Forms 的现代项目从 Xamarin.Forms 转移到 .NET MAUI。 因此可以认为,微软将Xamarin Forms 改名为 MAUI。 KDE 社区也有一个创建已有两年名为"Maui"的项目,这...
- 下一篇
Knife4j 2.0.3 正式发布,支持 springdoc 和 i18n
Knife4j前身是swagger-bootstrap-ui,是一个为Swagger接口文档赋能的工具 文档:https://doc.xiaominfo.com 效果(旧版):http://swagger-bootstrap-ui.xiaominfo.com/doc.html 效果(2.X版):http://knife4j.xiaominfo.com/doc.html Gitee:https://gitee.com/xiaoym/knife4j GitHub:https://github.com/xiaoymin/swagger-bootstrap-ui 示例:https://gitee.com/xiaoym/swagger-bootstrap-ui-demo 特性 & 优化 knife4j 1、读取Markdown文件时,当文件不存在时日志错误信息简化打印,开发者可以忽略该错误gitee #I1E1S1 knife4j-spring-ui 1、移除Vue中的pwa机制,解决service-work.js引起的各种问题github #206 2、支持UiConfiguratio...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Red5直播服务器,属于Java语言的直播服务器
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- Eclipse初始化配置,告别卡顿、闪退、编译时间过长
- CentOS6,CentOS7官方镜像安装Oracle11G
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2全家桶,快速入门学习开发网站教程
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- CentOS8编译安装MySQL8.0.19