面向全堆的收集器。
可预测的停顿时间模型。
每次收集都是
Region
大小的整数倍数。G1 将整个 Java 堆划分为多个大小相等的独立区域(Region),每个 Region 的大小可以通过参数 -XX:G1HeapRegionSize 设定,取值范围从 1MB 到 32MB,且应为 2 的幂次方。
为了实现对象在新生代和老年代之间的合理转移,JVM 为每个对象都维护了一个年龄计数器。对象每经历一次 Minor GC 且仍然存活,并且能够被复制到 Survivor 区,它的年龄就会加 1。
在大多数应用场景下,经过 15 次 Minor GC 仍然存活的对象,很可能在未来一段时间内也不会被回收,将其转移到老年代,可以减少新生代的内存碎片,提高新生代的内存回收效率,同时也能更好地利用老年代的内存空间,因为老年代的内存空间相对较大,回收频率相对较低。
JVM 的内存通常分为新生代和老年代。新生代又分为 Eden 区和 Survivor 区(一般有两个 Survivor 区,S0 和 S1)。新对象一般优先在 Eden 区分配内存。当 Eden 区内存满了,就会触发 Minor GC,把仍然存活的对象复制到 Survivor 区。
在默认情况下,Eden 区在新生代中占据较大的内存空间,通常新生代内存空间按照 8:1:1 的比例分配给 Eden 区、Survivor0 区和 Survivor1 区,即 Eden 区占新生代内存的 80%。
Survivor 区的两个区域 S0 和 S1 大小相等,它们各自占新生代内存的 10%。这种内存分配比例是可以通过 JVM 参数进行调整的,以适应不同应用程序的内存需求和垃圾回收策略。
对象进入 Survivor 区后,每经过一次 Minor GC 且仍然存活,对象的年龄就会加 1。当对象的年龄达到一定阈值(默认是 15)时,对象会被晋升到老年代。如果 Survivor 区内存不足,无法容纳所有存活对象,部分年龄较小的对象可能会被提前晋升到老年代,以确保有足够的空间存放其他存活对象。
通常采用复制算法进行内存分配和回收。当 Eden 区进行垃圾回收时,会将存活的对象复制到 Survivor 区或老年代,然后直接清理 Eden 区中所有未被复制的对象所占用的内存空间,这种方式可以快速地回收内存,减少内存碎片的产生。
同样采用复制算法,在垃圾回收时,会将其中一个 Survivor 区(如 S0)中存活的对象复制到另一个 Survivor 区(S1),如果对象年龄达到晋升阈值或 S1 空间不足,则将对象晋升到老年代。然后清空原来的 Survivor 区(S0),完成垃圾回收。
在常见的 JVM 默认配置中,新生代和老年代的大小比例通常是 1:2。也就是说,老年代在默认情况下占据整个堆内存的 2/3 空间,而新生代占据 1/3 空间。
G1 从整体来看是基于 “标记 - 整理” 算法实现的收集器,从局部(两个 Region 之间)上看是基于 “复制” 算法实现的,这意味着运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。
初始标记阶段是需要暂停所有用户线程(Stop - The - World)的,因为在标记过程中如果用户线程继续运行,可能会导致对象的引用关系发生变化,从而影响标记的准确性。不过,这个阶段的停顿时间相对较短,因为它只是标记 GC Roots 直接关联的对象,不需要遍历整个堆。
G1
垃圾收集器面向全堆的收集器
哪块内存中存放的垃圾数量最多,回收效益最大。
G1 能充分利用多 CPU、多核环境下的硬件优势,使用多个 CPU 来缩短 Stop - The - World 停顿时间,部分垃圾收集工作可以和用户线程并发执行。
G1 收集器的运作过程大致可划分为以下几个步骤:
初始标记(Initial Marking):标记一下 GC Roots 能直接关联到的对象,并且修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的 Region 中创建新对象。这个阶段需要停顿线程,但耗时很短,而且是借用 Minor GC 的时候同步完成的。
这个过程其实是在确定垃圾回收的起始范围,后续会基于这些被标记的对象,进一步去查找其他存活的对象。
在初始标记阶段,还会修改 TAMS(Next Top at Mark Start)的值。TAMS 是一个与 Region 相关的概念,G1 收集器将堆内存划分为多个大小相等的 Region,每个 Region 都有自己的 TAMS 指针。修改 TAMS 值的目的是为了让下一阶段用户程序并发运行时,能够在正确可用的 Region 中创建新对象。在标记过程中,新创建的对象会被自动认为是存活的,它们会被分配到 TAMS 指针之上的空间,这样可以保证在并发标记过程中,新对象不会干扰到标记的准确性。
【初始标记】阶段是需要【暂停所有用户线程】(Stop - The - World)的,因为在标记过程中如果用户线程继续运行,可能会导致对象的引用关系发生变化,从而影响标记的准确性。不过,这个阶段的停顿时间相对较短,因为它只是标记 GC Roots 直接关联的对象,不需要遍历整个堆。
为了减少额外的停顿时间,G1 收集器的初始标记阶段通常会借用 Minor GC 的时机同步完成。也就是说,当进行 Minor GC 时,会顺带完成初始标记的工作,这样就不需要额外专门为初始标记阶段进行一次停顿,从而在一定程度上优化了垃圾回收的性能。
并发标记(Concurrent Marking):从 GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
最终标记(Final Marking):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这个阶段需要【停顿线程】,但是可并行执行。
筛选回收(Live Data Counting and Evacuation):首先对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划,然后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的全部空间。这里的操作涉及存活对象的移动,【必须暂停用户线程】,由多条收集器线程并行完成。