[转帖]【技术剖析】2. JVM锁bug导致G1 GC挂起问题分析和解决

技术,剖析,jvm,bug,导致,g1,gc,挂起,问题,分析,解决 · 浏览次数 : 0

小编点评

**JVM锁机制问题分析** **问题现象:** AArch64平台上,业务挂死,而进程占用CPU持续维持在300%的案例。配合top和gdb,可以看到是3个GC线程在offer_termination处陷入了死循环。 **问题分析:** 由于是弱内存模型,在并发编程中,多个GC线程可能同时访问公共内存。由于Monitor锁机制仅为线程提供加锁的机制,多个GC线程可能会竞争获取同一个锁,导致死锁。 **Monitor机制的基本原理:** * **上锁流程:**首先走快速上锁流程,申请者尝试获取锁。 * **自旋上锁流程:**如果快速上锁失败,尝试自旋上锁流程,在短时间内获取锁。 * **慢速上锁流程:**申请者加入等待队列,进入睡眠状态,等待被唤醒后完成上锁。 **问题解决方案:** 1. 确保多个GC线程在获取锁前进行排序或依赖关系分析。 2. 使用 proper synchronization mechanisms,例如互斥锁或条件变量,来确保资源共享。 3. 使用 Load-Acquire 或 Store-Release 的 barrier 来确保读写操作的同步。 **问题场景:** 线程A处于解锁流程中,由于乱序,先写入了继承者同时解除内部锁线程B处于上锁流程,发现自己就是法定继承者后,立刻完成上锁线程B又迅速进入解锁流程,并从_EntryList中取出头元素(也就是线程B!)作为继承者写入_OnDeck,完成解锁走人线程A此时才更新_EntryList,然后唤醒继承者(也就是线程B!),完成解锁走人_OnDeck上的继承者线程B,实际已经完成加解锁离开,后续等待线程再也无法被唤醒正巧在社区的高版本上找到了一个相关的修复记录。

正文

https://bbs.huaweicloud.com/forum/thread-144146-1-1.html

 

发表于 2021-07-29 20:07:087037查看

作者:宋尧飞

编者按:笔者在AArch64中遇到一个G1 GC挂起,CPU利用率高达300%的案例。经过分析发现问题是由JVM的锁机制导致,该问题根因是并发编程中没有正确理解内存序导致。本文着重介绍JVM中Monitor的基本原理,同时演示了在什么情况下会触发该问题。希望通过本文的分析,读者能够了解到内存序对性能、正确性的影响,在并发编程时更加仔细。

现象

本案例是一个典型的弱内存模型案例,大致的现象就是AArch64平台上,业务挂死,而进程占用CPU持续维持在300%。配合top和gdb,可以看到是3个GC线程在offer_termination处陷入了死循环:

 

多个并行GC线程在Minor GC结束时调用offer_termination,在offer_termination中自旋等待其他并行GC线程到达该位置,才说明GC任务完成,可以终止。(关于并行任务的中止协议问题,可以参考相关论文,这里不做着重介绍。简单地说,在并行任务执行时,多个任务之间可能存在任务不均衡,所以JVM内部设计了任务均衡机制,同时必须设计任务终止的机制来保证多个任务都能完成,这里的offer_termination就是尝试终止任务)。

在该案例中,部分GC线程完成自己的任务,等待其他的GC线程。此时出现挂起,很有可能是因为发生了死锁。所以问题很可能是由于那些尚未完成任务的GC线程上错误地使用锁。

所以使用gdb观察了一下其他GC线程,发现其他GC线程全都阻塞在一把JVM的锁上:

 

 

而这把Monitor中的情况如下:

  • cxq上积累了大量GC线程
  • OnDeck记录的GC线程已经消失
  • _owner记录的锁持有者为NULL

分析

在进一步分析前,首先普及一下JVM锁组件Monitor的基本原理,Monitor类主要包含4个核心字段:

  1. “Thread * volatile _owner;” 字段指向这把锁的持有线程
  2. “SplitWord _LockWord;” 字段被设计为1个机器字长,目的是为了确保操作时天然的原子性,它的最低位被设计为上锁标记位,而高位区域用来存放256字节对齐的竞争队列(cxq)地址
  3. “ParkEvent * volatile _EntryList;” 字段指向一个等待队列,跟cxq差别不大,个人理解只是为了缓解cxq的竞争压力而设计
  4. “ParkEvent * volatile _OnDeck;” 字段指向这把锁的法定继承人,同时最低位还充当了内部锁的角色

