一、概述
管理 Java 程序中的对象,就是 JVM 垃圾回收器的任务,即在程序运行期间自动释放不再使用的内存。垃圾回收器可以分为不同的类型,每种类型都有其自己的工作原理和优缺点。下面总结了几种常见的 JVM 垃圾回收器的特点。
二、垃圾收集器分类
JVM 垃圾回收器可以分为不同的类型,每种类型都有其自己的工作原理和优缺点。下面是几种常见的 JVM 垃圾回收器:
1. 串行垃圾回收器
串行垃圾回收器是 JVM 最简单、最基础的垃圾回收器。它只使用一个线程进行垃圾回收,并且在进行垃圾回收时会暂停所有的应用程序线程。这种垃圾回收器适用于小型应用程序,因为它能够快速回收内存并且具有较低的 CPU 开销。然而,由于它需要停止应用程序线程,因此不适用于大型应用程序。
2. 并行垃圾回收器
并行垃圾回收器使用多个线程进行垃圾回收,可以在一定程度上提高垃圾回收的速度。与串行垃圾回收器相比,它对应用程序的影响更小,因为垃圾回收和应用程序可以并行运行。然而,由于并行垃圾回收器需要使用多个线程,因此它可能会占用更多的 CPU 资源,这可能会影响应用程序的性能。
3. 并发标记清除垃圾回收器
并发标记清除垃圾回收器是一种并发垃圾回收器,它可以在应用程序运行时进行垃圾回收。它的工作原理是先标记所有的存活对象,然后清除未被标记的对象。由于垃圾回收和应用程序可以并行运行,因此并发标记清除垃圾回收器对应用程序的影响非常小。但是,由于它需要在标记对象时使用多个线程,因此它可能会占用更多的 CPU 资源,这可能会影响应用程序的性能。
三、典型的垃圾收集器
1. Serial 垃圾回收器
Serial 垃圾回收器是 JVM 中最基本的垃圾回收器,它是一种单线程的垃圾回收器,它通过暂停所有 Java 线程来进行垃圾回收。Serial 垃圾回收器主要用于小型应用程序,因为它的性能比较低,但它也可以用于小型服务器上的中等负载应用程序。
Serial 垃圾回收器的工作原理是,首先它会暂停所有的 Java 线程,然后对整个堆进行垃圾回收。它通过标记-复制算法来清理垃圾对象,由于 Serial 垃圾回收器是单线程的,因此它的缺点是清理时间较长,会导致较长的停顿时间。
2. ParNew 垃圾回收器
ParNew 垃圾回收器是 Serial 垃圾回收器的多线程版本,它可以通过并行处理来减少停顿时间。ParNew 垃圾回收器主要用于多核 CPU 的服务器上,因为它可以利用多个 CPU 核心来加速垃圾回收过程。
ParNew 垃圾回收器的工作原理与Serial垃圾回收器类似,它也使用标记-复制算法来清理垃圾对象,但它可以同时使用多个线程来进行清理,从而减少垃圾回收的停顿时间。
3. Parallel Scavenge 垃圾回收器
Parallel Scavenge 垃圾回收器是一种并行的垃圾回收器,它被设计用于处理大量的垃圾数据,可以最大限度地利用多核 CPU 的性能。Parallel Scavenge 垃圾回收器主要用于高吞吐量的应用程序,例如 Web 应用程序或后端服务器。
Parallel Scavenge 垃圾回收器的工作原理是,将堆分成多个区域,并使用标记-复制算法来清理垃圾对象。Parallel Scavenge 垃圾回收器同时使用多个线程来处理垃圾回收,从而加速清理过程。
4. Serial Old 垃圾回收器
Serial Old 垃圾回收器是 Serial 垃圾回收器的老年代版本,它用于清理老年代中的垃圾对象。它与Serial垃圾回收器相同,也是单线程的,会暂停所有的 Java 线程来进行垃圾回收。由于它是单线程的,因此清理时间较长,会导致较长的停顿时间。Serial Old 垃圾回收器主要用于小型应用程序和测试环境。
5. Parallel Old 垃圾回收器
Parallel Old 垃圾回收器是 Parallel Scavenge 垃圾回收器的老年代版本,它用于处理老年代中的大量垃圾数据。Parallel Old 垃圾回收器是一种并行的垃圾回收器,可以最大限度地利用多核 CPU 的性能。
Parallel Old 垃圾回收器的工作原理与 Parallel Scavenge 垃圾回收器类似,将堆分成多个区域,并使用标记-复制算法来清理垃圾对象。Parallel Old 垃圾回收器同时使用多个线程来处理垃圾回收,从而加速清理过程。
6. CMS 垃圾回收器
CMS 垃圾回收器是一种并发的垃圾回收器,它不会暂停所有的 Java 线程来进行垃圾回收,而是在应用程序运行时与垃圾回收同时进行。CMS 垃圾回收器主要用于响应时间要求严格的应用程序,例如 Web 应用程序或交易系统。
CMS 垃圾回收器的工作原理是,将堆分成多个区域,并使用标记-清除算法来清理垃圾对象。与其他垃圾回收器不同的是,CMS 垃圾回收器会在应用程序运行时与垃圾回收同时进行,而不是暂停所有的 Java 线程来进行垃圾回收。这意味着 CMS 垃圾回收器可以最大限度地减少垃圾回收的停顿时间。
6.1 工作的具体步骤如下:
- 初始标记(Initial Mark):在此阶段,GC 线程会暂停应用程序的执行,然后标记所有直接可达的对象,并记录它们的引用信息,以便在后续的标记过程中能够正确地处理它们。
- 并发标记(Concurrent Mark):在此阶段,GC 线程会启动并发标记线程来进行并发标记,遍历整个堆,标记所有与初始标记中直接可达的对象间接可达的对象。
- 重新标记(Remark):在此阶段,GC 线程会再次暂停应用程序的执行,重新遍历堆中所有被标记的对象,并标记那些在并发标记期间被修改或新增的对象。
- 并发清理(Concurrent Sweep):在此阶段,GC 线程会启动并发清理线程来进行并发清理,清理未被标记的垃圾对象,并回收堆内存。
6.2 存在问题
CMS 回收器可以在应用程序运行时进行垃圾回收,但是在回收过程中可能会出现“浮动垃圾”问题和内存碎片化问题。
-
浮动垃圾可能产生并发失败
在上述并发标记和并发清除过程中,应用程序在运行中同时在修改和使用对象,会产生新的垃圾。但是标记过程已经结束了,产生的垃圾不会在本次垃圾收集被回收,只好等待下次 GC 时再进行回收,这部分称为浮动垃圾。由于垃圾收集阶段用户线程还在同步运行,就不能像其他收集器一样等待老年代几乎完全使用后再进行收集,必须预留一部分空间供并发收集时的应用程序运行使用。当这部分空间使用完毕后,新的对象已无法分配到内存,就会产生并发失败。这会导致虚拟机启动后备预案:冻结用户的所有线程,临时启用 Serial Old 收集器来进行老年代的收集。
-
内存碎片
在 CMS 回收器中,由于采用了标记-清除算法,当进行清除操作时,只会回收已标记的内存块,因此会出现内存碎片问题。具体来说,CMS 回收器在进行并发清理操作时,只会回收那些已标记为垃圾的对象,而对于那些没有被标记为垃圾的对象,CMS 回收器不会处理,它们会留在堆中的任意位置,形成内存碎片。由于内存碎片问题会导致内存空间的利用率下降,因此会影响应用程序的性能和稳定性。一方面,由于内存碎片导致内存空间无法被充分利用,因此 CMS 回收器可能需要频繁地触发 GC,以清理那些被浪费的内存空间,这样就会影响应用程序的吞吐量。另一方面,由于内存碎片导致内存空间不连续,因此在进行内存分配时,需要寻找足够大且连续的内存空间,这样就会导致内存分配效率下降,从而影响应用程序的响应时间。
为了解决内存碎片的问题,CMS 提供了下面两个参数来就行内存整理:
-XX:UseCMSCompactAtFullCollection
是一个布尔类型的参数,表示在 CMS 回收器执行 Full GC 操作时是否进行内存压缩。当该参数设置为 true 时,在进行 Full GC 操作时,CMS 回收器会自动启用内存压缩算法来整理内存碎片,从而提高内存的利用率和分配效率。-XX:CMSFullGCBeforeCompaction
是一个整型参数,表示在 CMS 回收器执行多少次 Full GC 操作后进行一次内存压缩。当该参数设置为一个正整数时,CMS 回收器会在执行指定次数的 Full GC 操作后,自动启用内存压缩算法来整理内存碎片。通过调整该参数的值,可以控制内存压缩的频率,从而进一步优化内存管理效率。需要注意的是,内存压缩算法会消耗一定的 CPU 和内存资源,因此在进行 Full GC 操作时启用内存压缩可能会导致应用程序的性能下降。因此,需要根据实际情况来调整相关参数,以取得最佳的性能表现。
7. G1 垃圾回收器
G1 垃圾回收器是一种面向服务端的垃圾回收器,它能够在保证较短停顿时间的同时,达到很高的吞吐量。G1 垃圾回收器将堆内存划分为多个大小相等的区域,每个区域可以是 Eden 区、Survivo 区或 Old 区。G1 垃圾回收器采用分代垃圾回收的思想,对于每个区域的垃圾回收采用标记-整理算法。
G1 垃圾回收器的优点在于,可以在一定程度上避免 Full GC,减少停顿时间。此外,G1 垃圾回收器还支持增量清理和并发清理,能够在保证较短停顿时间的同时,达到很高的吞吐量。
7.1 工作的具体步骤
- 初始标记(Initial Mark):在此阶段,GC 线程会暂停应用程序的执行,标记所有直接可达的对象,并记录它们的引用信息,以便在后续的标记过程中能够正确地处理它们。此阶段与 CMS 的初始标记类似。
- 并发标记(Concurrent Mark):在此阶段,GC 线程会启动并发标记线程来进行并发标记,遍历整个堆,标记所有与初始标记中直接可达的对象间接可达的对象。此阶段与 CMS 的并发标记类似。
- 最终标记(Final Mark):在此阶段,GC 线程会暂停应用程序的执行,再次遍历堆中所有被标记的对象,并标记那些在并发标记期间被修改或新增的对象,同时计算每个区域中的垃圾比例。
- 筛选回收(Live Data Counting and Evacuation):在此阶段,GC 线程会启动筛选回收线程,基于最终标记中的垃圾比例和空间利用率等信息,决定优先回收哪些区域,并将其内存中的存活对象转移到其他区域,以便后续的并发清理。
- 并发清理(Concurrent Clean-up):在此阶段,GC 线程会启动并发清理线程来进行并发清理,清理未被标记的垃圾对象,并回收堆内存。
7.2 实现逻辑
G1 垃圾收集器的设计思想是将整个 Java 堆内存分为多个大小相等的连续区域(Region),每个区域的大小一般为 1MB ~ 32MB 之间,其中一部分区域被划分为 Eden 区,一部分被划分为 Survivor 区,剩余的区域被划分为 Old 区。G1 在收集垃圾时,会优先回收占用空间最多的区域,以此来最大程度上减少垃圾回收的时间和成本。
G1 垃圾收集器中的 Region 是通过一个叫做 HeapRegion 的对象来实现的。每个 HeapRegion 对象都包含了一个连续的内存区域和一些用于垃圾回收的标记信息。G1 在收集垃圾时,会首先对所有的 HeapRegion 进行标记,然后根据标记的情况来决定哪些区域需要回收,哪些区域可以保留。
在对 HeapRegion 进行回收时,G1 垃圾收集器会采用一种叫做 Evacuation Pause 的策略,即将存活对象从一个 HeapRegion 复制到另一个 HeapRegion 中,并释放原 HeapRegion 的空间。通过这种方式,G1 垃圾收集器可以有效地避免内存碎片的问题,同时还能够保证高效的垃圾回收。
G1 的分区 Region 实现逻辑可以进一步细分为如下几个方面:
-
划分区域
G1 首先会将整个 Java 堆内存空间划分成若干个大小相等的 Region,每个 Region 默认大小为 1MB(在某些 JVM 版本中可以配置为其他大小),这些 Region 会被分为不同的集合:eden、survivor、old、humongous 等,其中 eden 区域用于存放新生代对象,survivor 区域则是用来存放 eden 区域中幸存的对象,old 区域用于存放老年代对象,humongous 区域则专门用于存放超大对象(即单个对象的大小超过 Region 大小的一半)。
-
选择收集区域
G1 在进行垃圾回收时,会选择一个或多个收集集合作为本次 GC 的目标,这个选择过程主要涉及到以下因素:
- 集合中对象的填充度和总共占用空间;
- 集合的 Region 数量和大小;
- 集合之间的负载均衡情况。
具体的选择策略是:在收集集合中选取一个 Region 作为收集器的起始点,从该起始点开始对整个收集集合进行 Tracing(也就是找到需要回收的对象),同时,会根据 Tracing 过程中发现的对象的分布情况,动态选择新的 Region 作为下一个起始点,这个选择过程主要依赖于 G1 的分代和时空局部性特性,以及 G1 的 Evacuation Pause 目标等。
-
并发标记和混合回收
G1 采用了增量并发标记算法,将整个 GC 过程分为多个阶段进行,并且在进行垃圾回收时,G1 不再使用传统的分代式 GC,而是采用了全局式 GC,以最小化垃圾回收期间的停顿时间。
具体来说,在 G1 中,第一个阶段是初始标记(Initial Mark),此时 G1 会暂停所有的应用线程,并在整个堆空间中标记出与根对象直接关联的对象。这个过程是串行的,并且会暂停应用线程。
第二个阶段是并发标记(Concurrent Mark),在这个阶段中,应用线程和垃圾回收线程并发执行,垃圾回收线程遍历堆空间,标记出所有与根对象间接关联的对象,并标记到一个记忆集(Remembered Set)中,这个阶段是增量式的,并且不会暂停应用线程。
第三个阶段是重新标记,当某个 G1 分区的存活对象比例低于一定阈值时,G1 会将该分区标记为可回收的。为了尽可能避免内存碎片化,G1 的回收器并不会直接回收一个被标记为可回收的分区,而是将其加入到一个回收集中(Remembered Set),该回收集维护了所有待回收的分区,G1 会在内部使用一组算法来优化回收集的维护。
回收集中的分区数量在 G1 的设计中有一个上限,当回收集中的分区数量超过了阈值,G1 就会启动一次垃圾回收,将回收集中的分区全部回收掉。为了减少停顿时间,G1 的回收器会在空闲时间段内定期地对回收集中的分区进行垃圾回收,而不需要等到分区数量达到阈值。
除了使用回收集来优化分区回收之外,G1 还使用了一些其他的技术来避免内存碎片化。G1 使用了一种叫做 Evacuation Pause 的机制来避免分区内存碎片的产生。当 G1 回收器准备回收某个分区时,它会首先将分区中存活对象复制到一个未使用的分区中,然后标记该分区为可回收的。这个过程被称为 Evacuation Pause,其中 Evacuation 是指将存活对象复制到新的分区中,Pause 则指回收器在进行这个操作时会停顿程序的运行。
G1 还使用了一些其他的技术来优化内存碎片的处理。其中一个重要的技术是在分区中保留了一些空闲空间,当分区中的存活对象被复制到另一个分区时,G1 会尽可能地将这些对象排列在一起,以减少空闲空间的碎片化。G1 还使用了一些特殊的算法来将小对象(通常指占用内存不到 1KB 的对象)存储在专门的分区中,以避免小对象的存储占用大量的内存碎片。
总的来说,G1 通过分区和回收集的机制,以及 Evacuation Pause 等技术来避免内存碎片的产生,从而保证了 JVM 的性能和稳定性。
8. ZGC 垃圾回收器
ZGC 垃圾回收器是一种全新的低延迟垃圾回收器,主要针对大内存、多核心的应用程序场景。ZGC 垃圾回收器将整个堆内存划分为多个大小相等的区域,每个区域采用指针压缩技术来压缩指针,以减小堆内存的占用。ZGC 垃圾回收器采用分代垃圾回收的思想,对于每个区域的垃圾回收采用标记-整理算法。
ZGC 垃圾回收器的优点在于,可以实现极低的停顿时间,甚至可以将停顿时间控制在几毫秒之内。此外,ZGC 垃圾回收器还支持并发清理,能够在保证极低停顿时间的同时,达到很高的吞吐量。
工作的具体步骤如下:
- 初始标记(Initial Mark):在此阶段,GC 线程会暂停应用程序的执行,标记所有直接可达的对象,并记录它们的引用信息,以便在后续的标记过程中能够正确地处理它们。与 CMS 和 G1 不同的是,ZGC 的初始标记是在每个小的分区内完成的,而不是在整个堆中完成的。
- 并发标记(Concurrent Mark):在此阶段,GC 线程会启动并发标记线程来进行并发标记,遍历整个堆,标记所有与初始标记中直接可达的对象间接可达的对象。
- 并发标记(Concurrent Evacuation):在此阶段,GC 线程会启动并发转移线程,将在并发标记期间被标记为垃圾的对象转移到空闲区域,并更新相应的引用关系。
- 并发清理(Concurrent Clean-up):在此阶段,GC 线程会启动并发清理线程来进行并发清理,清理未被标记的垃圾对象,并回收堆内存。与 CMS 和 G1 不同的是,ZGC 的并发清理是增量进行的,它会将堆内存的一小部分清理完后就让应用程序继续执行,以避免长时间的停顿。
四、总结
垃圾回收器 | 类型 | 应用场景 | 停顿时间 | 内存占用 |
---|---|---|---|---|
Serial | 串行 | 单核、新生代、小型应用 | 长 | 低 |
ParNew | 并行 | 多核、新生代 | 短 | 低 |
Parallel Scavenge | 并行 | 多核、新生代 、注重吞吐量 | 短 | 高 |
Serial Old | 串行 | 单核、老年代 | 长 | 低 |
Parallel Old | 并行 | 多核、老年代 | 短 | 高 |
CMS | 并发 | 多核、注重响应时间 | 短 | 较高 |
G1 | 混合 | 多核、大型应用 | 可控 | 高 |
ZGC | 并发 | 多核、大型应用 | 极短 | 高 |
JVM 垃圾回收器有很多种类型,每种类型都有自己的优缺点。了解不同类型的垃圾回收器的工作原理和适用场景可以帮助我们更好地管理和优化 Java 应用程序的内存使用。在实际应用中,我们应该根据应用程序的特点和性能要求来选择合适的垃圾回收器。
