JVM之卡表(Card Table)

本文深入探讨了JVM中的卡标记(CardMarking)技术,解释了其在垃圾回收过程中如何通过卡表和写屏障优化对GCRoots的扫描,以减少YGC时的老年代扫描开销,特别是在高并发环境下的性能提升。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

我们知道,JVM在进行垃圾收集时,需要先标记所有可达对象,然后再清除不可达对象,释放内存空间。那么,如何快速的找到所有可达对象呢?

最简单粗暴的实现,就是每次进行垃圾收集时,都对整个堆中的所有对象进行扫描,找到所有存活对象。逻辑是简单,但性能比较差。

简单粗暴的实现方式,通常都是不可取的。那JVM是如何实现快速标记可达对象的?

答案是GC Roots。

GC Roots是垃圾收集器寻找可达对象的起点,通过这些起始引用,可以快速的遍历出存活对象。GC Roots最常见的是静态引用和堆栈的局部引用变量。然而,这不是我们这讲的重点:)

现代JVM,堆空间通常被划分为新生代和老年代。由于新生代的垃圾收集通常很频繁,如果老年代对象引用了新生代的对象,那么,需要跟踪从老年代到新生代的所有引用,从而避免每次YGC时扫描整个老年代,减少开销。

对于HotSpot JVM,使用了卡标记(Card Marking)技术来解决老年代到新生代的引用问题。具体是,使用卡表(Card Table)和写屏障(Write Barrier)来进行标记并加快对GC Roots的扫描。

卡表(Card Table)

基于卡表(Card Table)的设计,通常将堆空间划分为一系列2次幂大小的卡页(Card Page)。

卡表(Card Table),用于标记卡页的状态,每个卡表项对应一个卡页。

HotSpot JVM的卡页(Card Page)大小为512字节,卡表(Card Table)被实现为一个简单的字节数组,即卡表的每个标记项为1个字节。

当对一个对象引用进行写操作时(对象引用改变),写屏障逻辑将会标记对象所在的卡页为dirty。

OpenJDK/Oracle 1.6/1.7/1.8 JVM默认的卡标记简化逻辑如下:

CARD_TABLE [this address >> 9] = 0;
复制代码

首先,计算对象引用所在卡页的卡表索引号。将地址右移9位,相当于用地址除以512(2的9次方)。可以这么理解,假设卡表卡页的起始地址为0,那么卡表项0、1、2对应的卡页起始地址分别为0、512、1024(卡表项索引号乘以卡页512字节)。

其次,通过卡表索引号,设置对应卡标识为dirty。

带来的2个问题

1.无条件写屏障带来的性能开销

每次对引用的更新,无论是否更新了老年代对新生代对象的引用,都会进行一次写屏障操作。显然,这会增加一些额外的开销。但是,与YGC时扫描整个老年代相比较,这个开销就低得多了。

不过,在高并发环境下,写屏障又带来了虚共享(false sharing)问题。

2.高并发下虚共享带来的性能开销

在高并发情况下,频繁的写屏障很容易发生虚共享(false sharing),从而带来性能开销。

假设CPU缓存行大小为64字节,由于一个卡表项占1个字节,这意味着,64个卡表项将共享同一个缓存行。

HotSpot每个卡页为512字节,那么一个缓存行将对应64个卡页一共64*512=32KB。

如果不同线程对对象引用的更新操作,恰好位于同一个32KB区域内,这将导致同时更新卡表的同一个缓存行,从而造成缓存行的写回、无效化或者同步操作,间接影响程序性能。

一个简单的解决方案,就是不采用无条件的写屏障,而是先检查卡表标记,只有当该卡表项未被标记过才将其标记为dirty。

这就是JDK 7中引入的解决方法,引入了一个新的JVM参数-XX:+UseCondCardMark,在执行写屏障之前,先简单的做一下判断。如果卡页已被标识过,则不再进行标识。

简单理解如下:

if (CARD_TABLE [this address >> 9] != 0)
  CARD_TABLE [this address >> 9] = 0;
复制代码

与原来的实现相比,只是简单的增加了一个判断操作。

虽然开启-XX:+UseCondCardMark之后多了一些判断开销,但是却可以避免在高并发情况下可能发生的并发写卡表问题。通过减少并发写操作,进而避免出现虚共享问题(false sharing)。

也用于CMS GC

CMS在并发标记阶段,应用线程和GC线程是并发执行的,因此可能产生新的对象或对象关系发生变化,例如:

  • 新生代的对象晋升到老年代;
  • 直接在老年代分配对象;
  • 老年代对象的引用关系发生变更;
  • 等等。

对于这些对象,需要重新标记以防止被遗漏。为了提高重新标记的效率,并发标记阶段会把这些发生变化的对象所在的Card标识为Dirty,这样后续阶段就只需要扫描这些Dirty Card的对象,从而避免扫描整个老年代。

参见:Java之CMS GC的7个阶段


参考

psy-lob-saw.blogspot.com/2014/10/the…

blogs.oracle.com/dave/false-…

bibliography.selflanguage.org/_static/wri…

www.memorymanagement.org/glossary/b.…

www.memorymanagement.org/glossary/w.…

docs.oracle.com/cd/E19205-0…

ifeve.com/falsesharin…

《深入拆解 Java 虚拟机》郑雨迪

个人公众号

更多文章,请关注公众号:二进制之路

