• [毕昇JDK] 【技术剖析】13. PriorityBlockingQueue比较器异常导致的NPE问题分析
    ## 作者:谢照昆、王嘉伟 > 编者按:笔者在使用PriorityBlockingQueue实现按照优先级处理任务时遇到一类NPE问题,经过分析发现根本原因是在任务出队列时调用比较器异常,进而导致后续任务出队列抛出NullPointerException。本文通过完整的案例复现来演示在什么情况会触发该问题,同时给出了处理建议。希望读者在编程时加以借鉴,避免再次遇到此类问题。 ## 背景知识 PriorityBlockingQueue是一个无界的基于数组的优先级阻塞队列,使用一个全局ReentrantLock来控制某一时刻只有一个线程可以进行元素出队和入队操作,并且每次出队都返回优先级别最高的或者最低的元素。PriorityBlockingQueue通过以下两种方式实现元素优先级排序: 1. 入队元素实现Comparable接口来比较元素优先级; 2. PriorityBlockingQueue构造函数指定Comparator来比较元素优先级; 关于PriorityBlockingQueue中队列操作的部分,基本和PriorityQueue逻辑一致,只不过在操作时加锁了。在本文中我们主要关注PriorityBlockingQueue出队的take方法,该方法通过调用dequeue方法将元素出队列。当没有元素可以出队的时候,线程就会阻塞等待。 ```java public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); E result; try { // 尝试获取最小元素,即小顶堆第一个元素,然后重新排序,如果不存在表示队列暂无元素,进行阻塞等待。 while ( (result = dequeue()) == null) notEmpty.await(); } finally { lock.unlock(); } return result; } ``` ## 现象 在某个业务服务中使用PriorityBlockingQueue实现按照优先级处理任务,某一天环境中的服务突然间不处理任务了,查看后台日志,发现一直抛出NullPointerException。将进程堆dump出来,使用MAT发现某个PriorityBlockingQueue中的size值比实际元素个数多1个(入队时已经对任务进行非空校验)。 异常堆栈如下: ``` java.lang.NullPointerException at java.util.concurrent.PriorityBlockingQueue.siftDownComparable(PriorityBlockingQueue.java:404) at java.util.concurrent.PriorityBlockingQueue.dequeue(PriorityBlockingQueue.java:333) at java.util.concurrent.PriorityBlockingQueue.take(PriorityBlockingQueue.java:548) ... ``` MAT结果: ![](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20225/6/1651803610401641713.png) ## 原因分析 在此我们分析下PriorityBlockingQueue是如何出队列的,PriorityBlockingQueue最终通过调用dequeue方法出队列,dequeue方法处理逻辑如下: 1. 将根节点(array[0])赋值给result; 2. array[n] 赋值给 arrary[0]; 3. 将 array[n] 设置为 null; 4. 调用siftDownComparable或siftDownUsingComparator对队列元素重新排序; 5. size大小减1; 6. 返回result; 如果在第4步中出现异常,就会出现队列中的元素个数比实际的元素个数多1个的现象。此时size未发生改变,arry[n]已经被置为null,再进行siftDown操作时就会抛出NullPointerException。继续分析第4步中在什么情况下会出现异常,通过代码走读我们可以发现只有在调用Comparable#compareTo或者Comparator#compare方法进行元素比较的时候才可能出现异常。这块代码的处理逻辑和业务相关,如果业务代码处理不当抛出异常,就会导致上述现象。 ```java /** * Mechanics for poll(). Call only while holding lock. */ private E dequeue() { int n = size - 1; if (n < 0) return null; else { Object[] array = queue; E result = (E) array[0]; //step1 E x = (E) array[n]; //step2 array[n] = null; //step3 Comparator<? super E> cmp = comparator; if (cmp == null) //step4 如果指定了comparator,就按照指定的comparator来比较。否则就按照默认的 siftDownComparable(0, x, array, n); else siftDownUsingComparator(0, x, array, n, cmp); size = n; //step5 return result; //step6 } } private static <T> void siftDownComparable(int k, T x, Object[] array, int n) { if (n > 0) { Comparable<? super T> key = (Comparable<? super T>)x; int half = n >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = array[child]; int right = child + 1; if (right < n && ((Comparable<? super T>) c).compareTo((T) array[right]) > 0) c = array[child = right]; if (key.compareTo((T) c) <= 0) break; array[k] = c; k = child; } array[k] = key; } } private static <T> void siftDownUsingComparator(int k, T x, Object[] array, int n, Comparator<? super T> cmp) { if (n > 0) { int half = n >>> 1; while (k < half) { int child = (k << 1) + 1; Object c = array[child]; int right = child + 1; if (right < n && cmp.compare((T) c, (T) array[right]) > 0) c = array[child = right]; if (cmp.compare(x, (T) c) <= 0) break; array[k] = c; k = child; } array[k] = x; } } ``` ## 复现代码 ```java import java.util.ArrayList; import java.util.List; import java.util.concurrent.PriorityBlockingQueue; public class PriorityBlockingQueueTest { static class Entity implements Comparable<Entity> { private int id; private String name; private boolean flag; public void setFlag(boolean flag) { this.flag = flag; } public Entity(int id, String name) { this.id = id; this.name = name; } @Override public int compareTo(Entity entity) { if(flag) { throw new RuntimeException("Test Exception"); } if (entity == null || this.id > entity.id) { return 1; } return this.id == entity.id ? 0 : -1; } } public static void main(String[] args) { int num = 5; PriorityBlockingQueue<Entity> priorityBlockingQueue = new PriorityBlockingQueue<>(); List<Entity> entities = new ArrayList<>(); for (int i = 0; i < num; i++) { Entity entity = new Entity(i, "entity" + i); entities.add(entity); priorityBlockingQueue.offer(entity); } entities.get(num - 1).setFlag(true); int size = entities.size(); for (int i = 0; i < size; i++) { try { priorityBlockingQueue.take(); } catch (Exception e) { e.printStackTrace(); } } } ``` 执行结果如下: ```java java.lang.RuntimeException: Test Exception at PriorityBlockingQueueTest$Entity.compareTo(PriorityBlockingQueueTest.java:31) at PriorityBlockingQueueTest$Entity.compareTo(PriorityBlockingQueueTest.java:8) at java.util.concurrent.PriorityBlockingQueue.siftDownComparable(PriorityBlockingQueue.java:404) at java.util.concurrent.PriorityBlockingQueue.dequeue(PriorityBlockingQueue.java:333) at java.util.concurrent.PriorityBlockingQueue.take(PriorityBlockingQueue.java:548) at PriorityBlockingQueueTest.main(PriorityBlockingQueueTest.java:71) java.lang.NullPointerException at java.util.concurrent.PriorityBlockingQueue.siftDownComparable(PriorityBlockingQueue.java:404) at java.util.concurrent.PriorityBlockingQueue.dequeue(PriorityBlockingQueue.java:333) at java.util.concurrent.PriorityBlockingQueue.take(PriorityBlockingQueue.java:548) at PriorityBlockingQueueTest.main(PriorityBlockingQueueTest.java:71) ``` ## 规避方案 可以通过以下两种方法规避: - 在take方法出现NPE时,清除队列元素,将未处理的元素重新进入队列; - 在 Comparable#compareTo 或 Comparator#compare 方法中做好异常处理,对异常情况进行默认操作; 建议使用后者。 ## 案例引申 使用PriorityBlockingQueue作为缓存队列来创建线程池时,使用submit提交任务会出现 java.lang.ClassCastException: java.util.concurrent.FutureTask cannot be cast to 异常,而使用execute没有问题。 观察submit源码可以发现在submit内部代码会将Runable封装成RunnableFuture对象,然后调用execute提交任务。 ```java public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<Void> ftask = newTaskFor(task, null); execute(ftask); return ftask; } ``` 以Comparable为例,任务入队列时,最终会调用siftUpComparable方法。该方法第一步将RunnableFuture强转为Comparable类型,而RunnableFuture类未实现Comparable接口,进而抛出ClassCastException异常。 ``` java public boolean offer(E e) { if (e == null) throw new NullPointerException(); final ReentrantLock lock = this.lock; lock.lock(); int n, cap; Object[] array; while ((n = size) >= (cap = (array = queue).length)) tryGrow(array, cap); try { Comparator<? super E> cmp = comparator; if (cmp == null) siftUpComparable(n, e, array); else siftUpUsingComparator(n, e, array, cmp); size = n + 1; notEmpty.signal(); } finally { lock.unlock(); } return true; } private static <T> void siftUpComparable(int k, T x, Object[] array) { Comparable<? super T> key = (Comparable<? super T>) x; while (k > 0) { int parent = (k - 1) >>> 1; Object e = array[parent]; if (key.compareTo((T) e) >= 0) break; array[k] = e; k = parent; } array[k] = key; } ``` 这也是常见的比较器调用异常案例,本文不再赘述,可自行参考其他文章。 ## 总结 在使用PriorityBlockingQueue时,注意在比较器中做好异常处理,避免出现类似问题。 ## 后记 如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 社区查找相关资源(点击阅读原文进入官网),包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。 ![](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20225/6/1651803913842455839.jpg) ------------ 原文转载自毕昇编译-[PriorityBlockingQueue比较器异常导致的NPE问题分析](https://mp.weixin.qq.com/s?__biz=MzkyNTMwMjI2Mw==&mid=2247484624&idx=1&sn=1386b641924946224560d0fd36407c27&chksm=c1c9e848f6be615e23ffaaeac2830f66539b01511549fe073e73b65d22fa57622470c90ceaec&token=776677221〈=zh_CN#rd) 关注毕昇编译获取编译技术更多信息 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/20225/6/1651803871554528113.png)
  • [讨论交流] 4月14日 Compiler SIG ODD 2022 版本规划会议回顾
    议题一:GCC、LLVM选型介绍和未来规划1.GCC/LLVM版本节奏openEuler GCC 每两年发一个版本,每次选择x.2.y以后的版本openEuler LLVM 每两年发布一个新版本,每次选择x.0.1以后的版本2.GCC/LLVM最新特性介绍openEuler GCC 引入Autoprefetch、AutoBolt、AutoFDO等优化特性openEuler yum源“yum install bisheng-compiler”引入毕昇编译器供开发者安装使用3.其他讨论     需求收集:openEuler发行版中多版本GCC支持、上游社区LTO+控制流优化支持。议题二:Bisheng JDK选型介绍和未来规划1.生命周期、发布节奏、roadmap毕昇JDK提供8、11、17三个版本二进制,其中8支持至2026.12,11支持至2024.12,17支持至2027.12毕昇JDK会在openjdk社区ga后两个月发布,一般在月底,即3.30、6.30、9.30、12.30发布毕昇JDK8预计在6.30合入dynamic cds特性(可能会推迟)2.毕昇JDK最近特性介绍     主要包括支持国密的BGMProvider,提供OpenJFX源码及构建方式,RISC-V后端合入openjdk19等。3.其他讨论需求收集:与会人员希望能够将BGMProvider合到上游社区,但由于底层算法采用了三方BC库,导致很难推进后续计划:ARM曾经在openssl社区实现过底层算法,后续可以考虑在openjdk社区实现,届时可以一起将BGMProvider推送至上游社区,相关信息在线下讨论,感兴趣的同学可以加入Compiler SIG微信群讨论。是否可以考虑将软件包构建重构?将底层算法依赖做成动态拉取和配置?议题三:Compiler SIG可持续发展讨论和开放讨论1.开源策略说明:明确Compiler SIG 在开源过程中upstream first的原则,只有上游社区不容易接受的特性、且对用户有价值的会整合到openEuler中。2.目前openEuler支持或者正在支持X86、ARM、龙芯、申威、RV架构,有专门的Intel、申威和Power SIG。Compiler后续需要和相关各个SIG合作,保证新特性和稳定性的平衡。扫码添加 SIG 小助手微信,邀请你进 Compiler SIG 微信交流群。原文转载自毕昇编译-Compiler SIG ODD 2022 版本规划会议回顾关注毕昇编译获取编译技术更多信息
  • [问题求助] 安装MindStudio运行是报错,好像是JDK版本问题求助
    【功能模块】1.按照下面的链接步骤安装的JDK,但是安装JDK后还是报错。https://support.huaweicloud.com/usermanual-MindStudioC76/atlasms_02_0318.html【操作步骤&问题现象】1、2、【截图信息】【日志信息】(可选,上传日志内容或者附件)
  • [技术干货] IOT常用通讯方式(有线、无线)
    有线部分RS232通讯方式:全双工通讯(能同时发送和接受数据)通讯距离:标准值为50英尺,实际上也只能用在15米左右传输方式:异步传输传输速率:20Kbps说明:主流的串行通信接口之一,常用的有DB9和DB25两种,传送的数字量采用负逻辑,且与地对称(逻辑1:-3 ~-15V ,逻辑0:+3~+15V)。 只能用于点对点通讯。RS485通讯方式:半双工通讯(不能同时发送和接受数据)通讯距离:最远可以达到1200米左右,但是它的传输速率和传输距离是成反比的,只有在100Kb/s以下的传输速度,才能达到最大的通信距离,如果需要传输更远距离可以使用中继。最多可以加八个中继,也就是说理论上RS485的最大传输距离可以达到10.8公里传输方式:异步传输传输速率:10Mbps说明:主流的串行通信接口之一,并不需要相对于某个参照点来检测信号,系统只需检测两线之间的电位差就可以。有极强的抗共模干扰的能力,逻辑“1”以两线间的电压差为+(2~6)V表示,逻辑“0”以两线间的电压差为-(2~6)V表示。支持多点数据通信,网络拓扑一般采用终端匹配的总线型结构即采用一条总线将各个节点串接起来,不支持环形或星型网络。最大支持32个节点。RS422通讯方式:全双工通讯(能同时发送和接受数据)通讯距离:1200米左右传输方式:异步传输传输速率:10Mbps说明:采用单独的发送和接收通道,因此不必控制数据方向。支持多点数据通信,最大支持10个节点。其他电气特性同RS485类似。M-BUS通讯方式:半双工通讯(主从式)通讯距离:最远3600米左右传输方式:异步传输传输速率:5kb/s左右说明:采用主叫/应答的方式通信,即只有处于中心地位的主站(Master)发出询问后,从站(Slave)才能向主站传输数据。总线型拓扑结构。主站向从站发送逻辑“1”时,总线电压Vmark≤42V,发送逻辑“0”时,电压下降10V以上,降到Vspace≥12V;从站向主站发送逻辑“1”时,从站所取电流为Imark≤1.5mA,发送逻辑“0”时,从站的会在Imark上加上脉冲电流11-20mA,形成Ispace。PLC(电力载波)通讯方式:全双工通讯通讯距离:不稳定传输方式:同步传输传输速率:根据不同方案差别较大说明:电力线载波通信是以高频载波信号通过高压或低压电力线传输信息的通信方式。电力线对载波信号造成高削减。当电力线上负荷很重时,线路阻抗可达1欧姆以下,造成对载波信号的高削减。实际应用中,当电力线空载时,点对点载波信号可传输到几公里。但当电力线上负荷很重时,只能传输几十米。无线部分Zigbee通讯频段:免执照频段。使用工业科学医疗(ISM)频段,915MHz(美国), 868MHz(欧洲), 2. 4GHz(全球)通讯距离:传输范围一般介于10~100m之间,在增加发射功率后,亦可增加到1~3km。这指的是相邻节点间的距离。如果通过路由和节点间通信的接力,传输距离将可以更远功耗:低传输速率:20~250kbps说明:近距离、高可靠、低复杂度、低功耗、低数据速率、低成本的无线网络技术,主要用于近距离无线连接。在整个网络范围内,每一个ZigBee网络数传模块之间可以相互通信,每个ZigBee网络节点不仅本身可以作为监控对象,例如其所连接的传感器直接进行数据采集和监控,还可以自动中转别的网络节点传过来的数据资料。除此之外,每一个ZigBee网络节点还可在自己信号覆盖的范围内,和多个不承担网络信息中转任务的孤立的子节点无线连接。Bluetooth(蓝牙)通讯频段:免执照频段。工业、科学和医疗用(ISM)波段的 2.4 GHz 短距离无线电频段通讯距离:最大100m,范围取决于功率功耗:相比Zigbee来说比较大传输速率:蓝牙2.0的速度:1.8M/s,蓝牙3.0的速度:可达24M/s,蓝牙4.0的速度:24M/s说明:蓝牙使用跳频技术,将传输的数据分割成数据包,通过79个指定的蓝牙频道分别传输数据包。每个频道的频宽为1 MHz。蓝牙4.0使用2 MHz 间距,可容纳40个频道。蓝牙主设备最多可与一个微微网中的七个设备通讯, 设备之间可通过协议转换角色,从设备也可转换为主设备。WIFI通讯频段:免执照通讯距离:取决于路由器功耗:较大传输速率:54Mbps说明:无线网络在无线局域网的范畴是指“无线相容性认证”,实质上是一种商业认证,同时也是一种无线联网技术,常见的就是一个无线路由器,那么在这个无线路由器的电波覆盖的有效范围都可以采用Wi-Fi连接方式进行联网,如果无线路由器连接了一条ADSL线路或者别的上网线路,则又被称为热点。WIFI的数据安全性比较差。NB-IOT通讯频段:沿用LTE定义的频段号(根据运营商)通讯距离:15km功耗:低传输速率:大于160kbps,小于250kbps说明:NB-IoT构建于蜂窝网络,只消耗大约180KHz的带宽,可直接部署于GSM网络、UMTS网络或LTE网络。NB-IoT是IoT领域一个新兴的技术,支持低功耗设备在广域网的蜂窝数据连接,也被叫作低功耗广域网(LPWAN)。支持待机时间长、对网络连接要求较高设备的高效连接。同时还能提供非常全面的室内蜂窝数据连接覆盖。远距离、低功耗、半双工通讯是其特点。LoRa通讯频段:ISM 频段 包括433、868、915 MH等通讯距离:城镇2-5 km ,郊区15 km功耗:低传输速率:速率越快,传输距离会缩短,通常使用1.1kbps说明:LoRa由美国公司SEMTECH独家私有技术垄断,终端和网关芯片IP专利由SMETCH独家掌控,最大特点就是在同样的功耗条件下比其他无线方式传播的距离更远,实现了低功耗和远距离的统一,它在同样的功耗下比传统的无线射频通信距离扩大3-5倍。RFID通讯频段:低频(125KHz到135KHz),高频(13.56MHz)和超高频(860MHz到960MHz)之间通讯距离:几十米功耗:低传输速率:取决于代码的长度、载体数据发送速率、读写距离、载体与天线间载波频率,以及数据传输的调制技术等因素。传输速率随实际应用中产品种类的不同而不同。说明:无线射频识别技术(Radio Frequency Idenfication,即RFID)是一种非接触的自动识别技术。其基本原理是利用射频信号和空间耦合(电感或电磁耦合)或雷达反射的传输特性,实现对被识别物体的自动识别。NFC通讯频段:13.56MHz通讯距离:小于10cm功耗:低传输速率:取决于代码的长度、载体数据发送速率、读写距离、载体与天线间载波频率,以及数据传输的调制技术等因素。传输速率随实际应用中产品种类的不同而不同说明:NFC是在RFID的基础上发展而来,NFC从本质上与RFID没有太大区别,都是基于地理位置相近的两个物体之间的信号传输。NFC技术增加了点对点通信功能,可以快速建立蓝牙设备之间的P2P(点对点)无线通信,NFC设备彼此寻找对方并建立通信连接。NFC通信的双方设备是对等的,而RFID通信的双方设备是主从关系。NFC因为通讯距离端,因此安全性很高。4G通讯频段:根据运营商通讯距离:根据基站功耗:较高传输速率:100Mbps说明:包括TD-LTE和FDD-LTE两种制式。原贴链接:https://blog.csdn.net/weixin_33729196/article/details/89542035
  • [交流分享] 【鲲鹏热点问题,你问我答】第13期 毕昇JDK是什么?
    本期精彩看点:毕昇JDK是什么?鲲鹏编译器-毕昇JDK:https://www.hikunpeng.com/zh/developer/devkit/compiler/jdk上一篇:Hyper Tuner是什么?下一篇:如何快速进行鲲鹏应用迁移调优实验?
  • [毕昇JDK] 【技术剖析】系列文章汇总帖
    【技术剖析】1. 使用 perf 解决 JDK8 小版本升级后性能下降的问题【技术剖析】2. JVM 锁 bug 导致 G1 GC 挂起问题分析和解决【技术剖析】3. Java Flight Recorder - 事件机制详解【技术剖析】4. 一个 JVM 解释器 bug 在 AArch64 平台导致应用崩溃的问题分析【技术剖析】5. JDK 从8升级到11,使用 G1 GC,HBase 性能下降近20%。JDK 到底干了什么?【技术剖析】6. JNI 中错误的信号处理导致 JVM 崩溃问题分析【技术剖析】7. 看看毕昇 JDK 团队是如何解决 JVM 中 CMS 的 Crash【技术剖析】8. 相同版本 JVM 和 Java 应用,在 x86 和AArch64 平台性能相差30%,何故?【技术剖析】9. 使用 NMT 和 pmap 解决 JVM 资源泄漏问题【技术剖析】10. JVM 中不正确的类加载顺序导致应用运行异常问题分析【技术剖析】11. 使用jemalloc解决JVM内存泄露问题【技术剖析】12. 毕昇 JDK 8 中 AppCDS 实现介绍【技术剖析】13. PriorityBlockingQueue比较器异常导致的NPE问题分析如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 社区查找相关资源,包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。
  • [毕昇JDK] 【技术剖析】12. 毕昇 JDK 8 中 AppCDS 实现介绍
    ## 作者:伍家华 > 编者按:笔者通过在 Hive 的场景发现 AppCDS 技术存在的价值,然后分析了 AppCDS 的工作原理,并将 JDK 11 中的特性移植到毕昇 JDK 8,在移植过程中由于 JDK 8 和 JDK 11 在类加载实现有所不同,JDK 11 在加载过程中增强了安全性检查,为了达到相同的效果没有对 JDK 8 中的类加载进行修改,而是通过额外的安全检查保证共享类的安全性。最后笔者介绍了如何使用 AppCDS 以及使用的注意事项,希望对读者有所帮助。 基于某产品集群业务中有使用 Hive 场景,我们发现该集群在执行任务时会启动大量的 Java 进程,且进程很快就执行结束。对于这种情况一个有效的解决方法是让 Java 进程之间重用部分公共数据,从而减少 Java 进程产生公共数据的耗时。在 JDK 11 中支持 CDS 和 AppCDS,CDS 的全称是 Class-Data Sharing,CDS 的作用是可以让类被预处理放到一个归档文件中,后续 Java 程序启动的时候可以直接带上这个归档文件,这样 JVM 可以直接将这个归档文件映射到内存中,以节约应用启动的时间。AppCDS 是 CDS 的扩展,不止能够作用于 Boot Class Loader,App Class Loader 和自定义的 Class Loader 也都能够起作用,加大了 CDS 的适用范围 [1]。技术原理是让多个 Java 进程共享类的元数据,其基本原理如下所示: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/104410z4rnlch0colqsajm.png) 左右两边分别都是一个 Java 进程,双方通过共享内存达到节约内存、减少类解析的目的。下面通过案例介绍一下我们发现的现象,然后再介绍类共享机制的内部原理。 # 1. 技术背景 ## 1.1 环境介绍 Hive on tez 集群,4 个节点,一个管理节点,其他三个节点作为工作节点。软件版本:Hadoop 3.1.0,hive 3.1.2,tez-0.9.1,ambari2.6,TPC-DS 标准测试套。硬件环境:Kunpeng 920,128 核,512G 内存。 ## 1.2 系统负载情况 运行 TPC-DS 自带的 sql8,使用 top 观察其中的一个节点的工作负载,发现启动了大量的 JVM 进程。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/104450m7cjjd1bzhuryedr.png) ## 1.3 热点分布 同时使用 perf 采集整机的热点数据,发现 JVM 的 C1, C2 编译线程占比很高,如下图所示。因此判断 hive sql 的 startup 和 warmup 阶段占比比较高,可以考虑从这两个方向优化。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/104507wlgt4bw1hwq11tpg.png) 考虑到目前 JVM 的 C2 代码比较难维护,也很难引进新的 feature。因此考虑看看在启动阶段有没有优化的可能。jdk1.8 带有 cds 功能,能够 share jdk 自带的 java class,能够减少 JVM 的类加载时间,因此首先尝试使用默认的 cds 功能,测试发现效果不明显。原因是 hive sql 执行过程中加载的类大部分是应用程序 classpath 下的类,jdk 自身的类占比很小,因此收益不明显。考虑到 jdk10 以后有了 AppCDS 功能,能够 share classpath 下的 class,因此考虑在 jdk1.8 上面实现 AppCDS 功能。 # 2. AppCDS 实现 毕昇 JDK 支持 AppCDS,即在原生 CDS 的基础上也支持用户自己程序 classpath 中的类共享。原生 CDS 只支持由 `Bootstrap classloader` 和 `Ext Classloader` 加载的 class,即 jdk 自带的 class 在多进程之间共享。但是在实际场景中,尤其是在使用了大量开源软件的情况下,用户指定 classpath 下的 jar 远远大于 jdk 自带的 class,这样 CDS 的实际效果比较有限。 AppCDS 能够扩大 class 共享的范围,从而在节省内存使用以及 JVM 的启动时间两个方面进一步提升效果。 ## 2.1 create class list 文件 java class 文件被 JVM 执行之前首先要被加载进 JVM 内存里面,作为 meta data 存储在 JVM 的 metaspace 内存区域。其中一个关键步骤就是解析 class 文件。JVM 内部有个 ClassFileParser 类,提供了 parseClassFile 方法,解析成功之后会生成一个对应 Klass。AppCDS 的第一步逻辑就是在类被解析完成之后,如果 class 不是匿名类,且 class 文件的 major version 大于 JAVA_1_5 Version,则会把 class 对应的 qualified name 写去到 list 文件中去。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/104531nijzq6lsdx6btjih.png) ## 2.2 create jsa 归档文件 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/104544ihisa4ev0ni6ndj7.png) 在这一步 JVM 会按行读取 appcds.lst 文件中内容,然后使用 App ClassLoader 去加载对应的 class。由于 Java classloader 自身的 delegate 机制,能够确保 jdk 自身的 class 也能够得到加载。Class 被加载之后存在 JVM 的 metaspace 区域,这个 metaspace 是 JVM Native Memory 的一部分,里面包含 klass, ConstantPool, Annotation, ConstMethod, Method, MethodData 等 meta 信息。 当 lst 文件中对应的 class 都被 JVM 加载完成之后,JVM 会把其对应的 metadata 写入到从虚拟地址 0x800000000 开始的内存区域,其格式是 JVM 内部私有的一种表示,然后再把这部分数据 dump 到归档文件中。 ## 2.3 与 JDK 11 实现的差别 与 JDK 11 中的 AppCDS 实现差别主要体现在加载用户指定的 class 逻辑上。当加载用户 class 时,是需要计算获取一个 ProtectionDomain 来做安全验证的。JDK 11 是直接在 hotspot 中使用 JavaCalls 模块构造这个 ProtectionDomain,而毕昇 JDK 8 是在 Java 的 ClassLoader 中提供了一个 `getProtectionDomainInternal` 函数来获取,并在加载 class 的时候使用 JavaCalls 调用这个 java 方法。 # 3. AppCDS 使用说明 ## 3.1 创建 lst 文件 启动 JVM 的时候添加 `- Xshare:off` 和 `- XX:+DumpLoadedClassList=/path/to/class.lst` 这两个选项。这里比较重要的是第二个选项,这个选项同时支持 %P,可以按进程生成 class.lst 文件,生成的文件名会带上 JVM 进程的 pid。class.lst 本质上是个纯文本文件,里面记录 JVM 运行过程中加载的 class 列表。在 java 程序被 JVM 执行的过程中,会不断地从 classpath 中加载 class,并在 JVM 内部创建对应的 klass 对象。Klass 对象创建成功之后 JVM 会把 class 在 JVM 内部的 qualified name 写入到 class.lst 文件中,比如 `java.lang.Objectd` 对应的是 java/lang/Object。 **使用例子:** ```Bash java -Xms16M -Xmx16M -XX:+UseG1GC -cp test.jar -Xshare:off -XX:+UseAppCDS -XX:DumpLoadedClassList=appcds.lst com.example.App ``` ## 3.2 创建 archive 文件 第二步:创建 cds 归档文件。使用方式: ```Bash java -Xms16M -Xmx16M -XX:+UseG1GC -cp test.jar -Xshare:dump -XX:+UseAppCDS -XX:SharedClassListFile=appcds.lst -XX:SharedArchiveFile=appcds.jsa com.example.App ``` ## 3.3 使用 archive 文件 使用示例: ```Bash java -Xms16M -Xmx16M -XX:+UseG1GC -cp test.jar -Xshare:on -XX:+UseAppCDS -XX:SharedArchiveFile=appcds.jsa com.example.App ``` 第三步:JVM 在启动的过程中会把 jsa mmap 到内存中,然后在触发类加载时候 JVM 会首先尝试从这块 mmap 进来的内存中去获取 klass 对象,如果获取到则会更新 JVM 内部的 SystemDictionary,以后再来获取对应的 klass 对象则不需要从 jsa 中获取。如果获取失败,JVM 则会走正常的类加载流程,尝试从 classpath 中加载对应的 class 文件,解析,链接,初始化,然后再放入 SystemDictionary 中。 # 4. AppCDS 效果 TPC-DS 测试套的 10 个 sql 在 5T 数据的场景下测试了一下 AppCDS 的效果,结果收益好,最低是 8.42% 的性能提升,最高是 26.9% 的性能提升。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/104759jowrhg8agxnaksbg.png) # 5. 注意事项 使用 AppCDS 三部曲过程中要保持 JVM 版本、JAVA_HOME 路径一致,不然会校验失败,无法启动 JVM。 在第二步加载 class 并创建 jsa 归档文件的时候,JVM 会记录 class 的 fingerprint 信息 (相当于 hash 值),在第三步使用 jsa 中的 class 信息时会校验这个 fingerprint 信息,因此如果业务侧代码发生变更,要重新部署的话,jsa 归档文件要重新制作。 使用 AppCDS 或者 CDS 的时候不能 debug java 程序。因为 debug 的时候 jvmti 会修改 class 对应的内容,而根据第二点,必然会导致 fingerprint 不一致。 目前 AppCDS 并不支持 `Custom ClassLoader`,因此在 tomcat、jetty、SpringBoot 这些使用较多 `Custom ClassLoader` 场景的,整体收益跟原生 CDS 应该差不多,class 可以 share 的范围仅仅多了一些 Application 自身的 boot class。 # 后记 如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 社区查找相关资源(点击[阅读原文](https://mp.weixin.qq.com/s/QVljVWthj9mvAL0HAiWFcw)进入官网),包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。 ![](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/104833lyyanphull8o7upu.jpg) # 参考 [1] https://juejin.cn/post/6844903581246914574 原文转载自 openEuler-[毕昇 JDK 8 中 AppCDS 实现介绍](https://mp.weixin.qq.com/s/QVljVWthj9mvAL0HAiWFcw)
  • [毕昇JDK] 【技术剖析】11. 使用jemalloc解决JVM内存泄露问题
    ## 作者:王坤 > 编者按:JVM 发生内存泄漏,如何能快速定位到内存泄漏点并不容易。笔者通过使用 jemalloc(可以替换默认的 glibc 库)中的 profiling 机制(通过对程序的堆空间进行采样收集相关信息),演示了如何快速找到内存泄漏的过程。 Java 的内存对象一般可以分为堆内内存、堆外内存和 Native method 分配的内存,对于前面两种内存,可以通过 JVM 的 GC 进行管理,而 Native method 则不受 GC 管理,很容易引发内存泄露。Native Method 导致的内存泄漏,无法使用 JDK 自带的工具进行分析,需要通过 malloc_hook 追踪 malloc 的调用链帮助分析,一般可以采用内存分配跟踪工具(malloc tracing)协助分析内存泄漏。该工具的使用较复杂:需要修改源码,装载 hook 函数,然后运行修改后的程序,生成特殊的 log 文件,最后利用 mtrace 工具分析日志来确定是否存在内存泄露并分析可能发生内存泄露的代码位置。由于 hotspot 代码量较大,虽然可以通过一些选项逐步缩小可疑代码范围,但是修改代码总不是最优选择。另外,Valgrind 扫描也是一种常用的内存泄露分析工具,虽然 Valgrind 非常强大,但是 Valgrind 会报出很多干扰项,且使用较为麻烦。本文主要是介绍另一种分析 Native method 内存泄漏的方法,供大家参考。 jemalloc 是通过 malloc(3) 实现的一种分配器,代替 glibc 中的 malloc 实现,开发人员通过 jemalloc 的 Profiling 功能分析内存分配过程,可帮助解决一些 Native method 内存泄漏问题。 # 1 jemalloc 使用方法 jemalloc 使用方法的详细介绍,请参考本文附录章节。 # 2 使用 jemalloc 工具解决实际业务中遇到 Native method 内存泄漏问题 毕昇 JDK 某个版本内部迭代开发期间,在特性功能开发测试完毕后,进行 7*24 小时长稳测试时发现开启 `- XX:+UseG1GC` 选项会导致内存迅速增加,怀疑 JVM 层面存在内存泄露问题。 Java 例子参考(例子仅作为帮助问题理解使用): ```Java import java.util.LinkedHashMap; public class SystemGCTest { static int Xmx = 10; private static final int MB = 1024 * 1024; private static byte[] dummy; private static Integer[] funny; private static LinkedHashMap map = new LinkedHashMap(); public static void main(String[] args) { int loop = Integer.valueOf(args[0]); if (loop 0) { loop = loop * -1; while (true) { doGc(loop); map.clear(); System.gc(); } } else { doGc(loop); map.clear(); System.gc(); } } private static void doGc(int numberOfTimes) { final int objectSize = 128; final int maxObjectInYoung = (Xmx * MB) / objectSize; for (int i = 0; i numberOfTimes; i++) { for (int j = 0; j maxObjectInYoung + 1; j++) { dummy = new byte[objectSize]; funny = new Integer[objectSize / 16]; if (j % 10 == 0) { map.put(Integer.valueOf(j), funny); } } } } } ``` ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/103043nbueckzoob7wlrqw.png) 上图是开启 `- XX:+UseG1GC` 选项,Java 进程内存增长曲线图。横坐标是内存使用的统计次数,每 10 分钟统计一次;纵坐标是 Java 进程占用物理内存的大小。从上图可以看出:物理内存持续增涨的速度很快,存在内存泄露问题。 我们在设置了 jemalloc 的环境下,重新运行该测试用例: ```Bash java -Xms10M -Xmx10M -XX:+UseG1GC SystemGCTest 10000 ``` 注意:10000 与 jemalloc 无关,是 SystemGCTest 测试用例的参数,java 是疑似存在内存泄漏的 Java 二进制文件。 程序启动后,会在当前目录下逐渐生成一些 heap 文件,格式如:`jeprof.26205.0.i0.heap`。jeprof 工具的环境变量设置正确后(可参考本文附录),开发可以直接执行 jeprof 命令查看运行结果,了解 jeprof 的使用方式。jeprof 可基于 jemalloc 生成的内存 profile 堆文件,进行堆文件解析、分析并生成用户容易理解的文件,工具使用方便。 下面我们通过上述内存泄露问题,简单介绍 jeprof 工具的典型使用方法。 jeprof 工具可以生成内存申请代码的调用路径图。上述 Java 例子运行一段时间后会产生一些 heap 文件,jeprof 可帮助开发者获取有助于分析的可视化文件。 方法 1,通过使用 jeprof 工具将这些 heap 文件转成 svg 文件,命令如下: ```Bash jeprof --show_bytes --svg /home/xxxx/jdk1.8.0_292/bin/java jeprof*.heap > app-profiling.svg ``` 这里需要注意的是:`/home/xxxx/jdk1.8.0_292/bin/java` 必须是绝对路径。 注意:执行生成 svg 文件的命令时,部分环境会遇到类似如下错误: ```Bash Dropping nodes with = 2140452 B; edges with = 428090 abs(MB) dot: command not found ``` 该问题的解决方法,需要在环境中安装 graphviz 和 gv: ```Bash sudo apt install graphviz gv ``` 安装成功后,再次执行方法 1 中命令,可以得到可视化 svg 文件。 测试用例执行三十分钟后,我们对最后十分钟的内存增长进行分析,结果发现:95.9% 的内存增长来自 `G1DefaultParGCAllocator` 的构造函数调用,这里的最后 10 分钟是和用例设置相关,如下图所示: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/103218zerzcbwj8vajtb3v.png) 上图比较清晰显示了内存申请相关函数的调用关系以及内存申请比例和数量,约95.9% 的堆内存是通过 `G1DefaultParGCAllocator` 完成申请,可以预测在 `G1DefaultParGCAllocator` 的构造函数中申请的内存没有被及时回收掉而导致内存泄漏的可能性非常大。这个时候可以通过代码协助分析了。 jeprof 工具不仅可以查看详细信息或者生成调用路径图(如上图所示),还可以用来比较两个 dump 文件(显示增量部分),既然作为工具使用介绍,我们继续介绍另一种补充性分析方法:将连续两次的 heap 文件做差异对比,输出的 PDF 可视化文件可以进一步确定是哪里内存持续申请没有被释放而导致内存增长。 方法如下: ```Bash jeprof --base=jeprof.34070.0.i0.heap --pdf /home/xxxx/jdk1.8.0_292/bin/java jeprof.34070.1.i1.heap > diff.pdf ``` 内存增加差异图: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/1033256kcctizr1j2rct1e.png) 通过上图可以非常清晰看到:`G1DefaultParGCAllocator` 的构造函数持续申请内存,导致内存增长迅速。后续的工作就是针对 `G1DefaultParGCAllocator` 构造函数中内存申请情况,排查释放逻辑,寻找问题原因并解决,这块的工作不属于 jemalloc 范畴,本内容不再赘述。 代码修复后 Java 进程物理内存使用情况如下(运行 30 小时 +): ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/103348vmyly0dqsqphh5tf.png) 内存使用符合预期,问题解决。 通过 jemalloc 工具和上面介绍的方法,帮助开发快速解决了此特性引起 Native method 内存泄漏问题,方法使用简单。在实际业务中有遇到类似问题的同学,不妨亲自尝试一下。 # 附录 A jemalloc 的编译 jemalloc 普通版并不包含 profiling 机制,所以需要下载源代码重新编译,在 configure 的时候添加了 --enable-prof 选项,这样才能打开 profiling 机制。 ## A.1 下载最新版本 jemalloc 源码 ```Bash git clone https://github.com/jemalloc/jemalloc.git ``` ## A.2 jemalloc 源码构建 1. 修改 autogen.sh 文件,使能 prof,如下: ```Bash diff --git a/autogen.sh b/autogen.sh index 75f32da6..6ab4053c 100755 --- a/autogen.sh +++ b/autogen.sh @@ -9,8 +9,8 @@ for i in autoconf; do fi done -echo "./configure --enable-autogen $@" -./configure --enable-autogen $@ +echo "./configure --enable-prof $@" +./configure --enable-prof $@ if [ $? -ne 0 ]; then echo "Error $? in ./configure" exit 1 ``` 执行: ```Bash $ ./autogen.sh $ make -j 6 ``` 以下命令可选: ```Bash $ make install ``` 2. 源码构建成功后(一般不会出错),会在当前目录的 bin 和 lib 目录下生成重要的文件: ```Bash $ ls -l total 376 -rw-rw-r-- 1 xxxx xxxx 1954 Jun 19 06:16 jemalloc-config -rw-rw-r-- 1 xxxx xxxx 1598 Jun 19 06:12 jemalloc-config.in -rw-rw-r-- 1 xxxx xxxx 145 Jun 19 06:16 jemalloc.sh -rw-rw-r-- 1 xxxx xxxx 151 Jun 19 06:12 jemalloc.sh.in -rw-rw-r-- 1 xxxx xxxx 182460 Jun 19 06:16 jeprof -rw-rw-r-- 1 xxxx xxxx 182665 Jun 19 06:12 jeprof.in $ cd ../lib/ $ ls -l total 89376 -rw-rw-r-- 1 xxxx xxxx 42058434 Jun 19 06:19 libjemalloc.a -rw-rw-r-- 1 xxxx xxxx 42062016 Jun 19 06:19 libjemalloc_pic.a lrwxrwxrwx 1 xxxx xxxx 16 Jun 19 06:19 libjemalloc.so -> libjemalloc.so.2 -rwxrwxr-x 1 xxxx xxxx 7390832 Jun 19 06:19 libjemalloc.so.2 $ pwd /home/xxxx/jemalloc/jemalloc/lib ``` 3. 设置环境变量和执行权限 bin 目录下的 jeprof 文件,没有执行权限,需要设置一下: ```Bash bin$ chmod +x ./* ``` 退到 bin 的上一层目录设置环境变量,可参考如下方法: ```Bash xxxx@hostname:jemalloc$ echo $JEMALLOC_DIR xxxx@hostname:jemalloc$ export JEMALLOC_DIR=`pwd` xxxx@hostname:jemalloc$ echo $JEMALLOC_DIR /home/xxxx/jemalloc/jemalloc xxxx@hostname:jemalloc$ export LD_PRELOAD=$JEMALLOC_DIR/lib/libjemalloc.so xxxx@hostname:jemalloc$ export MALLOC_CONF=prof:true,lg_prof_interval:30,lg_prof_sample:17 xxxx@hostname:jemalloc$ which jeprof xxxx@hostname:jemalloc$ export PATH=$PATH:$JEMALLOC_DIR/bin xxxx@hostname:jemalloc$ which jeprof /home/xxxx/jemalloc/jemalloc/bin/jeprof xxxx@hostname:jemalloc$ jeprof --version jeprof (part of jemalloc 5.2.1-737-g2381efab5754d13da5104b101b1e695afb442590) based on pprof (part of gperftools 2.0) Copyright 1998-2007 Google Inc. This is BSD licensed software; see the source for copying conditions and license information. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. ``` 到这一步,jeprof 可以在该环境中启动使用了。 # 后记 如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 社区查找相关资源(点击[阅读原文](https://www.openeuler.org/zh/other/projects/bishengjdk/)进入官网),包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。 ![](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/1036288oizzgopij7py9k7.jpg) # 参考 1. https://chenhm.com/post/2018-12-05-debuging-java-memory-leak 2. https://github.com/jemalloc/jemalloc 3. https://blog.csdn.net/qq_36287943/article/details/105491301 4. https://www.cnblogs.com/minglee/p/10124174.html 5. https://www.yuanguohuo.com/2019/01/02/jemalloc-heap-profiling 原文转载自 openEuler-[使用jemalloc解决JVM内存泄露问题](https://mp.weixin.qq.com/s/SwW0s_BKc1hT_Zm1H-3Arw)
  • [毕昇JDK] 【技术剖析】9. 使用 NMT 和 pmap 解决 JVM 资源泄漏问题
    ## 作者:宋尧飞 > 编者按:笔者使用 JDK 自带的内存跟踪工具 NMT 和 Linux 自带的 pmap 解决了一个非常典型的资源泄漏问题。这个资源泄漏是由于 Java 程序员不正确地使用 Java API 导致的,使用 Files.list 打开的文件描述符必须关闭。本案例一方面介绍了怎么使用 NMT 解决 JVM 资源泄漏问题,如果读者遇到类似问题,可以尝试用 NMT 来解决;另一方面也提醒 Java 开发人员使用 Java API 时需要必须弄清楚 API 使用规范,希望大家通过这个案例有所收获。 # 背景知识 ## NMT NMT 是 Native Memory Tracking 的缩写,一个 JDK 自带的小工具,用来跟踪 JVM 本地内存分配情况(本地内存指的是 non-heap,例如 JVM 在运行时需要分配一些辅助数据结构用于自身的运行)。 NMT 功能默认关闭,可以在 Java 程序启动参数中加入以下参数来开启: `-XX:NativeMemoryTracking=[summary | detail]` 其中,"summary" 和 "detail" 的差别主要在输出信息的详细程度。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/100207eqp7izyfvxa4l5fp.png) 开启 NMT 功能后,就可以使用 JDK 提供的 jcmd 命令来读取 NMT 采集的数据了,具体命令如下: ```Java jcmd VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] ``` NMT 参数的含义可以通过 `jcmd help VM.native_memory` 命令查询。通过 NMT 工具,我们可以快速区分内存泄露是否源自 JVM 分配。 ## pmap 对于非 JVM 分配的内存,经常需要用到 pmap 这个工具了,这是一个 linux 系统自带工具,能够从系统层面输出目标进程内存使用的详细情况,用法非常简单: ```Java pmap [参数] ``` 常用的选项是 "-x" 或 "-X",都是用来控制输出信息的详细程度。 上图是 pmap 部分输出信息,每列含义为 | Address | 每段内存空间起始地址 | | ---- | ---- | | Kbytes | 每段内存空间大小(单位 KB) | | RSS | 每段内存空间实际使用内存大小(单位 KB) | | Dirty | 每段内存空间脏页大小(单位 KB) | | Mode | 每段内存空间权限属性 | | Mapping | 可以映射到文件,也可以是“anon”表示匿名内存段,还有一些特殊名字如“stack” | # 现象 某业务集群中,多个节点出现业务进程内存消耗缓慢增长现象,以其中一个节点为例: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/1005325ilrliz8ahmowoyt.png) 如图所示,这个业务进程当前占用了 4.7G 的虚拟内存空间,以及 2.2G 的物理内存。已知正常状态下该业务进程的物理内存占用量不超过 1G。 # 分析 使用命令 `jcmdVM.native_memory detail` 可以看到所有受 JVM 监控的内存分布情况: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/100608owdtwmrm9leyukea.png) 上图只是截取了 nmt(Native Memory Tracking) 命令展示的概览信息,这个业务进程占用的 2.2G 物理内存中,受 JVM 监控的大概只占了 0.7G(上图中的 committed),意味着有 1.5G 物理内存不受 JVM 管控。JVM 可以监控到 Java 堆、元空间、CodeCache、直接内存等区域,但无法监控到那些由 JVM 之外的 Native Code 申请的内存,例如典型的场景:第三方 so 库中调用 malloc 函数申请一块内存的行为无法被 JVM 感知到。 nmt 除了会展示概览之外,还会详细罗列每一片受 JVM 监控的内存,包括其地址,将这些 JVM 监控到的内存布局和用 pmap 得到的完整的进程内存布局做一个对比筛查,这里忽略 nmt 和 pmap(下图 pmap 命令中 25600 是进程号)详细内存地址的信息,直接给出最可疑的那块内存: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/100618tjpfyqh0yrp8ss5f.png) 由图可知,这片 1.7G 左右的内存区域属于系统层面的堆区。 **备注:这片系统堆区之所以稍大于上面计算得到的差值,原因大概是 nmt 中显示的 committed 内存并不对应真正占用的物理内存(linux 使用 Lazy 策略管理进程内存),实际通常会稍小。** 系统堆区主要就是由 libc 库接口 malloc 申请的内存组合而成,所以接下来就是去跟踪业务进程中的每次 malloc 调用,可以借助 GDB: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/100633kpb31dj8wkf7vlex.png) 实际上会有大量的干扰项,这些干扰项一方面来自 JVM 内部,比如: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/1006459vtkfen2ogs8lxku.png) 这部分干扰项很容易被排除,凡是调用栈中存在 `os::malloc` 这个栈帧的干扰项就可以直接忽视,因为这些 malloc 行为都会被 nmt 监控到,而上面已经排除了受 JVM 监控内存泄漏的可能。 另一部分干扰项则来自 JDK,比如: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/100657ozdudwqjgtcteq5m.png) 有如上图所示,不少 JDK 的本地方法中直接或间接调用了 malloc,这部分 malloc 行为通常是不受 JVM 监控的,所以需要根据具体情况逐个排查,还是以上图为例,排查过程如下: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/100707bizeverlsiqjxgpv.png) 注意图中临时中断的值(0x0000ffff5fc55d00)来自于第一个中断 b malloc 中断发生后的结果。 这里稍微解释一下上面 GDB 在做的排查过程,就是检查 malloc 返回的内存地址后续是否有通过 free 释放(通过 `tb free if X3` 这个命令,具体用法可以参考 GDB 调试),显然在这个例子中是有释放的。 通过这种排查方式,几经筛选,最终找到了一个可疑的 malloc 场景: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/100719wqgwwuv9y9yv3gnm.png) 从调用栈信息可以知道,这是一个 JDK 中的本地方法 `sun.nio.fs.UnixNativeDispatcher.opendir0`,作用是打开一个目录,但后续始终没有进行关闭操作。进一步分析可知,该可疑 opendir 操作会周期性执行,而且都是操作同一个目录 `/xxx/nginx/etc/nginx/conf`,看来,是有个业务线程在定时访问 nginx 的配置目录,每次访问完却没有关闭打开的目录。 分析到这里,其实这个问题已经差不多水落石出。和业务方确认,存在一个定时器线程在周期性读取 nginx 的配置文件,代码大概是这样子的: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/100727x11xrekm7nfpjvx4.png) 翻了一下相关 JDK 源码,Files.list 方法是有在末尾注册一个关闭钩子的: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/1007353v3dw56ppbaufuxg.png) 也就是说,Files.list 方法返回的目录资源是需要手动释放的,否则就会发生资源泄漏。 由于这个目录资源底层是会关联一个 fd 的,所以泄漏问题还可以通过另一个地方进行佐证: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/100741eilhspkdikaf08d6.png) 该业务进程目前已经消耗了 51116 个 fd! 假设这些 fd 都是 opendir 关联的,每个 opendir 消耗 32K,则总共消耗 1.6G,显然可以跟上面泄漏的内存值基本对上。 # 总结 稍微了解了一下,发现几乎没人知道 JDK 方法 Files.list 是需要关闭的,这个案例算是给大家都提了个醒。 # 后记 如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 社区查找相关资源(点击[阅读原文](https://www.openeuler.org/zh/other/projects/bishengjdk/)进入官网),包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。 ![](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/11/100821urankftzcpzashgm.jpg) 原文转载自openEuler-[使用 NMT 和 pmap 解决 JVM 资源泄漏问题](https://mp.weixin.qq.com/s/8Kca9sWOK4BoXx-7CyBpMQ)
  • [毕昇JDK] 【技术剖析】7. 看看毕昇 JDK 团队是如何解决 JVM 中 CMS 的 Crash
    ## 作者: 王帅 > 编者按:笔者遇到一个非常典型 JVM 架构相关问题,在 x86 正常运行的应用,在 aarch64 环境上低概率偶现 JVM 崩溃。这是一个典型的 JVM 内部 bug 引发的问题。通过分析最终定位到 CMS 代码存在 bug,导致 JVM 在弱内存模型的平台上 Crash。在分析过程中,涉及到 CMS 垃圾回收原理、内存屏障、对象头、以及 ParNew 并行回收算法中多个线程竞争处理的相关技术。笔者发现并修复了该问题,并推送到上游社区中。毕昇 JDK 发布的所有版本均解决了该问题,其他 JDK 在 jdk8u292、jdk11.0.9、jdk13 以后的版本修复该问题。 # bug 描述 目标进程在 aarch64 平台上运行,使用的 GC 算法为 `CMS(-XX:+UseConcMarkSweepGC)`,会概率性地发生 JVM crash,且问题发生的概率极低。我们在 aarch64 平台上使用 fuzz 测试,运行目标进程 50w 次只出现过一次 crash(连续运行了 3 天)。 JBS issue:https://bugs.openjdk.java.net/browse/JDK-8248851 # 约束 - 我们对比了 x86 和 aarch64 架构,发现问题仅在 aarch64 环境下会出现。 - 文中引用的代码段取自 openjdk-8u262:http://hg.openjdk.java.net/jdk8u/jdk8u-dev/。 - 读者需要对 JVM 有基本的认知,如垃圾回收,对象布局,GC 线程等,且有一定的 C++ 基础。 # 背景知识 ## GC GC(Garbage Collection)是 JVM 中必不可少的部分,用于回收不再会被使用到的对象,同时释放对象占用的内存空间。 垃圾回收对于释放的剩余空间有两种处理方式: - 一种是存活对象不移动,垃圾对象释放的空间用空闲链表(free_list)来管理,通常叫做**标记-清除(Mark-Sweep)**。创建新对象时根据对象大小从空闲链表中选取合适的内存块存放新对象,但这种方式有两个问题,一个是空间局部性不太好,还有一个是容易产生内存碎片化的问题。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/154328iq9tj6d4ljgnbfqc.png) - 另一种对剩余空间的处理方式是 **Copy GC**,通过移动存活对象的方式,重新得到一个连续的空闲空间,创建新对象时总在这个连续的内存空间分配,直接使用碰撞指针方式分配(Bump-Pointer)。这里又分两种情况: - 将存活对象复制到另一块内存(to-space,也叫 survival space),原内存块全部回收,这种方式叫**撤离(Evacuation)**。 - 将存活对象推向内存块的一侧,另一侧全部回收,这种方式也被称为**标记-整理(Mark-Compact)**。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/154932g2q2hjgwwew5dubb.png) 现代的垃圾回收算法基本都是分代回收的,因为大部分对象都是朝生夕死的,因此将新创建的对象放到一块内存区域,称为**年轻代**;将存活时间长的对象(由年轻代晋升)放入另一块内存区域,称为**老年代**。根据不同代,采用不同回收算法。 - 年轻代,一般采用 Evacuation 方式的回收算法,没有内存碎片问题,但会造成部分空间浪费。 - 老年代,采用 Mark-Sweep 或者 Mark-Compact 算法,节省空间,但效率低。 GC 算法是一个较大的课题,上述介绍只是给读者留下一个初步的印象,实际应用中会稍微复杂一些,本文不再展开。 ## CMS CMS(Concurrent Mark Sweep)是一个以低时延为目标设计的 GC 算法,特点是 GC 的部分步骤可以和 mutator 线程(可理解为 Java 线程)同时进行,减少 STW(Stop-The-World)时间。年轻代使用 ParNewGC,是一种 Evacuation。老年代则采用 ConcMarkSweepGC,如同它的名字一样,采用 Mark-Sweep(默认行为)和 Mark-Compact(定期整理碎片)方式回收,它的具体行为可以通过参数控制,这里就不展开了,不是本文的重点研究对象。 CMS 是 openjdk 中实现较为复杂的 GC 算法,条件分支很多,阅读起来也比较困难。在高版本 JDK 中已经被更优秀和高效的 G1 和 ZGC 替代(CMS 在 JDK 13 之后版本中被移除)。 本文讨论的重点主要是年轻代的回收,也就是 ParNewGC 。 ## 对象布局 在 Java 的世界中,万物皆对象。对象存储在内存中的方式,称为对象布局。在 JVM 中对象布局如下图所示: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/155037uvgeamjdswbf3xzn.png) 对象由对象头加字段组成,我们这里主要关注对象头。对象头包括markOop和_matadata。前者存放对象的标志信息,后者存放 Klass 指针。所谓 Klass,可以简单理解为这个对象属于哪个 Java 类,例如:String str = new String(); 对象 str 的 Klass 指针对应的 Java 类就是 Ljava/lang/String。 - markOop 的信息很关键,它的定义如下[1]: ```Java 1. // 32 bits: 2. // -------- 3. // hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object) 4. // JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object) 5. // size:32 ------------------------------------------>| (CMS free block) 6. // PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object) 7. // 8. // 64 bits: 9. // -------- 10. // unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object) 11. // JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object) 12. // PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object) 13. // size:64 ----------------------------------------------------->| (CMS free block) 14. // 15. // unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object) 16. // JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object) 17. // narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object) 18. // unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block) ``` 对于一般的 Java 对象来说,markOop 的定义如下(以 64 位举例): 1. 低两位表示对象的锁标志:00-轻量锁,10-重量锁,11-可回收对象, 01-表示无锁。 2. 第三位表示偏向锁标志:0-表示无锁,1-表示偏向锁,注意当偏向锁标志生效时,低两位是 01-无锁。即 ---->|101 表示这个对象存在偏向锁,高 54 位存放偏向的 Java 线程。 3. 第 4-7 位表示对象年龄:一共 4 位,所以对象的年龄最大是 15。 CMS 算法还会用到 `markOop`,用来判断一个内存块是否为 `freeChunk`,详细的用法见下文分析。 `_metadata` 的定义如下: ```Java 1. class oopDesc { 2. friend class VMStructs; 3. private: 4. volatile markOop _mark; 5. union _metadata { 6. Klass* _klass; 7. narrowKlass _compressed_klass; 8. } _metadata; 9. // ... 10. } ``` `_metadata` 是一个 union,不启用压缩指针时直接存放 `Klass` 指针,启用压缩指针后,将 `Klass` 指针压缩后存入低 32 位。高 32 位留作它用。至于为什么要启用压缩指针,理由也很简单,因为每个引用类型的对象都要有 `Klass` 指针,启用压缩指针的话,每个对象都可以节省 4 个 byte,虽然看起来很小,但实际上却可以减少 GC 发生的频率。而压缩的算法也很简单,`base + _narrow_klass << offset` 。`base` 和 `offset` 在 JVM 启动时会根据运行环境初始化好。offset 常见的取值为 0 或者 3(8 字节对齐)。 ## memory barrier 内存屏障(Memory barrier)是多核计算机为了提高性能,同时又要保证程序正确性,必不可少的一个设计。简单来说是为了防止因为系统优化,或者指令调度等因素导致的指令乱序。 所以多核处理器大都提供了内存屏障指令,C++ 也提供了关于内存屏障的标准接口,参考 memory order 。 总的来说分为 `full-barrier` 和 `one-way-barrier`。 - `full barrier` 保证在内存屏障之前的读写操作的真正完成之后,才能执行屏障之后的读写指令。 - `one-way-barrier` 分为 `read-barrier` 和 `write-barrier`。以 `read-barrier` 为例,表示屏障之后的读写操作不能乱序到屏障之前,但是屏障指令之前的读写可以乱序到屏障之后。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/155338dxshvz9c9l56feeg.png) openjdk 中的 `barrier`定义[3] ```Java 1. class OrderAccess : AllStatic { 2. public: 3. static void loadload(); 4. static void storestore(); 5. static void loadstore(); 6. static void storeload(); 7. 8. static void acquire(); 9. static void release(); 10. static void fence(); 11. // ... 12. static jbyte load_acquire(volatile jbyte* p); 13. // ... 14. static void release_store(volatile jint* p, jint v); 15. // ... 16. private: 17. // This is a helper that invokes the StubRoutines::fence_entry() 18. // routine if it exists, It should only be used by platforms that 19. // don't another way to do the inline eassembly. 20. static void StubRoutines_fence(); 21. }; ``` 其中 `acquire()`和 `release()` 是 `one-way-barrier`, `fence()` 是 `full-barrier`。不同架构依照这个接口,实现对应架构的 `barrier` 指令。 # 问题分析 在问题没有复现之前,我们能拿到的信息只有一个名为 `hs_err_$pid.log` 的文件,JVM 在发生 crash 时,会自动生成这个文件,里面包含 crash 时刻 JVM 的详细信息。但即便如此,分析这个问题还是有相当大的困难。因为没有 core 文件,无法查看内存中的信息。好在我们在一台测试环境上成功复现了问题,为最终解决这个问题奠定了基础。 ## 第一现场 首先我们来看下 crash 的第一现场。 - backtrace ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/155519tqmvb8ngunplzby6.png) 通过调用栈我们可以看出发生 core 的位置是在 `CompactibleFreeListSpace::block_size` 这个函数,至于这个函数具体是干什么的,我们待会再分析。从调用栈中我们还可以看到,这是一个 ParNew 的 GC 线程。上文提到 CMS 年轻代使用 ParNewGC 作为垃圾回收器。这里 Par 指的是 `Parallel`(并行)的意思,即多个线程进行回收。 - pc ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/155554fai7uikkzl6es5kh.png) pc 值是 `0x0000ffffb2f320e8`,相对这段 Instruction 开始位置 `0x0000ffffb2f320c8` 偏移为 0x20,将这段 Instructions 用反汇编工具得到如下指令: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/155615x0brjk4qrhiejvrd.png) 根据相对偏移,我们可以计算出发生 core 的指令为 `02 08 40 B9 ldr w2, [x0, #8]`,然后从寄存器列表,可以看出 x0(上图中的 R0)寄存器的值为 `0x54b7af4c0`,这个值看起来不像是一个合法的地址。所以我们接下来看看堆的地址范围。 - heap ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/155715urhs51qnyhcmme7y.png) 从堆的分布可以看出 `0x54b7af4c0` 肯定不在堆空间内,到这里可以怀疑大概率是访问了非法地址导致 crash,为了更进一步确认这个猜想,我们要结合源码和汇编,来确认这条指令的目的。 - 首先我们看看汇编 下图这段汇编是由 `objdump` 导出 `libjvm.so` 得到,对应 `block_size` 函数一小部分: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/155745bvji8fivltyfzhsp.png) 图中标黄的部分就是 crash 发生的地址,这个地址在 hs_err_pid.log 文件中也有体现,程序运行时应该是由 `0x4650ac` 这个位置经过 cbnz 指令跳转过来的。而图中标红的这条指令是一条逻辑左移指令,结合 x5 寄存器的值是 3,我首先联想到 x0 寄存器的值应当是一个 Klass 指针。因为在 64 位机器上,默认会开启压缩指针,而 hs_err_$pid.log 文件中的 narrowklass 偏移刚好是 3(heap 中的 Narrow klass shift: 3)。到这里,如果不熟悉 Klass 指针是什么,可以回顾下背景知识中的对象布局。 如果 x0 寄存器存放的是 Klass 指针,那么 ldr w2, [x0, #8] 目的就是获取对象的大小,至于为什么,我们结合源码来分析。 - 源码分析 CompactibleFreeListSpace::block_size 源码[4]: ```Java 1. size_t CompactibleFreeListSpace::block_size(const HeapWord* p) const { 2. NOT_PRODUCT(verify_objects_initialized()); 3. // This must be volatile, or else there is a danger that the compiler 4. // will compile the code below into a sometimes-infinite loop, by keeping 5. // the value read the first time in a register. 6. while (true) { 7. // We must do this until we get a consistent view of the object. 8. if (FreeChunk::indicatesFreeChunk(p)) { 9. volatile FreeChunk* fc = (volatile FreeChunk*)p; 10. size_t res = fc->size(); 11. 12. // Bugfix for systems with weak memory model (PPC64/IA64). The 13. // block's free bit was set and we have read the size of the 14. // block. Acquire and check the free bit again. If the block is 15. // still free, the read size is correct. 16. OrderAccess::acquire(); 17. 18. // If the object is still a free chunk, return the size, else it 19. // has been allocated so try again. 20. if (FreeChunk::indicatesFreeChunk(p)) { 21. assert(res != 0, "Block size should not be 0"); 22. return res; 23. } 24. } else { 25. // must read from what 'p' points to in each loop. 26. Klass* k = ((volatile oopDesc*)p)->klass_or_null(); 27. if (k != NULL) { 28. assert(k->is_klass(), "Should really be klass oop."); 29. oop o = (oop)p; 30. assert(o->is_oop(true /* ignore mark word */), "Should be an oop."); 31. 32. // Bugfix for systems with weak memory model (PPC64/IA64). 33. // The object o may be an array. Acquire to make sure that the array 34. // size (third word) is consistent. 35. OrderAccess::acquire(); 36. 37. size_t res = o->size_given_klass(k); 38. res = adjustObjectSize(res); 39. assert(res != 0, "Block size should not be 0"); 40. return res; 41. } 42. } 43. } 44. } ``` 这个函数的功能我们先放到一边,首先发现 `else` 分支中有关于 `Klass` 的判空操作,且仅有这一处,这和反汇编之后的 cbnz 指令对应。如果 k 不等于 NULL,则会马上调用 size_given_klass(k) 这个函数[5],而这个函数第一步就是取 klass 偏移 8 个字节的内容。和 ldr w2, [x0, #8]对应。 ```Java 1. inline int oopDesc::size_given_klass(Klass* klass) { 2. int lh = klass->layout_helper(); 3. int s; 4. // ... 5. } ``` ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/160004vhfo74ojmghuharc.png) 通过 gdb 查看 Klass 的 fields offset,`_layout_helper` 的偏移刚好是 8 。 `klass->layout_helper();`这个函数就是取 Klass 的 `_layout_helper` 字段,这个字段在解析 class 文件时,会自动计算,如果为正,其值为对象的大小。如果为负,表示这个对象是数组,通过设置 bit 的方式来描述这个数组的信息。但无论怎样,这个进程都是在获取 `layouthelper` 时发生了 crash。 到这里,程序 core 在这个位置应该是显而易见的了,但是为什么 klass 会读到一个非法值呢?仅凭现有的信息,实在难以继续分析。幸运的是,我们通过 fuzz 测试,成功复现了这个问题,虽然复现概率极低,但是拿到了 `coredump` 文件。 ## debug 问题复现后,第一步要做的就是验证之前的分析结论: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/160102pgh9ryo1x0qernhl.png) 上述标号对应指令含义如下: 1. narrow_klass 的值最初放在 x6 寄存器中,通过 load 指令加载到 x0 寄存器 2. 压缩指针解压缩 3. 判断解压缩后的 klass 指针是否为 NULL 4. 获取 Klass 的 layouthelper 查看上述指令相关的寄存器: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/160127dltjaslsukhy5ptt.png) 1. 寄存器 x0 的值为 0x5b79f1c80 2. 寄存器 x0 的值是一个非法地址 3. 查看 narrow_klass 的 offset 4. 查看 narrow_klass 的 base 5. narrow_klass 解压缩,得到的结果是 0x100000200 和 x0 的值对应不上??? 6. 查看这个对象是什么类型,发现是一个 char 类型的数组。 通过以上调试基本信息,可以确认我们的猜想正确 ,但是问题是我们解压缩后得到的 Klass 指针是正确的,也能解析出 C,这是一个有效的 Klass。 但是 x0 中的值确实一个非法值。也就是说,内存中存放的 Klass 指针是正确的,但是 CPU 看见的 x0,也就是存放 Klass 指针的寄存器值是错误的。为什么会造成这种不一致呢,可能的原因是,这个地址刚被其他线程改写,而当前线程获取到的是写入之前的值,这在多线程环境下是非常有可能发生的,但是如果程序写的正确,且加入了正确的 `memory barrier`,也是不会有问题的,但现在出了问题,只能说明是程序没有插入适当的 `memory barrier`,或者插入得不正确。到这里,我们可以知道这个问题和内存序有关,但具体是什么原因导致这个地方读取错误,还要结合 GC 算法的逻辑进行分析。 ## ParNewTask 结合上文的调用栈,这个线程是在做根扫描,根扫描的意思是查找活跃对象的根,然后根据这个根集合,查找出根引用的对象的集合,进而找到所有活跃对象。因为 ParNew 是年轻代的垃圾回收器,要识别出整个年轻代的所有活跃对象。有一种可能的情况是根引用一个老年代对象 ,同时这个老年代对象又引用了年轻代的对象,那么这个年轻代的对象也应该被识别为活对象。 ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/160216ynzr7dox3nznt4fd.png) 所以我们需要考虑上述情况,但是没有必要扫描整个老年代的对象,这样太影响效率了,所以会有一个表记录老年代的哪些对象有引用到年轻代的对象。在 JVM 中有一个叫 `Card Table`的数据结构,专门干这个事情。 ## Card table 关于 `Card table` 的实现细节,本文不做展开,只是简单介绍下实现思路。有兴趣的读者可以参考网上其他关于 `Card table` 的文章。也可以根据本文的调用栈,去跟一下源码中的实现细节。 简单来说就是使用 `1 byte` 的空间记录一段连续的 `512 byte` 内存空间中老年代的对象引用关系是否发生变化。如果有,则将这个 `card` 标记置为 `dirty`,这样做根扫描的时候,只关注这些 `dirty card` 即可。当找到一个 `dirty card` 之后,需要对整个 `card` 做扫描,这个时候,就需要计算 `dirty card` 中的一块内存的大小。回忆下 CMS 老年代分配算法,是采用的 `freelist`。也就是说,一块连续的 `dirty card`,并不都是一个对象一个对象排布好的。中间有可能会产生缝隙,这些缝隙也需要计算大小。调用栈中的 `process_stride` 函数就是用来扫描一个 `dirtyCard` 的,而最顶层的 `block_size` 就是计算这个 `dirtyCard` 中某个内存块大小的。 `FreeChunk::indicatesFreeChunk(p)` 是用来判断块 p 是不是一个 `freeChunk`,就是这块内存是空的,加在 `free_list` 里的。如果不是一个 `freeChunk`,那么继续判断是不是一个对象,如果是一个对象,计算对象的大小,直到整个 card 遍历完。 ## 晋升 从上文中 gdb 的调试信息不难看出这个对象的地址为 `0xc93e2a18`(klass 地址 0xc93e2a20 -8),结合 heap 信息,这个对象位于老年代。如果是一个正常的老年代对象,在上一次 GC 完成之后,对象是不会移动的,那么作为对象头的 markOop 和 Klass 是大概率不会出现寄存器和内存值不一致的情况,因为这离现场太远了。那么更加可能的情况是什么呢?答案就是晋升。 熟悉 GC 的朋友们肯定知道这个概念,这里我再简单介绍下。所谓晋升就是发生 Evacuation 时,如果对象的年龄超过了阈值,那么认为这个对象是一个长期存活的对象,将它 copy 到老年代,而不是 survival space。还有一种情况是 survival space 空间已经不足了,这时如果还有活的对象没有 copy,那么也需要晋升到老年代。不管是那种情况,发生晋升和做根扫描这两个线程是可以同时发生的,因为都是 ParNewTask。 到这里,问题的重点怀疑对象,放在了对象晋升和根扫描两个线程之间没有做好同步,从而导致根扫描时读到错误的 Klass 指针。 所以简单看下晋升实现[6]。 ```Java 1. ConcurrentMarkSweepGeneration::par_promote { 2. 3. HeapWord* obj_ptr = ps->lab.alloc(alloc_sz); 4. |---> CFLS_LAB::alloc 5. |--->FreeChunk::markNotFree 6. 7. oop obj = oop(obj_ptr); 8. OrderAccess::storestore(); 9. 10. obj->set_mark(m); 11. OrderAccess::storestore(); 12. 13. // Finally, install the klass pointer (this should be volatile). 14. OrderAccess::storestore(); 15. obj->set_klass(old->klass()); 16. 17. ...... 18. 19. void markNotFree() { 20. // Set _prev (klass) to null before (if) clearing the mark word below 21. _prev = NULL; 22. #ifdef _LP64 23. if (UseCompressedOops) { 24. OrderAccess::storestore(); 25. set_mark(markOopDesc::prototype()); 26. } 27. #endif 28. assert(!is_free(), "Error"); 29. } ``` 看到这个地方,隔三岔五的一个 `OrderAccess::storestore();` 我感觉到我离真相不远了,这里已经插了这么多 `memory barrier` 了,难道之前经常出过问题吗?但是已经插了这么多了,难道还有问题吗?哈哈哈… 看下代码逻辑,首先从 `freelist` 中分配一块内存,并将其初始化为一个新的对象 oop,这里需要注意的一个地方是 `markNotFree` 这个函数,将 `prev`(转换成 oop 是对象的 Klass)设置为 NULL,然后将需要 copy 的对象的 `markOop` 赋值给这个新对象,再然后 copy 对象体,最后再将需要 copy 对象的 Klass 赋值给新对象。这中间的几次赋值都插入了 `OrderAccess::storestore()`。回忆下背景知识中的 `memory barrier` ,`OrderAccess::storestore()` 的含义是,`storestore` 之前的写操作,一定比 `storestore` 之后的写操作先完成。换句话说,其他线程当看到 `storestore` 之后写操作时,那么它观察到的 `storestore` 之前的写操作必定能完成。 ## 根因 通过上面的介绍,相信大家理解了 `block_size` 的功能,以及 `par_promote` 的写入顺序。那么这两个函数,或者说执行这两个函数的线程是如何造成 `block_size` 函数看见的 `klass` 不一致(CPU 和内存不一致)的呢?请看下面的伪代码: ![image.png](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/160948nbataq1lzladq1jg.png) 1. `scan card` 线程先读 klass,此时读到取到的 klass 是一个非法地址; 2. `par_promote` 线程设置 klass 为 NULL; 3. `par_promote` 设置 `markoop`,判断一块内存是不是一个 `freeChunk`,就是 `markoop` 的第 8 位判断的(回忆背景知识); 4. `scan card` 线程根据 `markoop` 判断该内存块是一个对象,进入 `else` 分支; 5. `par_promote` 线程此时将正确的 `klass` 值写入内存; 6. `scan card` 线程发现 `klass` 不是 NULL,访问 `klass` 的` _layout_helper`,出现非法地址访问,发生 coredump。 到这里,所有的现象都可以解释通了,但是线程真正执行的时候,会发生上述情况吗?答案是会发生的。 - 我们先看 scan card 线程 ① 中 `isfreeChunk` 会读 p(对应 `par_promote` 的 oop)的 `markoop`,④ 会读 p 的 `klass`,这两者的读写顺序,按照程序员的正常思维,一定是先读 `markoop`,再读 `klass`,但是 CPU 运行时,为了提高效率,会一次性取多条指令,还可能进行指令重排,使流水线吞吐量更高。所以 `klass` 是完全有可能在 `markoop` 之前就被读取。那么我们实际的期望是先读 `markoop`,再读 `klass`。那么怎样确保呢? - 接下来看下 par_promote 线程 根据之前堆 `storestore` 的解释,③ 写入 `markoop` 之后,`scan_card` 线程必定能观察到 `klass` 赋值为 NULL,但也有可能直接观察到 ⑤ `klass` 设置了正确的值。 - 我们再看下 scan card 线程 试想以下,如果 `markoop` 比 `klass` 先读,那么在 ① 读到的 `klass`,要么是 NULL,要么是正确的 `Klass`,如果读到是 NULL,则会在 `while(true)` 内循环,再次读取,直到读到正确的 `klass`。那么如果反过来 `klass` 比 `markoop` 先读,就有可能产生上述标号顺序的逻辑,造成错误。 综上,我们只要确保 `scan_card` 线程中 `markoop` 比 `klass` 先读,就能确保这段代码逻辑无懈可击。所以修复方案也自然而然想到,在 ① 和 ④ 之间插入 load 的 `memory barrier`,即加入一条 `OrderAccess::loadload()`。 详细的修复 patch 见 https://hg.openjdk.java.net/jdk-updates/jdk11u/rev/ae52898b6f0d 。目前已经 backport 到 jdk8u292,以及 JDK 13。 # x86 ? 至于这个问题为什么在 x86 上不会出现,这是因为 x86 的内存模型是 `TSO(Total Store Ordering)`的,他不允许读读乱序,从架构层面避免了这个问题。而 aarch64 内存模型是松散模型(Relaxed),读和写可以任意乱序,这个问题也随之暴露。关于这两种内存模型,Relaxed 的模型理论上肯定是更有性能优势的,但是对程序员的要求也更大。TSO 模型虽然只允许写后读提前,但是在大多数情况下,能够确保程序顺序和执行顺序保持一致。 # 总结 这是一个极小概率发生的 bug,因此隐藏的很深。解这个 bug 也耗费了很长时间,虽然最后修复方案就是一行代码,但涉及的知识面还是比较广的。其中 `memory barrier` 是一个有点绕的概念,GC 算法的细节也需要理解到位。如果读者第一次接触 JVM,希望有耐心看下去,反复推敲,相信你一定会有所收获。 # 后记 如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入[毕昇 JDK 社区](https://www.openeuler.org/zh/other/projects/bishengjdk/)查找相关资源,包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。 ![](https://bbs-img.huaweicloud.com/data/forums/attachment/forum/202111/10/161859lazmb9h9qwwajjoz.jpg) # 参考 [1]http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/markoop.hpp: L37~L54 [2]https://developer.arm.com/documentation/100941/0100/barriers [3]https://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/runtime/orderaccess.hpp:L243~L316 [4]http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/bca17e38de00/src/share/vm/gc_implementation/concurrentmarksweep/compactiblefreelistspace.cpp:L986~L1017 [5]http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/oop.inline.hpp:L403~L481 [6]http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/gc_implementation/concurrentmarksweep/concurrentmarksweepgeneration.cpp:L1354https://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/runtime/orderAccess.hpp:L243~L316 [4]http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/bca17e38de00/src/share/vm/gc_implementation/concurrentmarksweep/compactiblefreelistspace.cpp:L986~L1017 [5]http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/oops/oop.inline.hpp:L403~L481 [6]http://hg.openjdk.java.net/jdk8/jdk8/hotspot/file/87ee5ee27509/src/share/vm/gc_implementation/concurrentmarksweep/concurrentmarksweepgeneration.cpp:L1354 原文转载自openEuler-[看看毕昇 JDK 团队是如何解决 JVM 中 CMS 的 Crash](https://mp.weixin.qq.com/s/RwAapBnaY5-FiZzPgGp4aQ)
  • [问题求助] Mind Studio1.0安装不成功
    MInd Studio1.0,按照操作手册配置环境执行命令,profile安装成功但mind studio安装不成功,查看日志显示是jdk密钥校验的问题,具体日志见附件。我安装了jdk,版本为1.8.0_292,麻烦帮忙看下怎么解决该问题。 [ERROR] [StudioHttp11NioProtocol 49]          - decrypt pass failed!!Key protection  algorithm not found: java.security.UnrecoverableKeyException: Encrypt Private Key failed: unrecognized algorithm name: PBEWithSHA1AndDESede2021-10-18 10:42:02,650[main]             [ERROR] [StudioHttp11NioProtocol 50]          - failed caused byjava.security.KeyStoreException: Key protection  algorithm not found: java.security.UnrecoverableKeyException: Encrypt Private Key failed: unrecognized algorithm name: PBEWithSHA1AndDESede
  • [技术干货] 打开一个表格,JDK拉满CPU
    打开一个表格的时候,JDK会直接拉满CPU,设计器也卡住不能操作,多次操作可能会报设计器内部异常,尝试重装设计器,配置环境变量都没得到解决,但在别人电脑上打开这个文件又不会出现这个问题。
  • [技术干货] 安装CTI用户的JDK
    由于部分业务需要在IVR流程里面调用jar包,所以需要在IVR服务器上安装JDK。首先在/home/cti/目录下,新建一个JDK的文件夹,上传JDK压缩包并解压到当前目录。然后编辑/home/cti/目录下的.cshrc文件,在末尾加上代码:setenv JAVA_HOME /home/cti/JDK/jre setnev PATH .:$JAVA_HOME/bin:$PATH setenv CLASSPATH .:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar加上代码后如图1所示,然后执行source .cshrc命令。图1 在/home/cti/.cshrc文件里面配置CTI用户的JDK环境变量最后,执行java -version命令检查cti用户的JDK环境变量是否配好。
  • [问题求助] spec jvm使用毕昇jdk报javac检查不过
    看毕昇jdk的介绍,在specjvm上性能有4.6%提升。安装配置毕昇jdk 11# java -versionopenjdk version "11.0.10" 2021-02-27OpenJDK Runtime Environment Bisheng (build 11.0.10+13)OpenJDK 64-Bit Server VM Bisheng (build 11.0.10+13, mixed mode)运行specjvm测试,报javac检查不过,请问怎么解决?The Javac version test in check failed.The Javac version must be the one included in SPECjvm2008.另外请问xms等堆栈参数能否配置在specjvm.properties中,如何配置?
  • [毕昇JDK] 【毕昇 JDK】毕昇 JDK 四大关键特性解读
    毕昇 JDK 是华为基于 OpenJDK 优化后的开源版本,是一款高性能、可用于生产环境的 OpenJDK 发行版。毕昇 JDK 稳定运行在华为内部 500 多个产品上,毕昇 JDK 团队积累了丰富的开发经验,解决了许多实际业务中由原生 OpenJDK 缺陷引起的问题。毕昇 JDK 致力于为 Java 开发者提供一款稳定可靠、高性能、易调测的 JDK,也为用户在鲲鹏 AArch64 架构上提供一个更好的选择。本文基于毕昇 JDK8 和毕昇 JDK11 版本特性进行介绍。了解毕昇 JDK毕昇 JDK 是 OpenJDK 的下游,对华为内部一些应用场景上遇到的性能和稳定性问题进行了修复,并针对鲲鹏 AArch64 架构进行了稳定性增强和性能优化,尤其在大数据场景下性能更好。License:采用 GPLv2 with Classpath Exception 协议。支持 Java 版本:目前毕昇 JDK 支持 8 和 11 两个 LTS 版本。支持架构:支持鲲鹏 AArch64 架构,毕昇 JDK 开源代码支持 x86 版本自构建。支持操作系统:目前仅支持 Linux 版本,对操作系统的要求是鲲鹏  AArch64 平台上 glibc 版本不低于 2.17,基本覆盖所有主流操作系统,已验证的 OS 列表,可参考鲲鹏社区产品页的兼容性查询工具。毕昇 JDK 新增特性在 OpenJDK 基础上,毕昇 JDK 在鲲鹏 ARM 架构上进行了性能优化,新增了快速序列化、AppCDS、G1GC 堆内存伸缩、KAE Provider 等特性,充分提升毕昇 JDK 在鲲鹏 ARM 架构下的运行性能。下文将对这四项优势特性进行详细的介绍。快速序列化——提升原生序列化性能问题背景:在一些无法使用 Kyro(无法修改代码时),需要使用 OpenJDK 原生序列化特性的场景,OpenJDK 原生的序列化机制会耗时较长,导致性能较低。毕昇 JDK8&11 通过实行快速序列化特性提升其性能,快速序列化实现原理大致如下图:快速序列化的实现原理通过减少序列化数据字段和提供数据缓存的方式提升 OpenJDK 原生序列化的性能,详细介绍可参考毕昇 JDK 社区快速序列化特性介绍,该特性在毕昇 JDK8&11 都支持。场景建议:业务中序列化/反序列化部分占比较高的场景可以考虑使用。比如:readObject/ writeObject 热点方法占比较高,可以尝试使能该特性。重复对象越多,场景收益越大。实测在某序列化占比较高场景,快速序列化特性较原生序列化性能收益提升15%。使能方法:-XX:+UnlockExperimentalVMOptions-XX:+UseFastSerializer -DfastSerializerEscapeMode=trueAppCDS——提升 java 应用启动速度在 Java 程序运行初始阶段,类的加载是一个比较耗时的过程,且在每次程序运行中均需要执行一遍。而 CDS(Class Data Sharing)技术,就是把类加载后的数据保存到文件中,下次运行时,直接将加载后的类数据从文件中恢复到内存中,不需要再重新执行类的加载过程,从而提高性能。AppCDS 特性在原 CDS 特性基础上增加了对应用类的支持,实现原理如下图所示:AppCDS 的实现原理从实现原理图可以看到,AppCDS 特性通过从 JSA 文件读取共享数据,省略了共享类的加载过程。同时 Shared Memory 支持多进程间共享,也减少对内存的占用。具体使能方式可参考毕昇 JDK 社区关于 AppCDS 特性的使用介绍。G1GC 堆内存伸缩——及时释放空闲堆内存在 OpenJDK 社区的 8u 版本中,即使 G1GC 在空闲堆内存没有被使用时,也不会主动及时归还给 OS,会造成内存资源占用浪费情况。由于 G1 尽可能避免触发 Full GC,因此在许多情况下,除非强制从外部执行 Full GC,否则 G1 不会将空闲的 Java 堆内存释放给操作系统。毕昇 JDK8 通过在 G1 中引入堆内存伸缩特性,在应用程序 CPU 占比不高情况下,定期尝试释放 G1 的空闲堆内存空间给 OS,达到内存资源的最优使用。特性开启后内存释放示意图上图为某业务使能该特性后实测的 JProfile 示意图,可见空闲内存会被平滑释放。在产品线某业务场景下运行 49 个微服务,实测开启 G1 堆内存回收特性比默认 G1GC 的实际物理内存减少 40%。在默认情况下,毕昇 JDK8 不主动开启此功能,对于延迟和吞吐量敏感的业务,不会受到影响。使能方式:-XX:+UseG1GC –XX:+G1UncommitKAE Provider——支持鲲鹏硬加速/提升加解密速度KAE 加解密是鲲鹏加速引擎的加解密模块,鲲鹏硬加速模块实现了 RSA/ SM3/ SM4/ DH/ MD5/ AES等算法,提供了高性能对称加解密、非对称加解密算法能力,兼容 openssl1.1.1a 及其之后版本,支持同步和异步机制。毕昇 JDK 8 通过利用 Provider 机制,实现对鲲鹏服务器 KAE 加解密特性的支持,以帮助用户提升在鲲鹏 AArch64 服务器加解密业务的竞争力,特性实现原理可参考下图,详细介绍可参考毕昇 JDK 社区关于 KAE Provider 特性说明。KAE Provider 特性实现原理该特性在某 web 中间件业务场景中,性能收益较 OpenJDK 原生特性提升90%。支持算法列表:算法说明摘要算法包括 MD5、SHA256、SHA384、SM3对称加密算法 AES支持 ECB、CBC、CTR、GCM 模式对称加密算法 SM4包括 ECB、CBC、CTR、OFB 模式HMac包括 HmacMD5、HmacSHA1、HmacSHA224、HmacSHA256、HmacSHA384、HmacSHA51非对称加解密算法 RSA支持512、1024、2048、3072、4096位秘钥大小DH包括 DHKeyPairGenerator 和 DHKeyAgreement,支持512、1024、2048、3072、4096位秘钥ECDH包括 ECKeyPairGenerator 和 ECDHKeyAgreement,支持曲线secp224r1、prime256v1、secp384r1、secp521r1RSA 签名包括 RSASignature 和 RSAPSSSignature,私钥只支持 RSAPrivateCrtKey其他特性毕昇 JDK 其他特性如:G1GC 支持 Numa-Aware、毕昇 JDK 11 AArch64 版本支持的 ZGC、G1 Full GC 优化、Jmap 支持并行扫描等详情请点击毕昇JDK产品页获取。毕昇 JDK8 和 11 均已开源,并每隔 3 个月进行版本升级和新特性合入,欢迎来毕昇 JDK 开源社区获取最新信息和技术讨论。如果遇到相关技术问题(包括但不限于毕昇 JDK),也可以通过毕昇 JDK 社区进行讨论。原文转载自 华为计算-【鲲鹏 DevKit 黑科技揭秘】┃毕昇 JDK 4大关键特性解读