接下来通过一组流程图来介绍加解锁的具体流程:

 

上图是加锁的一个整体流程,大致分为3步:

1. 首先走快速上锁流程,主要对应锁本身无人持有的最理想情况

2. 接着是自旋上锁流程,这是预期将在短时间内获取锁的情况

3. 最后是慢速上锁流程,申请者将会加入等待队列(cxq),然后进入睡眠,直到被唤醒后发现自己变成了法定继承者,于是进入自旋,直到完成上锁。

 

 

而且,基于性能考虑,整个上锁流程中的每一步几乎都做了“插队”的尝试:

 

如上图代码中所示,“插队”的意思就是不经过排队(cxq),直接尝试置上锁标志位。

 

 

 上图就是整个解锁流程了,显然真正的解锁操作在第二步中就已经完成了(意味着接下来时刻有“插队”现象发生),剩下的主要就是选出继承者的过程,大致分为以下几步:

  1. 解锁线程首先需要将内部锁(_OnDeck)标记上锁
  2. 从竞争队列(cxq)抽取所有等待者放入等待队列(_EntryList)
  3. _ EntryList取出头一个元素,写入_OnDeck的同时解除内部锁标记,这代表选出了继承者
  4. 唤醒继承者

 

当然伴随着整个解锁流程每一步的,还有对“插队”行为的处理。

至此,JVM锁组件Monitor的原理就介绍到这里,再回归到问题本身,一个疑问就是_OnDeck上记录的继承者为何消失?作为继承者,既然已经消失在竞争队列和等待队列里,显然意味着它大概率已经持有锁、然后解锁走人了,所以问题很可能跟继承者选取过程有关。基于这种猜测,我们对相关代码着重进行了梳理,就发现了下图两处红框标记位置存在疑点,那就是在选继承者过程第3步中:

 

写_ EntryList和写_OnDeck之间没有barrier来保证执行顺序,这可能出现_OnDeck先于_ EntryList写入的情况,一旦继承人提前持有锁,后果就可能非常糟糕…

 

 

这里贴了一张可能的问题场景:

  1. 线程A处于解锁流程中,由于乱序,先写入了继承者同时解除内部锁
  2. 线程B处于上锁流程,发现自己就是法定继承者后,立刻完成上锁
  3. 线程B又迅速进入解锁流程,并从_EntryList中取出头元素(也就是线程B!)作为继承者写入_OnDeck,完成解锁走人
  4. 线程A此时才更新_EntryList,然后唤醒继承者(也就是线程B!),完成解锁走人
  5. _OnDeck上的继承者线程B,实际已经完成加解锁离开,后续等待线程再也无法被唤醒

正巧在社区的高版本上找到了一个相关的修复记录,这里贴出2个关键的代码片段:

 

 

上面这段代码位于慢速上锁流程,被唤醒后检查继承者是否是自己,修复后的代码在读_OnDeck时加了Load-Acquire的barrier。

 

 

上面这段代码位于解锁时选继承者流程,从_ EntryList取出头一个元素,写入_OnDeck的同时解除内部锁标记,修复后的代码在写_OnDeck时加了Store-Release的barrier。

显然,围绕_OnDeck添加的这对One-way barrier可以确保:当继承者线程被唤醒时,该线程可以“看”到_EntryList已经被及时更新。

总结

在AArch64这种弱内存模型的平台上(关于内存序更多的知识在接下来的分享中会详细介绍),一旦涉及多线程对公共内存的每一次访问,必须反复确认是否需要通过barrier来严格保序,而且除非存在有效的依赖关系,否则barrier需要在读写端成对使用。

后记

