一、概述
G1(Garbage First)垃圾回收器是Java虚拟机(JVM)中的一种垃圾回收器,设计目标是提供可预测的低延迟垃圾回收,同时保持较高的吞吐量。G1垃圾回收器适用于具有大堆内存和对暂停时间有严格要求的应用程序。
1.G1垃圾回收器的主要特点:
-
分代收集
:
- G1收集器依然保留了年轻代和老年代的概念,但其收集方式不同于传统的分代垃圾回收器。
- 堆被划分为多个大小相同的区域(regions),每个区域可以在年轻代或老年代之间动态分配。
-
并发和并行
:
- G1垃圾回收器能够并发执行标记过程,从而减少了停顿时间。
- 它使用多线程并行执行垃圾收集任务,以提高效率和性能。
-
预测性垃圾回收
:
- G1能够通过设置参数来预测和控制垃圾回收的停顿时间。
- 例如,
-XX:MaxGCPauseMillis
参数可以用来指定期望的最大停顿时间,G1将尽力在该时间内完成垃圾回收。
-
整堆压缩
:
- G1会在必要时进行整堆压缩(mixed garbage collection),以减少内存碎片,提高内存使用效率。
- 在老年代空间不足时,G1会将年轻代和老年代的垃圾回收和压缩结合起来执行。
二、G1垃圾回收阶段
G1的三个主要阶段分别是:年轻代收集、混合收集和并发标记。每个阶段都有其特定的目标和操作方式,共同作用来管理和回收堆内存中的垃圾。
1.年轻代收集(Young Generation Collection)
年轻代收集主要针对新创建的对象。年轻代使用标记-复制算法,将对象从Eden区复制到幸存者区。
-
过程
:
- Eden区分配:新对象分配到Eden区。
- Minor GC触发:当Eden区满时,触发Minor GC。存活对象被复制到Survivor区(如果Survivor区也满了,则直接进入老年代)。
- 幸存者对象复制:从一个Survivor区复制到另一个Survivor区,未复制的对象被认为是垃圾并回收。
- 特点:Minor GC 是“Stop-The-World”(STW)事件,但由于年轻代较小,停顿时间一般较短。
2.混合收集(Mixed Collection)
混合收集是G1垃圾回收器的一个独特特性,在这个阶段,G1同时收集年轻代和老年代的垃圾。混合收集的目的是在老年代充满之前回收一部分老年代的垃圾,从而避免Full GC。
-
触发:当老年代使用率达到一定阈值时(由参数
-XX:InitiatingHeapOccupancyPercent
控制,默认值是45%)。 -
过程
:
- 初始标记(Initial Mark):标记GC Roots直接可达的对象,这是一个STW事件,通常在Minor GC过程中完成。
- 并发标记(Concurrent Mark):从GC Roots开始并发地遍历整个堆,标记所有可达对象。
- 最终标记(Remark):修正并发标记阶段中遗漏的对象引用变化,这是一个STW事件。
- 筛选清理(Cleanup):筛选出需要回收的区域,并进行回收和压缩。
- 特点:混合收集阶段结合了年轻代和老年代的垃圾回收,通过多次小规模的老年代回收避免了长时间的Full GC停顿,每次只会对部分含大量垃圾的老年代进行垃圾回收并不会对所有的老年代进行垃圾回收。
3. 并发标记(Concurrent Marking)
并发标记阶段是G1垃圾回收器中处理老年代垃圾回收的主要过程。其目标是找到所有存活对象,并在后台进行标记,以尽量减少停顿时间。
-
过程
:
- 初始标记(Initial Mark):标记GC Roots直接可达的对象,作为STW事件触发。
- 并发标记(Concurrent Mark):并发进行,遍历并标记整个堆中的对象,从GC Roots开始,找到所有可达对象。这个过程不会停止应用程序的执行。
- 最终标记(Remark):修正并发标记阶段遗漏的对象引用变化,作为STW事件触发。
- 筛选清理(Cleanup):并发回收未被标记的对象并整理堆内存。
-
特点:通过并发标记和筛选清理,G1能够有效地标记和回收老年代的垃圾,同时减少对应用程序的影响。
三、FULL GC
- SerialGC
- 新生代内存不足发生的垃圾收集-minor gc
- 老年代内存不足发生的垃圾收集-full gc
- ParalleGC
- 新生代内存不足发生的垃圾收集-minor gc
- 老年代内存不足发生的垃圾收集-full gc
- CMS
- 新生代内存不足发生的垃圾收集-minor gc
- 老年代内存不足
- G1
- 新生代内存不足发生的垃圾收集-minor gc
- 老年代内存不足
- CMS和G1的并发清理时回收的速度低于垃圾产生的速度,这个时候并发收集就会失败,此时并发收集将会变为串行收集(FULL GC)
四、Young Collection跨代引用
卡表(Card Table)
- 作用:卡表是另一个用于管理跨代引用的机制,它将堆内存划分为固定大小的卡(Card),通常是512字节或更小的单位。卡表记录了老年代中某个卡块是否包含对年轻代的引用。
- 工作原理:当一个老年代对象引用了年轻代对象时,相应的卡会被标记为“脏”卡。当进行年轻代垃圾回收时,垃圾回收器只需要扫描这些标记为“脏”的卡,而不需要扫描整个老年代,从而提高了垃圾回收的效率。
这里进行新生代的垃圾回收时,只有标记为“脏”的卡片需要被扫描,这些卡片是那些有可能包含跨代引用的区域。因此,扫描的范围是有限的,减少了垃圾回收的计算量。如果不使用卡表,垃圾回收器需要扫描整个老年代来检测是否有对年轻代的引用,这在内存较大时会非常低效。
五、Remark
写屏障的主要功能
-
维护跨代引用
:
- 当一个老年代对象引用了一个年轻代对象时,写屏障可以捕捉到这个引用的创建,并将相关信息记录在卡表(Card Table)或记忆集(Remembered Set)中。这确保了在年轻代垃圾回收时,老年代对年轻代的引用不会被遗漏。
-
支持并发垃圾回收
:
- 在并发垃圾回收器(如 G1 或 CMS)中,应用程序线程和垃圾回收线程可能会同时访问和修改对象。写屏障在应用程序线程进行写操作时,可以记录对象引用的变化,从而让垃圾回收线程能够正确地处理这些变化。
-
维护对象标记状态
:
- 写屏障有时用于更新对象的标记状态,帮助垃圾回收器识别哪些对象是存活的,哪些是垃圾。例如,在 G1 垃圾回收器的并发标记阶段,写屏障可以用来捕捉应用线程在并发标记期间对对象的修改。
六、字符串去重
Java 等编程语言中的字符串去重是通过字符串常量池来实现的。字符串常量池是一个专门用于存储字符串字面量和 interned 字符串的内存区域。以下是其工作原理:
-
字符串常量池的概念
:
- 字符串常量池是 JVM 在堆内存中专门为字符串设计的一个区域,用来存储所有字符串字面量和通过
String.intern()
方法显式加入常量池的字符串。 - 当创建一个字符串字面量时,JVM 会首先检查常量池中是否已经存在内容相同的字符串。如果存在,则直接返回这个字符串的引用,而不是重新创建一个新的字符串对象。
- 字符串常量池是 JVM 在堆内存中专门为字符串设计的一个区域,用来存储所有字符串字面量和通过
-
去重机制
:
- 当你创建两个相同内容的字符串时,如果它们都在常量池中,那么它们会共享同一个字符串对象,而不是各自占用独立的内存空间。
- 例如:
String s1 = "hello";
String s2 = "hello";
// s1 和 s2 指向同一个字符串对象,内存中只存在一个 "hello" 对象。
-
String.intern()
方法:
- Java 中的
String.intern()
方法可以手动将一个字符串放入常量池中。调用intern()
方法后,JVM 会检查常量池中是否存在相同内容的字符串。如果存在,则返回常量池中已有的字符串的引用;如果不存在,则将当前字符串添加到常量池中并返回它的引用。 - 例如:
- Java 中的
String s1 = new String("hello");
String s2 = s1.intern();
String s3 = "hello";
// s2 和 s3 指向常量池中的同一个字符串对象
System.out.println(s2 == s3); // 输出: true
垃圾回收中的字符串去重
-
垃圾回收的作用
:
- 字符串常量池中的字符串在 JVM 生命周期内不会被垃圾回收,因为常量池中的字符串通常在整个应用程序的生命周期中都会被使用。
- 对于常量池外的字符串对象,JVM 的垃圾回收器会像对待普通对象一样处理它们:当一个字符串对象不再有任何引用时,它会被标记为垃圾并在合适的时间被回收。
-
内存优化
:
- 通过字符串常量池实现的去重机制,可以显著减少 JVM 中字符串对象的数量,从而降低内存使用量。
- 在应用程序中,如果有大量重复的字符串,使用
String.intern()
方法可以显著节省内存。
七、G1 中的巨型对象回收机制
-
巨型对象的分配
:
- 当 JVM 创建一个巨型对象时,G1 会在堆内存中寻找足够多的连续 Region 来容纳这个对象。
- 由于巨型对象占用多个 Region,因此在 G1 的内存布局中,巨型对象的管理比普通对象更复杂。
-
巨型对象与老年代
:
- 巨型对象一般会直接分配到老年代(Old Generation),因为它们的体积太大,无法有效地在年轻代(Young Generation)中进行垃圾回收。
- 这意味着巨型对象的生命周期通常较长,并且它们的垃圾回收往往需要在老年代回收阶段进行。
-
巨型对象的标记和清除
:
- 在 G1 的垃圾回收过程中,巨型对象会在标记阶段被标记为存活或可回收。
- 如果巨型对象不再被引用且被标记为垃圾,它所占用的连续 Region 会被回收,并释放回内存池以供后续分配。
巨型对象被老年代的脏表引用,当一个巨型对象没有被老年的脏表引用时它可以在新生代的垃圾回收时被垃圾回收。(巨型对象优先处理,越早回收越好)
八、调整并发标记起始时间
调整并发标记的启动时间通常通过设置 -XX:InitiatingHeapOccupancyPercent
参数来实现。你可以根据应用程序的实际情况,调整这个参数的值,以更好地控制并发标记的时机。
如何调整:
-
降低
InitiatingHeapOccupancyPercent
:
- 如果你发现应用程序频繁触发 Full GC,可能是并发标记启动得太晚,导致老年代过度使用。通过降低
InitiatingHeapOccupancyPercent
,你可以使 G1 更早地开始并发标记,从而减轻 Full GC 的压力。 - 例如:
- 如果你发现应用程序频繁触发 Full GC,可能是并发标记启动得太晚,导致老年代过度使用。通过降低
-XX:InitiatingHeapOccupancyPercent=35
-
这样设置后,G1 会在堆使用量达到 35% 时就开始并发标记。
-
提高
InitiatingHeapOccupancyPercent
:
- 如果你的应用程序内存使用比较平稳,并且 Full GC 较少发生,你可以考虑稍微提高这个值,让 G1 更加延迟地启动并发标记,这样可以减少标记阶段的开销。
- 例如:
-XX:InitiatingHeapOccupancyPercent=50
yPercent=35
- 这样设置后,G1 会在堆使用量达到 35% 时就开始并发标记。
- 提高 `InitiatingHeapOccupancyPercent`
:
- 如果你的应用程序内存使用比较平稳,并且 Full GC 较少发生,你可以考虑稍微提高这个值,让 G1 更加延迟地启动并发标记,这样可以减少标记阶段的开销。
- **例如**:
-XX:InitiatingHeapOccupancyPercent=50
- 这样设置后,G1 会在堆使用量达到 50% 时才开始并发标记。