• [毕昇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、【截图信息】【日志信息】(可选,上传日志内容或者附件)
  • [新手课堂] JDK中的注解
    JDK中的主键比较多,下面列举一些进行说明@Override:检查被该注解注释的是否是继承自父类@Deprecated:表示被注释的内容已经过时,不过还可以用@SuppressWarnings:压制警告@SuppressWarnings,压制警告属性:all to suppress all warnings (抑制所有警告)boxing to suppress warnings relative to boxing/unboxing operations(抑制装箱、拆箱操作时候的警告)cast to suppress warnings relative to cast operations (抑制映射相关的警告)dep-ann to suppress warnings relative to deprecated annotation(抑制启用注释的警告)deprecation to suppress warnings relative to deprecation(抑制过期方法警告)fallthrough to suppress warnings relative to missing breaks in switchstatements(抑制确在switch中缺失breaks的警告)finally to suppress warnings relative to finally block that don’t return (抑制finally模块没有返回的警告)hiding to suppress warnings relative to locals that hide variable()incomplete-switch to suppress warnings relative to missing entries in a switchstatement (enum case)(忽略没有完整的switch语句)nls to suppress warnings relative to non-nls string literals(忽略非nls格式的字符)null to suppress warnings relative to null analysis(忽略对null的操作)rawtypes to suppress warnings relative to un-specific types when using genericson class params(使用generics时忽略没有指定相应的类型)restriction to suppress warnings relative to usage of discouraged or forbiddenreferencesserial to suppress warnings relative to missing serialVersionUID field for aserializable class(忽略在serializable类中没有声明serialVersionUID变量)static-access to suppress warnings relative to incorrect static access(抑制不正确的静态访问方式警告)synthetic-access to suppress warnings relative to unoptimized access from innerclasses(抑制子类没有按最优方法访问内部类的警告)unchecked to suppress warnings relative to unchecked operations(抑制没有进行类型检查操作的警告)unqualified-field-access to suppress warnings relative to field accessunqualified (抑制没有权限访问的域的警告)unused to suppress warnings relative to unused code (抑制没被使用过的代码的警告)
  • [技术干货] 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.8我卸载后,怎么装不上1.7版本
  • [新手课堂] JDK中的注解
    JDK中的主键比较多,下面列举一些进行说明@Override:检查被该注解注释的是否是继承自父类@Deprecated:表示被注释的内容已经过时,不过还可以用@SuppressWarnings:压制警告@SuppressWarnings,压制警告属性:all to suppress all warnings (抑制所有警告)boxing to suppress warnings relative to boxing/unboxing operations(抑制装箱、拆箱操作时候的警告)cast to suppress warnings relative to cast operations (抑制映射相关的警告)dep-ann to suppress warnings relative to deprecated annotation(抑制启用注释的警告)deprecation to suppress warnings relative to deprecation(抑制过期方法警告)fallthrough to suppress warnings relative to missing breaks in switchstatements(抑制确在switch中缺失breaks的警告)finally to suppress warnings relative to finally block that don’t return (抑制finally模块没有返回的警告)hiding to suppress warnings relative to locals that hide variable()incomplete-switch to suppress warnings relative to missing entries in a switchstatement (enum case)(忽略没有完整的switch语句)nls to suppress warnings relative to non-nls string literals(忽略非nls格式的字符)null to suppress warnings relative to null analysis(忽略对null的操作)rawtypes to suppress warnings relative to un-specific types when using genericson class params(使用generics时忽略没有指定相应的类型)restriction to suppress warnings relative to usage of discouraged or forbiddenreferencesserial to suppress warnings relative to missing serialVersionUID field for aserializable class(忽略在serializable类中没有声明serialVersionUID变量)static-access to suppress warnings relative to incorrect static access(抑制不正确的静态访问方式警告)synthetic-access to suppress warnings relative to unoptimized access from innerclasses(抑制子类没有按最优方法访问内部类的警告)unchecked to suppress warnings relative to unchecked operations(抑制没有进行类型检查操作的警告)unqualified-field-access to suppress warnings relative to field accessunqualified (抑制没有权限访问的域的警告)unused to suppress warnings relative to unused code (抑制没被使用过的代码的警告)
  • [技术干货] ZGC的现状和未来以及毕昇JDK对ZGC的支持
    Z Garbage Collect,简称ZGC,早期是Oracle的一个内部项目,在2017年Oracle公司决定将ZGC进行开源。在JDK11(JDK在本文中特指Oracle公司发布的JDK,下同)时,ZGC代码正式合入到主干分支,成为一款标准的官方垃圾回收器。ZGC发布时,由于功能、性能等方面均不完全成熟,所以是一款“实验性质”的垃圾回收器。在经过了JDK的4个迭代开发之后,在JDK15中,ZGC正式升级为一款可用于生产环境的垃圾回收器。JDK中已经包含了不少的垃圾回收器实现,例如Parallel Scavenge(简称PS), Concurrent-Mark-Sweep(简称CMS), Garbage First(简称G1)等。为什么ZGC能够被引入到JDK中?ZGC的定位是一款可扩展性、低时延的垃圾回收器,它的主要目标有3个,分别是:1. 支持超大堆内存(TB级别),目前ZGC可以支持堆内存的范围在8MB到16TB之间;2. 最大停顿时间在毫秒级,即不超过10毫秒;3. 停顿时间不会随着堆内存的增加而增加。注:垃圾回收执行时,需要识别垃圾对象,并将活跃对象进行移动。为了防止垃圾回收器和应用程序同时访问内存,造成内存访问的不一致性,在垃圾回收过程需要应用暂停运行,应用程序暂停运行的时间被称为停顿时间。1 ZGC算法介绍对于一款垃圾回收器来说支持TB级别内存也许并不困难,但是要想在TB级别的内存空间中,在毫秒级别停顿时间完成垃圾回收则是不可想象的。例如在一台配置一般的机器上,通常直接通过memcpy这样的函数进行内存复制的速度大概在GB/sec这样的水平。而垃圾回收过程需要大量的复制内存,在TB级别的堆空间完成数百GB或者几TB的内存复制,但把停顿时间控制在毫秒级别,听起来完全不可能。那么ZGC是怎么做到的呢?最关键的一点就是并发处理。并发处理贯穿在整个垃圾回收的全过程,ZGC的垃圾回收分为3个阶段:标记(Mark)、转移(Relocate)、重定位(Remap)。ZGC对这个3个阶段都采用了并发处理,只有在必须暂停的地方才会暂停应用的执行。这3个阶段完成的主要功能分别是:• 标记:识别堆内存空间中的活跃对象• 转移:将内存空间中的活跃对象转移到一块新的内存空间中,对象原来的空间已被回收• 重定位:对象的位置发生了变化,对象的引用关系应该更新,确保对象之间的引用都指向对象新的位置另外在算法实现中,标记阶段和重定位阶段都是针对活跃对象进行的。当活跃对象转移完成以后,当在对活跃对象进行重定位的同时标记活跃对象。这样就把上一次垃圾回收阶段的重定位和本次垃圾回收的标记进行合并,从而优化了垃圾回收的执行过程。ZGC中3个阶段执行示意图如下所示:要完美的支持并发的标记和转移并非易事,为了方便的支持并发,ZGC中引入了Colored Pointer和Load Barrier技术。所谓的Colored Pointer机制指的是ZGC将不同阶段的对象放在地址空间中,通过地址位来标记对象所处的空间,然后不同的地址空间映射到同一物理空间中。以16TB的堆在aarch64系统为例,地址空间划分如下:地址空间的区分通过地址位实现,如下所示:3个逻辑视图对应同一物理视图在ZGC中是一个非常巧妙的设计。这里通过一个例子介绍一下这3个视图的作用,如下图所示:假定在初始阶段地址视图为Remapped,此时产生的对象都位于Remapped空间,按照图3的介绍,第46地址位会被设置为1.当ZGC执行垃圾回收时,此时将进入标记阶段,假设标记阶段地址视图为Marked0,即第44地址位为1.这意味在标记阶段完成后,所以活跃对象的地址的第44位都被设置1.假设一个对象在Remapped视图产生,在标记完成地址视图变成Marked0,一般的做法是将Remapped视图中的对象复制到Marked0,而复制是非常耗时的,所以ZGC不希望发生真实的对象复制,而是把不同的地址视图映射到同一物理空间,当需要从Remapped到Marked0复制是,仅仅把地址位的第46设置0,同时把第44位设置为1,这就和对象真的复制一样的结果,从而大大的节约了时间。ZGC里面有两个标记视图Marked0和Marked1是为了区别上一次标记和本次标记对象是否仍然存活。具体不进一步展开,可以参考相关书籍和文献。在垃圾回收(也称为Collector)和应用线程(也称为Mutator)并发访问同一对象时,为了正确处理一致性,ZGC采用了“目标空间”一致性的做法。即当Collector和Mutator访问对象时,都保证它们访问的是对象的目标空间的对象,也就是说当对象的发生空间变化时,首先进行空间变化然后再访问对象。这里就用到了Load Barrier技术,Load Barrier会执行额外的动作确保对象都位于目标空间,Load Barrier是否执行额外动作的前提是访问对象的视图是否符合预期。当对象的视图符合预期时,Load Barrier不会执行额外的动作,这也称为Good Path;当对象的视图不符合预期时才会执行额外的动作,也称为Slow Path。对于Slow Path来说不同阶段执行的动作不同,在标记/重定位阶段,Load Barrier可能会执行标记/重定位的动作,在转移阶段,Load Barrier可能会执行转移的动作。关于Load Barrier更详细的介绍,可以参考相关书籍和文献。除了并发执行这个特点之外,ZGC在内存管理时采用了分区的管理形式,使得内存的管理更为灵活。同时完善了NUMA-Aware的功能,在NUMA系统中能取得更好的性能。2 ZGC发展历程的关键里程从2018年9月JDK11发布到2020年9月JDK15发布,ZGC是整个JDK中合入功能最多的特性。社区对于ZGC的认可度也越来越高,部分公司已经开始在生产环境尝试使用ZGC,并且取得不错的效果。下面稍微梳理一下ZGC在过去2年的5个版本中发布的重要功能。1. JDK11:在该版本中,ZGC作为实验性中的垃圾回收器引入。在初始发布中,ZGC仅支持Linux平台,且仅仅支持运行在64位系统之上。但在该版本中ZGC的整体框架已经全部实现,包括Colored Pointer,Load Barrier, NUMA-Aware等重要功能。2. JDK12:ZGC引入了一个最主要的功能,并发类卸载。从而大大降低了停顿时间。3. JDK13:引入了aarch64的支持,此时ZGC可以运行在x64和ARM平台的Linux之上。另外ZGC为了迎合现代云场景的诉求,加入了内存释放(归还给操作系统)的功能。4. JDK14:该版本最终的功能就是支持在x64平台的Windows和MacOS系统,从而使得多个平台可以使用ZGC。5. JDK15:该版本中ZGC成为一个生产可用的垃圾回收器。在该版本中增加了通用功能的支持,例如支持压缩指针(在小内存下性能更优),支持Class Data Sharing,支持堆空间使用NVRAM等。使得ZGC功能完备。在将于2021年3月发布的JDK16中,ZGC还将增加一个功能:并发线程栈扫描。这个特性将进一步的减少停顿时间。3 华为公司产品毕昇JDK对ZGC的支持由于Oracle公司对于JDK发布策略的变化,目前JDK11是一个长期支持版本(Long Term Support,简称LTS),而JDK12、13、14、15都是功能特性合入的开发版本,这些开发版本在新的版本发布之后都不在继续维护。下一个LTS的版本是2021年月的JDK17。由于JDK发布策略的变化,在JDK的升级和使用方面,大家趋向于使用LTS版本,而非开发版本。目前JDK存在两个LTS:JDK8和JDK11。另外Oracle公司对于JDK的使用策略也发生了变化,对于商业环境中使用JDK8U202之后的版本都需要购买Oracle的License(Oracle的云产品或者Oracle授权产品除外),目前License的价格大概是25美金/processor。如果在商业环境中使用JDK8U202之后的版本,但并未付费,是违反Oracle公司对于JDK的商业协议,可能会收到来自Oracle公司的诉求。JDK的开发是以OpenJDK项目为基础,而OpenJDK项目是完全开源的(许可证是 GPLv2+CE,也就是说使用者可以根据OpenJDK的源码开发自己的JDK,并开源自研的JDK)。基于上述原因以及其他外部原因,华为公司也基于OpenJDK开发了自己的产品:毕昇JDK,目前毕昇JDK已经在码云上开源。毕昇JDK致力于维护安全、稳定、高效的JDK。目前对于LTS的JDK8和JDK11都进行投入了大量的人力和物力。毕昇JDK全部通过JCK、FUZZ,JTreg等测试套。毕昇JDK运行在华为内部500多个产品上,积累了大量使用场景和Java开发者反馈的问题和诉求,解决了业务实际运行中遇到的多个问题,并在ARM架构上进行了性能优化,毕昇JDK运行在大数据等场景下可以获得更好的性能。这里以ZGC为例,稍微介绍几个毕昇JDK11相对于JDK11做了哪些工作。• 支持aarch64。在JDK11中ZGC仅支持x64平台,毕昇JDK11是目前所有开源JDK中唯一支持x64和aarch64的产品。• Bug修复。在整个运行和维护过程中,修复了一些bug,这些bug有些是社区在JDK14或者JDK15进行修复,有些我们是将社区的修复方案回合到毕昇JDK11,有些是我们在毕昇JDK11重新做了实现。举一个简单的例子,在我们的测试中发现,对于某些测试用例对于G1 GC和ZGC运行结果不一致,经过案例分析,对汇编代码进行跟踪,最终发现在ZGC的C1支持中对于浮点数寄存器使用有误,据此进行了修复。在毕昇JDK11上ZGC稳定性远远超过JDK11,可以在生产环境中使用。4 ZGC的未来发展点ZGC作为低时延垃圾回收器的杰出代表,正在努力将停顿时间减少到几毫秒。目前正在开发的是并发栈扫描。在当前的实现中栈作为垃圾回收的根集合之一,在扫描时需要暂停Mutator的执行。并发栈扫描是指Collector和Mutator都可以扫描栈,从而完成根集合处理,这将大大减少因为线程过多、调用栈复杂导致过长的停顿时间。由于Collector和Mutator并发访问栈,为了保持栈访问的一致性,引入了所谓watermark机制,该机制本质上也是一种barrier,ZGC的barrier通过记录SP的地址来判断操作运行栈是否发生变化。当然在并发处理中还需要考虑栈对象的完整性,比如参数、异常处理都会影响扫描栈的对象。并发栈扫描可以简单的总结为,当Mutator在接受到暂停请求后,会将栈顶的栈帧进行遍历,并设置watermark。当暂停结束后,如果Mutator访问栈帧,根据栈帧的SP和watermark进行比较,根据栈的增长方向可以判断栈帧是新栈帧(以及扫描或者新创建的栈帧)还是老栈帧(尚未扫描的栈帧),对于老栈帧Mutator会扫描“当前”正在栈帧(为了保证Mutator的正常运行,Mutator仅仅扫描“当前”栈帧,这里当前是指当前运行的完整的栈帧,为了便于处理参数和异常可能会额外扫描调用者的栈帧),扫描完成后Mutator继续执行,而把栈帧继续扫描的任务留给Collector。ZGC非常完美的实现了低时延的需求,把尽可能的工作并发执行,在并发执行的时候,通常需要Mutator“帮助”Collector,完成本应该由Collector完成的工作,也就说Mutator除了完成应用代码的执行还需要做一些额外的辅助工作,由于Mutator不能专心执行应用代码,这会造成ZGC在吞吐率方面的不足。另外目前ZGC是单代回收,更加放大了ZGC的吞吐率不足的缺点。要解决吞吐率的问题,通常的是方法是实现分代回收。另外一种方法是线程局部回收(即Thread Local Garbage Collection,简称为TLGC),TLGC来说天然对于多核的系统更为友善。TLGC的核心思想是:每个Mutator在发现内存不足时,优先回收Mutator自己分配的内存。TLGC的另一个好处是不需要全局的暂停,故不会增加停顿时间。在实际应用中,Mutator分配的对象可能被另外一个Mutator使用,这就所谓的逃逸对象,对于逃逸对象在TLGC中不能回收。这里逃逸分析还会涉及到真逃逸、假逃逸,所谓假逃逸指的是对象可以被另外一个Mutator看到,但是在实际运行过程中并未访问。所以TLGC的核心是一套高效的逃逸分析技术,包含准确的识别假逃逸。目前华为毕昇JDK团队正在设计和实现ZGC的分代和TLGC。期望不久的将来大家可以在毕昇JDK中看到这些特性。5 参考文献%4. ZGC官网信息,https://wiki.openjdk.java.net/display/zgc/ %4. 毕昇JDK11码云地址,https://gitee.com/openeuler/bishengjdk-11 %4. 图书《新一代垃圾回收器ZGC设计与实现》
  • [技术干货] JVM 中不正确的类加载顺序导致应用运行异常问题分析
    编者按:两位笔者分享了不同的案例,一个是因为 JDK 小版本升级后导致运行出错,最终分析定位原因为应用启动脚本中指定了 Classpath 导致 JVM 加载了同一个类的不同版本,而 JVM 在选择加载的类则是先遇到的先加载,进而导致应用出错,该问题的根因是设置了错误的 Classpath。第二个案例是在相同的 OS、JDK 和应用,不同的文件系统导致应用运行的结果不一致,最终分析定位的原因是 JVM 在加载类时遇到了多个版本的问题,但是该问题的根因是没有指定 Classpath,JVM 加载类会依赖于 OS 读取文件的顺序,而不同的文件系统导致提供文件的顺序不同,导致了问题的发生。经过这两个案例的分析,可以得到 2 个结论:需要指定 Classpath 避免不同文件系统(或者 OS)提供不同的文件顺序;需要正确地指定 Classpath,避免加载错误的类。希望读者可以了解 JVM 加载类文件的基本原理,避免出现类似错误。1 问题1现象某产品线进行 JDK 升级,将 JDK 版本从 8u181 升级到 8u191 后,日志中报出大量 java.lang.NoSuchFieldError,导致基本服务功能不可用。 具体报错如下:2 分析从这个调用栈来看,问题可能出现在 JDK 自身,并没有涉及到业务代码。首先自然应该去看一下 ClientHandshaker.java 的源码,确认一下出错时的上下文。先看 JDK 8u191 的代码,相关如下:可以看到,虽然这里对 handshakeState 进行了 check,但是代码中完全没有出现 state 这个变量;那么 8u181 又如何呢?继续看代码:这里的实现方式和 8u191 中有明显的不同,其中最重要的一点是,在 194 行中确确实实访问了 state 这个变量。追踪一下代码可以得知,ClientHandshaker 类继承自 Handshaker 类,state 也是从父类之中继承过来的一个 field。 于是,可以得到一个初步的结论:JDK 8u191 中,ClientHandshaker 的实现方式与 8u181 不同,去除了 state 这个 field。既然报错报了这个 field,因此可以确定,JVM 中加载的 ClientHandshaker 肯定不是 8u191 中的这一个。 那么,可能是产品线在替换 JDK 时,没有替换完全,导致残留了一部分 8u181 的东西,让 JVM 加载了?这个猜测很快就被否定了,因为行号对不上:错误栈中的行号是 198,而 8u181 代码中对 state 的访问是在 194 行。因此,为了直接推进问题,最好的办法就是确定 JVM 到底是从哪里加载了 ClientHandshaker。在 java 启动命令中加入如下参数,就可以追踪每一个 class 的加载:java -XX:+TraceClassLoading会产生类似于下面的输出:(加载的类 + 类的来源)从这个输出中搜索一下 ClientHandshaker,最终发现了这么一行:[Loaded sun.security.ssl.ClientHandshaker from /mypath2/lib/alpn-boot.jar]果然,出问题的 ClientHandshaker 并不是加载自 JDK 8u191 中,而是加载自 alpn-boot.jar 这个包。那么这个包又是从哪里找到的?检查了一下产品线的 java 启动命令,发现里面用 -cp 参数指定了许多 Classpath 路径,最后从里面找到了 "/mypath2/lib/alpn-boot.jar"。到这个目录下,找到产品线所使用的 jar 包,然后将其中包含的 ClientHandshaker.class 反编译后,发现代码基本与 8u181 代码相同——也访问了 state,并且连行号(198)也能对应上。到此,根因基本确定。alpn-boot.jar 是 Jetty 中用来实现 TLS 的扩展。产品线当时所使用的 alpn 版本是 8.1.12.v20180117,根据官方文档,这个版本只能兼容到 JDK 8u181,而 8u191 之后,alpn 的版本也应有相应的变化,以兼容新的 JDK 代码。 为什么当时 alpn 没有自动适应 JDK 版本?因为产品的启动脚本里写死了那个老版本的 alpn-boot.jar,而在升级的时候却没有适配启动脚本。3 问题2现象笔者将相同的 java 应用和 JDK 部署在 Linux 环境中,一台机器上运行正常,另外一台和预期不一样。对于这个现象我们非常奇怪,从问题表象应该是外部环境导致了运行结果不同,为此我们对软件、硬件、环境进行排查,发现 2 台机器除了使用的文件系统不同外,其他并无不同。为什么不同的文件系统会影响 JVM 的运行结果?根源在哪里?通过排查发现有两个 jar 中存在全限定名相同的两个主类,那么 JDK 会去选择哪个主类加载呢,对于 Classpath 通配符 JDK 又是如何解析的,下面笔者对上面问题进行分析。4 环境介绍准备两台 Linux 机器,其中一台文件系统为 ext4,另一台为 xfs。可以使用 df -T 命令查看使用的文件系统。复现问题的方式非常简单,可以创建两个全限定名一样的类,然后编译打成 jar 包放在同一个目录中。运行 java 进程时,指定的 Classpath 路径使用通配符。可以通过下面的 demo 复现这个问题。• Demo 的目录结构moduleA Main.javamoduleB Main.java• 编译及打包先使用 java 命令将这两个类编译,然后分别使用 jar 命令打包到 moduleA.jar 和 moduleB.jar 中,并保存在 lib 目录中。切换到 demo 目录,执行如下命令进行编译、打包。• 运行5 测试结果对于不同文件系统的环境,输出的结果可能不同,下面以 ext 和 xfs 文件系统为例,可以看到输出不同的结果。• ext4moduleA.jar 创建时间早于 moduleB.jar,输出 module B。moduleA.jar 创建时间晚于 moduleB.jar,输出 module B。无论是先创建 moduleA.jar 还是 moduleB.jar,最终的输出结果都是 module B。• xfsmoduleA.jar 创建时间早于 moduleB.jar,输出 module A。moduleA.jar 创建时间晚于 moduleB.jar,输出 module B。如果先创建 moduleA.jar,然后创建 moduleB.jar,输出的结果是 module A;反之,输出的结果是 module B。6 原因分析6.1 排查方法使用 JDK 8 进程启动时,添加 VM 参数 -XX:+TraceClassPaths -XX:+TraceClassLoading。其中以 ext 文件系统为例,可以得到如下的日志:从日志可以发现 Classpath 解析后的路径是 moduleB.jar 在 moduleA.jar 之前,并且加载的是 moduleB.jar 的类。对于解析后的 Classpath,还可以通过添加 -XshowSettings 选项查看。对于常驻进程,查看通配符解析后的 Classpath,可以使用 jcmd 命令查看。当然使用 jconsole 或者 visualvm 等工具连接进程也可以查看解析后的 Classpath。6.2 Classpath 通配符如何解析以 Linux 系统为例,在 Classpath 中使用冒号分割多个路径,并且按照定义的顺序进行通配符解析。如下所示,任何文件系统解析出来的路径始终是 lib 目录的 jar 包在 lib2 目录的 jar 包之前。JVM 在解析通配符 * 时,最终会调用系统函数 opendir、readdir 读取遍历目录。ext4 创建文件的顺序与实际 readdir 读取的顺序不一致的原因主要在于 ext 系列文件系统有个 feature,即 dir_index,用于加快查找目录项(可直接计算其 hash 值定位到它的目录项),目录项也便成了以 hash 值大小进行排序。通常 dir_index 默认开启,可以通过 / etc/mke2fs.conf 查看默认配置。创建一个 test_readdir.c 文件,用 C 语言实现一个 demo。通过调用系统 readdir 遍历目录,并且打印文件的 d_off、d_name 属性值。编译、执行命令如下所示:编译执行test_readdir.c 文件运行结果分别在 ext4、xfs 文件系统上按顺序创建 m1~m10 文件,然后查看运行结果。对比发现 readdir 函数在不同文件系统中都是按照 d_off 属性值从小到大顺序进行遍历,不同的是 xfs 文件 d_off 的值按照创建时间依次增大,而 ext4 和文件的创建顺序无关。ext4xfs6.3 解决办法&修复方法可以将需要加载的主类所在的 jar 包存放在新创建的文件目录 libmain 下,并且将libmain 目录放在 lib 目录之前,则指定 Classpath 路径的顺序如下所示:.:/home/user/demo/libmain/*:/home/user/demo/lib/*后记如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 社区查找相关资源(点击阅读原文进入官网),包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。
  • [技术干货] 使用 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 例子参考(例子仅作为帮助问题理解使用):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<Integer, Integer[]> 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);              }          }      }  }}上图是开启 - XX:+UseG1GC 选项,Java 进程内存增长曲线图。横坐标是内存使用的统计次数,每 10 分钟统计一次;纵坐标是 Java 进程占用物理内存的大小。从上图可以看出:物理内存持续增涨的速度很快,存在内存泄露问题。我们在设置了 jemalloc 的环境下,重新运行该测试用例: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 文件,命令如下: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 文件的命令时,部分环境会遇到类似如下错误:Dropping nodes with <= 2140452 B; edges with <= 428090 abs(MB)dot: command not found该问题的解决方法,需要在环境中安装 graphviz 和 gv:sudo apt install graphviz gv安装成功后,再次执行方法 1 中命令,可以得到可视化 svg 文件。测试用例执行三十分钟后,我们对最后十分钟的内存增长进行分析,结果发现:95.9% 的内存增长来自 G1DefaultParGCAllocator 的构造函数调用,这里的最后 10 分钟是和用例设置相关,如下图所示:​上图比较清晰显示了内存申请相关函数的调用关系以及内存申请比例和数量,约 95.9% 的堆内存是通过 G1DefaultParGCAllocator 完成申请,可以预测在 G1DefaultParGCAllocator 的构造函数中申请的内存没有被及时回收掉而导致内存泄漏的可能性非常大。这个时候可以通过代码协助分析了。jeprof 工具不仅可以查看详细信息或者生成调用路径图(如上图所示),还可以用来比较两个 dump 文件(显示增量部分),既然作为工具使用介绍,我们继续介绍另一种补充性分析方法:将连续两次的 heap 文件做差异对比,输出的 PDF 可视化文件可以进一步确定是哪里内存持续申请没有被释放而导致内存增长。方法如下:jeprof --base=jeprof.34070.0.i0.heap --pdf /home/xxxx/jdk1.8.0_292/bin/java jeprof.34070.1.i1.heap > diff.pdf内存增加差异图:​通过上图可以非常清晰看到:G1DefaultParGCAllocator 的构造函数持续申请内存,导致内存增长迅速。后续的工作就是针对 G1DefaultParGCAllocator 构造函数中内存申请情况,排查释放逻辑,寻找问题原因并解决,这块的工作不属于 jemalloc 范畴,本内容不再赘述。代码修复后 Java 进程物理内存使用情况如下(运行 30 小时 +):内存使用符合预期,问题解决。​通过 jemalloc 工具和上面介绍的方法,帮助开发快速解决了此特性引起 Native method 内存泄漏问题,方法使用简单。在实际业务中有遇到类似问题的同学,不妨亲自尝试一下。附录:jemalloc 的编译jemalloc 普通版并不包含 profiling 机制,所以需要下载源代码重新编译,在 configure 的时候添加了 --enable-prof 选项,这样才能打开 profiling 机制。(1) 下载最新版本 jemalloc 源码git clone https://github.com/jemalloc/jemalloc.git(2) jemalloc 源码构建(a) 修改 autogen.sh 文件,使能 prof,如下:diff --git a/autogen.sh b/autogen.shindex 75f32da6..6ab4053c 100755--- a/autogen.sh+++ b/autogen.sh@@ -9,8 +9,8 @@ for i in autoconf; do    fidone​-echo "./configure --enable-autogen $@"-./configure --enable-autogen $@+echo "./configure --enable-prof $@"+./configure --enable-prof $@if [ $? -ne 0 ]; then    echo "Error $? in ./configure"    exit 1执行:$ ./autogen.sh$ make -j 6以下命令可选:$ make install (b) 源码构建成功后(一般不会出错),会在当前目录的bin和lib目录下生成重要的文件:$ ls -ltotal 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 -ltotal 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.alrwxrwxrwx 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(c) 设置环境变量和执行权限bin 目录下的 jeprof 文件,没有执行权限,需要设置一下:bin$ chmod +x ./*退到 bin 的上一层目录设置环境变量,可参考如下方法:xxxx@hostname:jemalloc$ echo $JEMALLOC_DIR​xxxx@hostname:jemalloc$ export JEMALLOC_DIR=`pwd`xxxx@hostname:jemalloc$ echo $JEMALLOC_DIR/home/xxxx/jemalloc/jemallocxxxx@hostname:jemalloc$ export LD_PRELOAD=$JEMALLOC_DIR/lib/libjemalloc.soxxxx@hostname:jemalloc$ export MALLOC_CONF=prof:true,lg_prof_interval:30,lg_prof_sample:17xxxx@hostname:jemalloc$ which jeprofxxxx@hostname:jemalloc$ export PATH=$PATH:$JEMALLOC_DIR/binxxxx@hostname:jemalloc$ which jeprof/home/xxxx/jemalloc/jemalloc/bin/jeprofxxxx@hostname:jemalloc$ jeprof --versionjeprof (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 conditionsand license information.There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR APARTICULAR PURPOSE.到这一步,jeprof 可以在该环境中启动使用了。后记如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 社区查找相关资源(点击阅读原文进入官网),包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。参考%4. https://chenhm.com/post/2018-12-05-debuging-java-memory-leak %4. https://github.com/jemalloc/jemalloc %4. https://blog.csdn.net/qq_36287943/article/details/105491301 %4. https://www.cnblogs.com/minglee/p/10124174.html %4. https://www.yuanguohuo.com/2019/01/02/jemalloc-heap-profiling /
  • [新手课堂] JDK中的注解
    JDK中的主键比较多,下面列举一些进行说明@Override:检查被该注解注释的是否是继承自父类@Deprecated:表示被注释的内容已经过时,不过还可以用@SuppressWarnings:压制警告@SuppressWarnings,压制警告属性:all to suppress all warnings (抑制所有警告)boxing to suppress warnings relative to boxing/unboxing operations(抑制装箱、拆箱操作时候的警告)cast to suppress warnings relative to cast operations (抑制映射相关的警告)dep-ann to suppress warnings relative to deprecated annotation(抑制启用注释的警告)deprecation to suppress warnings relative to deprecation(抑制过期方法警告)fallthrough to suppress warnings relative to missing breaks in switchstatements(抑制确在switch中缺失breaks的警告)finally to suppress warnings relative to finally block that don’t return (抑制finally模块没有返回的警告)hiding to suppress warnings relative to locals that hide variable()incomplete-switch to suppress warnings relative to missing entries in a switchstatement (enum case)(忽略没有完整的switch语句)nls to suppress warnings relative to non-nls string literals(忽略非nls格式的字符)null to suppress warnings relative to null analysis(忽略对null的操作)rawtypes to suppress warnings relative to un-specific types when using genericson class params(使用generics时忽略没有指定相应的类型)restriction to suppress warnings relative to usage of discouraged or forbiddenreferencesserial to suppress warnings relative to missing serialVersionUID field for aserializable class(忽略在serializable类中没有声明serialVersionUID变量)static-access to suppress warnings relative to incorrect static access(抑制不正确的静态访问方式警告)synthetic-access to suppress warnings relative to unoptimized access from innerclasses(抑制子类没有按最优方法访问内部类的警告)unchecked to suppress warnings relative to unchecked operations(抑制没有进行类型检查操作的警告)unqualified-field-access to suppress warnings relative to field accessunqualified (抑制没有权限访问的域的警告)unused to suppress warnings relative to unused code (抑制没被使用过的代码的警告)
  • [毕昇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)
总条数:97 到第
上滑加载中