-
## 作者:宋尧飞 > 编者按:笔者使用 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)
-
## 作者: 王帅 > 编者按:笔者遇到一个非常典型 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)
-
1、下载openjdkwget https://github.com/AdoptOpenJDK/openjdk8-binaries/releases/download/jdk8u252-b09/OpenJDK8U-jdk_aarch64_linux_hotspot_8u252b09.tar.gztar -zxf OpenJDK8U-jdk_aarch64_linux_hotspot_8u252b09.tar.gzmkdir -p /home/data/openjdk/mv jdk8u252-b09 /home/data/openjdk/2、配置环境变量vim /etc/profile在文件末尾添加环境变量配置代码export JAVA_HOME=/home/data/openjdk/jdk8u252-b09export PATH=$JAVA_HOME/bin:$PATH:wq保存并退出。3、检查openjdk是否安装成功java -version 4、下载tomcat在tomcat官网下载相应的版本。wget https://mirrors.bfsu.edu.cn/apache/tomcat/tomcat-8/v8.5.69/bin/apache-tomcat-8.5.69.tar.gztar -zxf apache-tomcat-8.5.69.tar.gz#重命名mv apache-tomcat-8.5.69 tomcat-zgshh5、开启8080端口firewall-cmd --state 显示已开启状态。#若没有开启,则输入以下命令开启systemctl start firewalld.service;#开启8080端口firewall-cmd --zone=public --add-port=8080/tcp --permanent;#重启防火墙systemctl restart firewalld.service;#重载配置firewall-cmd --reload; 在华为云控制台查看对应的安全组,将8080端口加入入方向规则。6、开启tomcat服务/home/data/tomcat-zgssh/bin/startup.sh至此tomcat安装完成。
-
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,设计器也卡住不能操作,多次操作可能会报设计器内部异常,尝试重装设计器,配置环境变量都没得到解决,但在别人电脑上打开这个文件又不会出现这个问题。
-
由于部分业务需要在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环境变量是否配好。
-
看毕昇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 是华为基于 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大关键特性解读
-
Java的发展从1995年开始经历了许多的过程,但是有三个最具有代表性的JDK版本;JDK1.0:标志着java的诞生;JDK1.2:加入了javax.swing组件,这是主要新特性;JDK1.5:标记为tiger,出现了许多一直沿用至今的特性;JDK1.8:Lambda表达式、接口的定义加强已经接触过了一些新特性,例如:自动装箱与拆箱、switch对String的判断支持。
-
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
-
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 (抑制没被使用过的代码的警告)
-
【功能模块】【操作步骤&问题现象】无法安装JDK1.8,提示“无法执行二进制文件”【截图信息】【日志信息】(可选,上传日志内容或者附件)
-
作者:宋尧飞编者按:JNI 是 Java 和 C 语言交互的主要手段,要想做好 JNI 的编程并不容易,需要了解 JVM 内部机理才能避免一些错误。本文分析 Cassandra 使用 JNI 本地库导致 JVM 崩溃的一个案例,最后定位问题根源是信号的错误处理(一些 C 编程人员经常会截获信号,做一些额外的处理),该案例提示 JNI 编程时不要随意截获信号处理。现象在使用 Cassandra 时遇到运行时多个位置都有发生 crash 现象,并且没有 hs_err 文件生成,这里列举了其中一个 crash 位置:分析首先直接基于上面这个 crash 的 core 文件展开分析,下面分别是对应源码上下文和指令上下文:使用 GDB 调试对应的 core 文件,如下图所示:在 GDB 中进行单步调试(GDB 调试可以参考官方文档),配合源代码发现 crash 的原因是传入的 name 为 null,导致调用 name.split("\_") 时触发了 SIGSEGV 信号,直接 crash。暂时抛开这个方法传入 name 为 null 是否有问题不论,从 JVM 运行的机制来说,这里有个疑问,遇到一个 Null Pointer 为什么不是抛出 Null Pointer Exception(简称 NPE)而是直接 crash 了呢?这里有一个知识需要普及一下:Java 层面的 NPE 主要分为两类,一类是代码中主动抛出 NPE 异常,并被 JVM 捕获 (这里的代码既可以是 Java 代码,也可以是 JVM 内部代码);另一类隐式 NPE(其原理是 JVM 内部遇到空指针访问,会产生 SIGSEGV 信号, 在 JVM 内部还会检查运行时是否存在 SIGSEGV 信号)。带着上面的疑问,又看了几处其他位置的 crash,发现都是因为对象为 null 导致的 SIGSEGV,却都没有抛出 NPE,而是直接 crash 了,再结合都没有 hs_err 文件生成的现象, hs_err 文件生成功能位于 JVM 的 SIGSEGV 信号处理函数中,代码如下:由于 hs_err 文件没有产生,一个很自然的推断:Cassandra 运行中可能篡改了或者捕获了 SIGSEGV 信号,并且可能做了处理,以至于 JVM 无法正常处理 SIGSEGV 信号。然后排查业务方是否在 Cassandra 中用到了自定义的第三方 native 库,果然笔者所猜测的,有两个 native 库里都对 SIGSEGV 信号做了捕获,注释掉这些代码后重新跑对方的业务,crash 现象不再发生,问题(由于 Cassandra 中对 NPE 有异常处理导致 JVM 崩溃)解决。总结C/C++ 的组件在配合 Java 一起使用时,需要注意的一点就是不要随意去捕获系统信号,特别是 SIGSEGV、SIGILL、SIGBUS 等,因为会覆盖掉 JVM 中的信号捕获逻辑。附录 这里贴一个 demo 可以用来复现 SIGSEGV 信号覆盖造成的后果,有兴趣的可以跑一下:// JNITest.java import java.util.UUID; public class JNITest { public static void main(String[] args) throws Exception { System.loadLibrary("JNITest"); UUID.fromString(null); } }// JNITest.c #include <signal.h> #include <jni.h> JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) { signal(SIGSEGV, SIG_DFL);//如果注释这条语句,在运行时会出现NullPointerExcetpion异常 return JNI_VERSION_1_8; }通过 GCC 编译并执行就可以触发相同的问题,编译执行命令如下:$ gcc -Wall -shared -fPIC JNITest.c -o libJNITest.so -I$JAVA_HOME/include -I$JAVA_HOME/include/linux $ javac JNITest.java $ java -Xcomp -Djava.library.path=./ JNITest后记如果遇到相关技术问题(包括不限于毕昇 JDK),可以进入毕昇 JDK 社区查找相关资源,包括二进制下载、代码仓库、使用教学、安装、学习资料等。毕昇 JDK 社区每双周周二举行技术例会,同时有一个技术交流群讨论 GCC、LLVM、JDK 和 V8 等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复 Compiler 入群。原文转载自 openEuler-JNI 中错误的信号处理导致 JVM 崩溃问题分析
-
at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116) at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93) at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)2021-08-06 10:26:10,057 [ 24656] ERROR - llij.ide.plugins.PluginManager - MindStudio 3.0.1 Build #MS-202.8194.7.1 2021-08-06 10:26:10,057 [ 24656] ERROR - llij.ide.plugins.PluginManager - JDK: 1.8.0_292; VM: OpenJDK 64-Bit Server VM; Vendor: Private Build 2021-08-06 10:26:10,057 [ 24656] ERROR - llij.ide.plugins.PluginManager - OS: Linux 2021-08-06 10:26:10,057 [ 24656] ERROR - llij.ide.plugins.PluginManager - Last Action: ModelConvert mindstudio3.0.1版本,感觉是JDK配置的问题,请问重新配置JDK的资料文档在那里?zhengzheng1215 发表于2021-08-06 10:30:40 2021-08-06 10:30:40 最后回复 pengcheng20320 2021-08-21 09:39:521661 7
上滑加载中
推荐直播
-
全面解析华为云EI-API服务:理论基础与实践应用指南
2024/11/29 周五 18:20-20:20
Alex 华为云学堂技术讲师
本期直播给大家带来的是理论与实践结合的华为云EI-API的服务介绍。从“主要功能,应用场景,实践案例,调用流程”四个维度来深入解析“语音交互API,文字识别API,自然语言处理API,图像识别API及图像搜索API”五大场景下API服务,同时结合实验,来加深开发者对API服务理解。
回顾中 -
企业员工、应届毕业生、在读研究生共探项目实践
2024/12/02 周一 19:00-21:00
姚圣伟 在职软件工程师 昇腾社区优秀开发者 华为云云享专家 HCDG天津地区发起人
大神带你一键了解和掌握LeakyReLU自定义算子在ONNX网络中应用和优化技巧,在线分享如何入门,以及在工作中如何结合实际项目进行学习
即将直播 -
昇腾云服务ModelArts深度解析:理论基础与实践应用指南
2024/12/03 周二 14:30-16:30
Alex 华为云学堂技术讲师
如何快速创建和部署模型,管理全周期AI工作流呢?本期直播聚焦华为昇腾云服务ModelArts一站式AI开发平台功能介绍,同时结合基于ModelArts 的实践性实验,帮助开发者从理论到实验更好地理解和使用ModelArts。
去报名
热门标签