<think>好的,我现在需要回答用户关于JVM新生代和老年代垃圾回收过程图的问题。首先,我得回忆一下JVM的内存结构以及垃圾回收的相关知识。 用户想要了解的是垃圾回收的过程图,可能需要详细解释新生代和老年代的回收机制,包括步骤和相关算法。首先,我应该从JVM的内存划分开始,明确新生代和老年代的位置。新生代通常分为Eden区、Survivor区(From和To),而老年代则是存放长期存活的对象。 接下来,需要考虑垃圾回收的具体过程。新生代的GC是Minor GC,使用复制算法;老年代的GC是Major GC或Full GC,通常使用标记-清除或标记-整理算法。需要详细描述对象如何从Eden区分配到Survivor区,再晋升到老年代的过程。 可能会遇到的问题是如何用文字描述这个流程图,因为用户可能期望一个图,但这里只能用文字解释。我需要分步骤说明,每个步骤中的对象状态变化,以及涉及的垃圾回收算法。此外,要确保术语正确,比如区分Minor GC和Full GC,以及不同的收集器(如Serial、Parallel、CMS、G1等)是否有不同的处理方式,但用户可能只需要基础流程,所以保持通用性较好。 还要注意用户提到的LaTeX格式要求,所有数学公式需要用$...$,但这个问题可能涉及较少的数学公式,更多是流程和结构描述。不过如果有涉及算法的时间复杂度或其他公式,需要正确应用格式。 需要确认流程的正确性,比如对象优先在Eden区分配,当Eden满时触发Minor GC,存活对象复制到Survivor From区,年龄增加到1,下次GC时From和To交换,年龄递增,达到阈值(默认15)则晋升到老年代。老年代空间不足时会触发Major GC,可能伴随Full GC,导致STW时间较长。 在组织回答时,按照用户的要求结构清晰,分步骤解释,可能分为新生代回收过程、老年代回收过程、跨代引用处理等部分。同时,需要指出常见算法和可能的影响,如STW暂停时间。 需要检查是否有遗漏的关键点,比如大对象直接进入老年代,空间分配担保机制等,但根据用户的问题,可能不需要太深入,保持基础即可。 最后,确保用中文回答,使用正确的术语,避免拼写和语法错误,并符合用户给出的格式要求,比如行内公式的正确使用(如果有的话),但这个问题可能更多是文字描述,所以需要合理分段,使用列或步骤式说明。</think>以下是JVM新生代与老年代垃圾回收过程的逐步解析,采用文字描述“流程图”的关键节点和逻辑: --- ### **1. JVM堆内存结构** JVM堆内存分为**新生代**和**老年代**: - **新生代** - **Eden区**:新对象优先分配在此 - **Survivor区**:分`From`和`To`两个区,用于存放Minor GC后存活的对象 - **对象年龄计数器**:记录对象经历GC的次数(默认阈值15) - **老年代**:存放长期存活的对象或大对象 --- ### **2. 新生代垃圾回收(Minor GC)** **触发条件**:Eden区内存不足时触发 **核心算法**:**复制算法**(Copying) **过程分步**: 1. **对象分配** - 新对象首先尝试在**Eden区**分配。 - 若Eden区空间不足,触发Minor GC。 2. **标记存活对象** - 暂停用户线程(**STW**,Stop-The-World)。 - 标记Eden区和当前`From`区中的存活对象。 3. **复制存活对象** - 将存活对象复制到`To`区,并**年龄+1**。 - **分代策略**: - 若对象年龄超过阈值(默认15),晋升到**老年代**。 - 若`To`区空间不足,直接晋升到老年代。 4. **清理内存** - 清空Eden区和`From`区。 - `From`和`To`区角色交换,等待下一次GC。 --- ### **3. 老年代垃圾回收(Major GC / Full GC)** **触发条件**: - 老年代空间不足 - 调用`System.gc()`(不保证立即执行) - 空间分配担保失败(Minor GC后存活对象超过老年代剩余空间) **核心算法**: - **标记-清除**(Mark-Sweep):标记无用对象后直接清除(可能产生内存碎片)。 - **标记-整理**(Mark-Compact):标记后整理存活对象到连续空间(减少碎片)。 **过程分步**: 1. **初始标记**(STW) - 标记与GC Roots直接关联的对象(速度极快)。 2. **并发标记** - 遍历对象图,标记所有存活对象(与用户线程并发执行)。 3. **重新标记**(STW) - 修正并发标记期间对象状态的变化(如新增的垃圾)。 4. **清理/整理** - 清除无用对象(标记-清除)或整理内存(标记-整理)。 --- ### **4. 跨代引用处理** - **问题**:老年代对象可能引用新生代对象,需避免错误回收。 - **解决方案**: - **Card Table)**:记录老年代中引用了新生代的内存块(Card),Minor GC时只需扫描关联区域。 --- ### **关键流程图总结** ``` 新对象分配 → Eden区 │ └─Eden满 → Minor GC │ ├─标记存活对象(Eden + From区) │ ├─复制到To区(年龄+1) → 年龄超阈值 → 晋升老年代 │ └─清空Eden + From区 老年代空间不足 → Major GC/Full GC │ ├─初始标记(STW) │ ├─并发标记 │ ├─重新标记(STW) │ └─清理/整理 ``` --- ### **注意事项** 1. **STW影响**:Full GC会导致长时间停顿,需优化参数(如堆大小、晋升阈值)。 2. **垃圾收集器差异**:不同收集器(如Parallel Scavenge、CMS、G1)具体流程可能不同。 如需进一步细节(如具体收集器实现),可提供补充说明!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值