如果遇到相关技术问题(包括不限于毕昇JDK),可以在论坛求助(目前毕昇JDK最新的官网http://bishengjdk.openeuler.org已经上线,可以进入官网查找所有相关资源,包括二进制下载、代码仓库、使用教学、安装、学习资料等)。毕昇JDK社区每双周周二举行技术例会,同时有一个技术交流群讨论GCC、LLVM、JDK和V8等相关编译技术,感兴趣的同学可以添加如下微信小助手,回复Compiler入群。

与[转帖]【技术剖析】2. JVM锁bug导致G1 GC挂起问题分析和解决相似的内容:

[转帖]【技术剖析】2. JVM锁bug导致G1 GC挂起问题分析和解决

https://bbs.huaweicloud.com/forum/thread-144146-1-1.html 发表于 2021-07-29 20:07:087037查看 作者:宋尧飞 编者按:笔者在AArch64中遇到一个G1 GC挂起,CPU利用率高达300%的案例。经过分析发现问题是由JVM

[转帖]【技术剖析】8. 相同版本 JVM 和 Java 应用,在 x86 和AArch64 平台性能相差30%,何故?

https://bbs.huaweicloud.com/forum/thread-168532-1-1.html 作者: 吴言 > 编者按:目前许多公司同时使用 x86 和 AArch64 2 种主流的服务器。这两种环境的算力相当,内存相同的情况下:相同版本的 JVM 和 Java 应用,相同的 J

[转帖]【技术剖析】16. Native Memory Tracking 详解(2):追踪区域分析(一)

https://bbs.huaweicloud.com/forum/thread-0295101552606827089-1-1.html 上篇文章 Native Memory Tracking 详解(1):基础介绍 中,分享了如何使用NMT,以及NMT内存 & OS内存概念的差异性,本篇将介绍NM

[转帖]【技术剖析】17. Native Memory Tracking 详解(3):追踪区域分析(二)

https://bbs.huaweicloud.com/forum/thread-0227103792775240073-1-1.html 应用性能调优 发表于 2022-11-14 15:19:36143查看 上篇文章 Native Memory Tracking 详解(2):追踪区域分析(一) 

[转帖]Linux块层技术全面剖析-v0.1

Linux块层技术全面剖析-v0.1 perftrace@gmail.com 前言 网络上很多文章对块层的描述散乱在各个站点,而一些经典书籍由于更新不及时难免更不上最新的代码,例如关于块层的多队列。那么,是时候写一个关于linux块层的中文专题片章了,本文基于内核4.17.2。 因为文章中很多内容都

[转帖]【技术剖析】15. Native Memory Tracking 详解(1):基础介绍

https://bbs.huaweicloud.com/forum/thread-0246998875346680043-1-1.html 0.引言 我们经常会好奇,我启动了一个 JVM,他到底会占据多大的内存?他的内存都消耗在哪里?为什么 JVM 使用的内存比我设置的 -Xmx 大这么多?我的内存

[转帖]【技术剖析】18. Native Memory Tracking 详解(4):使用 NMT 协助排查内存问题案例

https://bbs.huaweicloud.com/forum/thread-0211103793043202049-1-1.html 其他 发表于 2022-11-14 15:38:571174查看 从前面几篇文章,我们了解了 NMT 的基础知识以及 NMT 追踪区域分析的相关内容,本篇文章将

[转帖]【技术剖析】10. JVM 中不正确的类加载顺序导致应用运行异常问题分析

https://bbs.huaweicloud.com/forum/thread-169439-1-1.html 神Bug... 发表于 2021-11-15 10:36:113973查看 作者:程经纬、谢照昆 > 编者按:两位笔者分享了不同的案例,一个是因为 JDK 小版本升级后导致运行出错,最终

[转帖]【技术剖析】12. 毕昇 JDK 8 中 AppCDS 实现介绍

https://bbs.huaweicloud.com/forum/thread-169622-1-1.html 作者:伍家华 > 编者按:笔者通过在 Hive 的场景发现 AppCDS 技术存在的价值,然后分析了 AppCDS 的工作原理,并将 JDK 11 中的特性移植到毕昇 JDK 8,在移植

[转帖]【技术剖析】11. 使用jemalloc解决JVM内存泄露问题

https://bbs.huaweicloud.com/forum/thread-169523-1-1.html 作者:王坤 > 编者按:JVM 发生内存泄漏,如何能快速定位到内存泄漏点并不容易。笔者通过使用 jemalloc(可以替换默认的 glibc 库)中的 profiling 机制(通过对程