当前位置:首页 > 公众号精选 > Linux阅码场
[导读]编者按:笔者遇到一个非常典型JVM架构相关问题,在x86正常运行的应用,在aarch64环境上低概率偶现JVM崩溃。这是一个典型的JVM内部bug引发的问题。通过分析最终定位到CMS代码存在bug,导致JVM在弱内存模型的平台上Crash。在分析过程中,涉及到CMS垃圾回收原理、...

编者按:笔者遇到一个非常典型 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)。创建新对象时根据对象大小从空闲链表中选取合适的内存块存放新对象,但这种方式有两个问题,一个是空间局部性不太好,还有一个是容易产生内存碎片化的问题。

  • 另一种对剩余空间的处理方式是 Copy GC,通过移动存活对象的方式,重新得到一个连续的空闲空间,创建新对象时总在这个连续的内存空间分配,直接使用碰撞指针方式分配(Bump-Pointer)。这里又分两种情况:

  • 将存活对象复制到另一块内存(to-space,也叫 survival space),原内存块全部回收,这种方式叫撤离(Evacuation)

  • 将存活对象推向内存块的一侧,另一侧全部回收,这种方式也被称为标记-整理(Mark-Compact)

现代的垃圾回收算法基本都是分代回收的,因为大部分对象都是朝生夕死的,因此将新创建的对象放到一块内存区域,称为年轻代;将存活时间长的对象(由年轻代晋升)放入另一块内存区域,称为老年代。根据不同代,采用不同回收算法。

  • 年轻代,一般采用 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 中对象布局如下图所示:

对象由对象头加字段组成,我们这里主要关注对象头。对象头包括markOop和_matadata。前者存放对象的标志信息,后者存放 Klass 指针。所谓 Klass,可以简单理解为这个对象属于哪个 Java 类,例如:String str = new String(); 对象 str 的 Klass 指针对应的 Java 类就是 Ljava/lang/String。

  • markOop 的信息很关键,它的定义如下[1]:
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 
本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭
关闭