写点什么

不止新生代与老年代:深入 Java 虚拟机堆内存布局与 TLAB、卡表等优化机制

作者:poemyang
  • 2025-11-03
    北京
  • 本文字数:2136 字

    阅读完需:约 7 分钟

Java 虚拟机运行数据区域

在 JDK 8 及以上版本中,Java 虚拟机运行时数据区域主要包括以下部分:1)堆(Heap):这是 Java 虚拟机中最大的内存区域,所有线程共享,主要用于存放对象实例和数组。这也是垃圾回收的主要区域,因此也被称作 GC 堆(Garbage Collection Heap)。2)方法区(Method Area):在 JDK 8 之前,这被称为永久代(PermGen)。但从 JDK 8 开始,被替换为元空间(Metaspace)。主要用于存储已加载的类信息、常量、静态变量、编译后的代码等。3)栈(Stack):每个线程创建时都会有一个对应的 Java 栈,用于存储局部变量、操作数栈、动态链接、方法出口等信息。4)程序计数器(Program Counter Register):这是一块小内存区域,作为当前线程执行的字节码的行号指示器。每个线程都有一个独立的程序计数器。5)本地方法栈(Native Method Stack):类似于 Java 栈,用于支持本地方法的执行。6)直接内存(Direct Memory):虽然不是虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但这部分内存被频繁使用,主要被 NIO 用于提高 IO 效率。



Java 虚拟机的堆划分

当前,主流的 Java 虚拟机主要采用分代回收(Generational Garbage Collection)。分代回收,更准确地说,它是一种理念。这种理念将系统中的所有对象划分为不同的代(Generation),并根据对象的生命周期长度将其分类到相应的代中,每个代则采用适合其特性的垃圾回收算法。这种理念主要基于两个分代假设。1)弱分代假说(Weak Generational Hypothesis):大部分对象都会在创建后不久就变得不可达。也就是说,许多对象的生命周期都很短;2)强分代假说(Strong Generational Hypothesis):存活时间较长的对象,很可能会引用存活时间较短的对象,但反之则不然。也就是说,老的对象很少引用新的对象。Java 虚拟机将堆划分为新生代(Young Generation)和老年代(Old Generation)。其中,新生代又被划分为 Eden 区,以及两个大小相同的 Survivor 区。默认情况下,Java 虚拟机采取一种动态分配的策略,根据生成对象的速率,以及 Survivor 区的使用情况动态调整 Eden 区和 Survivor 区的比例。



TLAB

通常,调用 new 指令时,会在 Eden 区中划出一块作为存储对象的内存。由于堆空间是线程共享的,在多线程环境下,为了避免多个线程同时向堆内存申请空间而产生的竞争,Java 虚拟机为每个线程分配了一个私有的缓冲区,即 TLAB(Thread-Local Allocation Buffer)。由于 TLAB 是线程私有的,因此在分配对象时不需要进行线程同步,大大提高了对象分配的效率。如果 TLAB 空间不足,那么线程可能需要申请新的 TLAB。需要注意的是,TLAB 只适用于小对象的分配。对于大对象,通常直接在堆内存中进行分配。



Minor GC、Minor GC、Full GC

当 Eden 区空间不足时,会触发 Minor GC,对年轻代进行垃圾回收。存活下来对象,会被迁移到 Survivor 区。新生代包含两个 Survivor 区,分别为 from 和 to 区,其中 to 区始终保持空闲。Minor GC 期间,Eden 区和 from 区的存活对象被复制至 to 区,随后交换 from 和 to,确保下次 Minor GC 时,to 区为空。Java 虚拟机会跟踪 Survivor 区对象的复制次数,当达到 15 次或单个 Survivor 区占用率超过 50%时,那么较高复制次数的对象,将被晋升(Promote)至老年代。Minor GC 主要采用复制算法,Survivor 区中的老存活对象晋升至老年代,然后将剩余存活对象与 Eden 区的存活对象复制至另一 Survivor 区。在理想情况下,Eden 区的对象大多已死亡,因此需要复制的数据量较小,因此采用复制算法效率较高。当老年代空间不足时,触发 Major GC,对老年代进行垃圾回收。Major GC 通常采用清除或整理算法,由于需要扫描整个老年代并可能进行对象移动以整理内存,因此会导致较长的停顿时间。当 Java 堆内存空间不足时,触发 Full GC,对整个堆内存(包括新生代和老年代)进行垃圾回收。由于需要扫描整个堆内存并可能进行对象移动以整理内存,Full GC 会导致比 Major GC 更长的停顿时间。


卡表

Minor GC 期间,在标记存活对象的时候,可能会碰到跨代引用对象的问题。由于年轻代大都是存活时间较短的对象,跨代引用通常是指老年代对象存在对新生代对象的引用。为了保证对年轻代存活对象标记准确性,就不得不把老年代也纳入到扫描范围。为了解决跨代引用的问题,可以在新生代可以引入记忆集(RememberSet)。记忆集位于新生代中,是一种用于记录从非回收区域指向回收区域的指针集合的抽象数据结构。



记忆集是一种概念,在 Java 虚拟机中,记忆集通常通过卡表(Card Table)实现,它也是目前最常用的一种方式。卡表是一个字节数组,其中每个元素对应一块特定大小(默认 512 字节)的内存区域,称为卡页(Card Page)。当对象的引用发生修改时,写屏障会被触发,将对应的卡页标记为"脏",表示该内存区域的对象已被修改。为了降低写屏障的开销并保持其生成指令的简洁性,写屏障并不判断更新后的引用是否指向新生代对象,而是统一视为可能指向新生代对象的引用。


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


在进行 Minor GC 的时,就无需扫描整个老年代,而是在卡表中寻找标记为脏卡的区域,并将这些脏卡区域的对象加入到 Minor GC 的 GC Roots 中。完成所有脏卡的扫描后,Java 虚拟机会清除所有脏卡的标记。



未完待续


很高兴与你相遇!如果你喜欢本文内容,记得关注哦

发布于: 刚刚阅读数: 5
用户头像

poemyang

关注

让世界知道我的存在 2018-03-05 加入

技术/人文, 互联网

评论

发布
暂无评论
不止新生代与老年代:深入Java虚拟机堆内存布局与TLAB、卡表等优化机制_垃圾回收_poemyang_InfoQ写作社区