系列文章目录
第一章 Java核心篇之JVM探秘:内存模型与管理初探
第二章 Java核心篇之JVM探秘:对象创建与内存分配机制
第三章 Java核心篇之JVM探秘:垃圾回收算法与垃圾收集器
第四章 Java核心篇之JVM调优实战:Arthas工具使用及GC日志分析
目录
前言
一、垃圾收集算法
引用计数法 (Reference Counting)
标记清除算法 (Mark-Sweep)
复制算法 (Copying)
标记整理算法 (Mark-Compact 或 Mark-Sweep-Compact)
分代算法 (Generational)
增量回收算法 (Incremental Garbage Collection)
二、垃圾收集器
(1)Serial Collector(串行收集器)
(2)Parallel Collector(并行收集器)
(3)ParNew Collector(并行新生代收集器)
(3)CMS 收集器
(4)G1 收集器
(5)ZGC (Z Garbage Collector)
三、垃圾收集底层算法
(1)三色标记
示例
(2)颜色指针
总结
前言
前边我们已经了解过了JVM的内存模型与内存分配机制,在Java的世界里,内存管理是一项至关重要的任务。与C或C++等语言不同,Java自动处理对象的创建和销毁,这一过程主要由Java虚拟机(JVM)中的垃圾回收器(GC)来完成。本文将深入探讨垃圾回收算法与垃圾收集器的细节,旨在为读者提供一个全面且深入的理解。
一、垃圾收集算法
垃圾回收(Garbage Collection, GC)是现代编程语言运行环境中的一个重要特性,它自动管理内存的分配和释放,避免了程序员手动管理内存可能引入的错误。以下是一些常见的垃圾回收算法及其特点:
引用计数法 (Reference Counting)
- 思想:为每个对象关联一个引用计数器,每当有一个地方引用它,计数器就增加1;当引用失效时,计数器减少1。当计数器为0时,表示没有引用指向该对象,可以被回收。
- 优点:实现简单,不需要停止整个应用程序,可以立即回收无引用的对象。
- 缺点:无法处理循环引用的情况,因为即使对象之间形成循环引用链,它们的引用计数也不会降到0。
标记清除算法 (Mark-Sweep)
- 思想:分为“标记”和“清除”两个阶段。首先从根节点开始标记所有可达对象,然后清除未被标记的对象。
- 优点:可以有效回收不再使用的对象。
- 缺点:执行过程中需要暂停应用程序(Stop-the-world),并且会留下内存碎片,可能导致后续分配大对象时失败。
复制算法 (Copying)
- 思想:将内存区域划分为两块相等的区域,每次只使用其中一个区域,垃圾回收时将存活对象复制到另一个区域,然后清空当前区域。
- 优点:适用于新生代,其中大部分对象是短暂的,因此复制成本低,同时可以消除内存碎片。
- 缺点:需要双倍的内存空间,并且如果存活对象过多,可能需要频繁复制,影响性能。
标记整理算法 (Mark-Compact 或 Mark-Sweep-Compact)
- 思想:结合了标记清除算法和内存整理过程,除了标记和清除之外,还会将存活的对象移动到内存的一端,从而消除内存碎片。
- 优点:解决了标记清除算法的内存碎片问题。
- 缺点:移动对象需要更新所有指向这些对象的指针,可能会导致额外的开销。
分代算法 (Generational)
- 思想:基于观察到的现象,大多数对象很快就会变得不可达(短暂生存期)。将堆分为几代(如新生代和老年代),使用不同的算法处理不同代的对象。
- 优点:可以优化垃圾回收过程,减少全局停顿时间,提高回收效率。
- 缺点:算法实现复杂,需要维护不同代之间的转换逻辑。
增量回收算法 (Incremental Garbage Collection)
- 思想:将垃圾回收工作分成多个小的步骤,逐步完成,而不是一次性完成,这样可以减少停顿时间。
- 优点:可以显著降低应用程序的暂停时间。
- 缺点:实现复杂,需要精细控制垃圾回收的进度。
二、垃圾收集器
虽然我们对各个收集器进行比较,但并非为了挑选出一个最好的收集器。更加没有万能的收集器,开发者应根据具体应用场景选择合适的GC算法,并通过调优提高系统效率。随着JVM的不断演进,未来的垃圾收集器将更加智能和高效。
(1)Serial Collector(串行收集器)
描述:这是最简单的垃圾收集器,使用单线程执行垃圾回收操作。在执行GC时,所有其他线程都会暂停。
算法:年轻代使用复制算法,老年代使用标记-整理算法。
优势:消耗资源少,适用于小内存环境。
劣势:GC停顿时间长,不适合多核处理器。
适用场景:小内存、单核处理器的环境,或测试环境。
参数:-XX:+UseSerialGC -XX:+UseSerialOldGC
(明确指定使用串行收集器)。
(2)Parallel Collector(并行收集器)
描述:
- Parallel Collector在年轻代使用多个线程进行垃圾收集,以减少GC停顿时间。
算法:
- 年轻代使用复制算法,老年代使用标记-压缩算法。
优势:
- 多线程可以加快GC速度,减少停顿时间。
劣势:
- 在小堆内存中,多线程的优势不明显。
适用场景:
- 多核处理器、对GC停顿时间敏感的应用。
参数:
-XX:+UseParallelGC
(年轻代)、-XX:+UseParallelOldGC
(老年代)。
(3)ParNew Collector(并行新生代收集器)
描述:ParNew是Parallel Collector的年轻代版本,它使用多线程进行垃圾收集。
算法:使用复制算法。
优势:多线程可以减少GC停顿时间,适合多核处理器。
劣势:与Serial Collector相比,消耗更多系统资源。
适用场景:多核处理器、需要与CMS Collector配合使用的情况。
参数:-XX:+UseParNewGC
(启用ParNew收集器)。
(3)CMS 收集器
描述:CMS Collector旨在最小化停顿时间,它在标记阶段与应用程序线程并发运行。
算法:使用标记-清除算法,但在清除阶段可能会暂停应用程序。
优势:低停顿时间,适合交互式应用。
劣势:可能导致内存碎片,不适合大堆内存。
适用场景:需要低停顿时间、对用户响应敏感的应用。
参数:
- -XX:+UseConcMarkSweepGC:启用cms
-
-XX:ConcGCThreads:并发的GC线程数
-
-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)
-
-XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一次
-
-XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
-
-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,
JVM仅在第一次使用设定值,后续则会自动调整 -
-XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,降低CMS GC标记阶段(也会对年轻代一起做标记,如果在
minor gc就干掉了很多对垃圾对象,标记阶段就会减少一些标记时间)时的开销,一般CMS的GC耗时 80%都在标记阶段 -
-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
-
-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;
(4)G1 收集器
描述:
G1是一种基于分区的垃圾收集器,它可以预测停顿时间,并尝试将停顿时间保持在一个可接受的范围内,即使在大堆内存中也是如此。
优点:均衡停顿时间和内存碎片,适合大堆内存。
缺点:配置复杂度较高,需要更多试验才能找到最佳配置。
使用场景:适合需要低停顿时间和大堆内存的企业级应用。
参数:
- -XX:+UseG1GC:使用G1收集器
- -XX:ParallelGCThreads:指定GC工作的线程数量
- -XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的N次幂),默认将整堆划分为2048个分区
- -XX:MaxGCPauseMillis:目标暂停时间(默认200ms)
- -XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
- -XX:G1MaxNewSizePercent:新生代内存最大空间
- -XX:TargetSurvivorRatio:Survivor区的填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
- -XX:MaxTenuringThreshold:最大年龄阈值(默认15)
- -XX:InitiatingHeapOccupancyPercent:老年代占用空间达到整堆内存阈值(默认45%),则执行新生代和老年代的混合收集(MixedGC),比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了
- -XX:G1MixedGCLiveThresholdPercent(默认85%) region中的存活对象低于这个值时才会回收该region,如果超过这个值,存活对象过多,回收的的意义不大。
- -XX:G1MixedGCCountTarget:在一次回收过程中指定做几次筛选回收(默认8次),在最后一个筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。
- -XX:G1HeapWastePercent(默认5%): gc过程中空出来的region是否充足阈值,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其他Region,然后这个Region中的垃圾对象全部清理掉,这样的话在回收过程就会不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会立即停止混合回收,意味着本次混合回收就结束了。
(5)ZGC (Z Garbage Collector)
描述:
ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器,ZGC可以说源自于是Azul System公司开发的C4(Concurrent Continuously Compacting Collector) 收集器。
优点:极低的停顿时间,适合需要极低延迟的大数据应用。
缺点:新特性,可能在某些环境中稳定性未知。
使用场景:适合超大堆内存和对延迟敏感的应用。
参数:-XX:+UnlockExperimentalVMOptions -XX:+UseZGC
(开启ZGC收集器)。
三、垃圾收集底层算法
(1)三色标记
三色标记算法是垃圾回收器(Garbage Collector, GC)中用于并发标记阶段的一种技术,主要用于解决在并发环境中准确标记存活对象的问题。这种算法由三种不同的“颜色”表示对象的不同状态,分别是白色、灰色和黑色。
在并发标记算法中,三色标记的作用在于帮助垃圾回收器追踪和标记所有可达的对象,同时处理在标记过程中由于其他线程修改对象引用而可能产生的漏标或错标问题。
下面是三色标记的基本概念:
-
白色(White):表示尚未被访问或标记的对象。在垃圾收集开始时,所有的对象都被认为是白色的。
-
灰色(Gray):表示已经开始访问但其引用还未完全检查的对象。当一个对象首次被发现并且其引用列表尚未被检查时,它会被标记为灰色。
-
黑色(Black):表示已经完全访问和标记的对象。一旦一个对象的所有引用都被检查完毕,并且确定它是可达的,那么它就会被标记为黑色。
在并发标记过程中,如果一个新的引用指向了一个白色的对象,那么这个对象会被标记为灰色,以确保它能被正确地加入到待检查的队列中。此外,当一个灰色对象的引用被访问时,如果这个引用指向的是白色对象,那么这个对象也会被标记为灰色,从而保证了所有可达对象都能被正确标记。
为了防止在并发标记阶段出现的数据竞争和不一致性,垃圾收集器会使用诸如读写屏障等技术来确保引用的正确更新和对象状态的同步。
三色标记算法是CMS和G1等现代垃圾收集器中使用的关键技术之一,它有助于实现高效、低中断的垃圾收集过程。
示例
假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历访问过程为:
- 初始时,所有对象都在【白色集合】中;
- 将GC Roots直接引用到的对象挪到【灰色集合】中;
- 从灰色集合中获取对象:
- 将本对象引用到的其他对象全部挪到【灰色集合】中;
- 将本对象挪到【黑色集合】里面。
- 重复步骤3,直至【灰色集合】为空时结束。
- 结束后,仍在【白色集合】的对象即为GC Roots不可达,可以进行回收。
需要注意,传统标记方式发生Stop The World时,对象间的引用是不会发生变化的,可以轻松完成标记。
而并发标记在标记期间应用线程还在继续跑,对象间的引用可能发生变化,就会出现错标和漏标的情况就有可能发生。
(2)颜色指针
颜色指针(Colored Pointers)是一种用于辅助并发标记阶段的技术,它的设计目的是为了在不中断应用程序执行的情况下,高效地进行垃圾收集,同时避免数据竞争和标记错误。
在传统的并发标记算法中,如果多个线程同时访问同一对象,或者在标记过程中对象的引用发生变化,就可能产生标记不一致的情况,即所谓的“漏标”和“错标”。为了避免这些问题,ZGC引入了颜色指针的概念。
颜色指针包含两部分信息:
- 对象地址:存储实际对象的内存位置。
- 颜色标记:一个额外的位(或几位),用来标识该指针指向的对象是否已经被标记。
在ZGC中,颜色标记有两种状态:
- 黑色(Black):表示该对象已经被标记为存活。
- 白色(White):表示该对象还没有被标记,可能是未访问或未存活的对象。
ZGC如何利用颜色指针:
- 初始化:在垃圾收集开始时,所有指针的颜色标记都会被设为白色。
- 标记过程:当一个对象被访问时,它的颜色标记会变成黑色。同时,任何指向该对象的指针的颜色也会被更新为黑色,表明这个对象是存活的。
- 读写屏障:在并发标记过程中,每当一个线程读取或修改对象引用时,ZGC会使用读写屏障来更新指针的颜色。如果一个线程读取了一个白色指针,它会将该指针标记为黑色,并将这个对象加入到待检查的队列中。如果一个线程尝试修改一个黑色指针,它会先检查目标对象的颜色,如果目标对象仍然是白色,则将其标记为黑色,并同样将该对象加入到待检查的队列中。
颜色指针技术允许ZGC在并发标记阶段安全地处理对象引用的变化,确保所有存活对象能够被正确标记,同时最小化了对应用程序执行的干扰。这种机制使得ZGC能够在高并发环境下提供非常低的暂停时间和高效的内存管理。
需要注意的是,颜色指针的实现细节可能会因具体实现和处理器架构而异,例如在64位系统中,颜色位可能直接编码在指针值中,而在某些情况下,可能需要使用额外的元数据来存储颜色信息。
如上图所示,64位对象引用划分如下:
18位:未使用的位
1位:可最终确定
1位:重新映射
1位:标记1
1位:标记为0
42位:对象地址
前18位保留供将来使用。42位可以寻址高达4 TB的数据。。。
总结
理解垃圾回收算法与垃圾收集器不仅有助于开发者编写更高效的代码,还能在遇到性能瓶颈时,提供有效的诊断和优化策略。随着JVM技术的不断进步,未来我们或许能看到更加智能和高效的垃圾回收机制。