JVM 垃圾收集器全面剖析:算法、实现和优化
JVM 的一个重要组件是垃圾收集器(GC,Garbage Collector)。垃圾收集器负责自动管理 Java 应用程序中的内存资源,以确保程序能够在充足的内存中运行
垃圾收集算法
垃圾收集算法主要用于判断对象是否还在使用,以及如何释放不再使用的对象所占用的内存。常见的垃圾收集算法包括
标记-清除算法(Mark-Sweep)
首先,将所有对象分为两类,一类是可达对象,指的是从根节点(例如局部变量、静态变量等)通过引用链可以访问到的对象;另一类是不可达对象,指的是无法通过根节点访问到的对象。标记-清除算法首先将所有可达对象进行标记,然后对标记的对象进行清除。
标记-清除算法分为两个阶段:标记阶段和清除阶段。
在标记阶段,垃圾收集器从根节点(如局部变量、静态变量等)出发,遍历所有可达对象(即从根节点通过引用链可以访问到的对象),并将这些对象进行标记。在实际实现中,标记可以通过在对象头中设置一个标志位来实现。
标记阶段完成后,进入清除阶段。清除阶段会遍历整个堆空间,释放未被标记的对象所占用的内存。这样,不再使用的对象所占用的内存就被回收了。
复制算法(Copying)
复制算法将内存分为两个相等的区域,同时只使用一个区域。当垃圾收集开始时,遍历所有可达对象,并将这些对象及其引用复制到另一个区域中。然后丢弃原区域的所有内容,将内存指针指向新的区域。这样,新区域中的内存就不再有碎片,可以提高内存分配效率。
复制算法主要解决了标记-清除算法中的内存碎片问题。复制算法将内存空间划分为两个相等的区域 A 和 B(例如,半区),同时只使用一个区域(例如区域 A)来存储对象。当垃圾收集开始时,垃圾收集器遍历所有可达对象,并将这些对象及其引用复制到另一个区域(例如区域 B)中。接着,清空区域 A 的所有内容,将内存指针指向区域 B。因此,活动对象被复制到新的区域后,内存空间变为连续的。
复制算法的主要问题是效率低下,因为需要将所有可达对象复制到新的内存区域,而且内存的一半空间始终是空闲的。
标记-整理算法(Mark-Compact)
标记-整理算法结合了标记-清除和复制算法的优点,主要用于解决老年代的垃圾收集问题。
首先,在标记阶段,垃圾收集器和标记-清除算法一样,从根节点开始遍历所有可达对象并进行标记。
接下来,在整理阶段,将所有被标记的对象向一端靠拢,并丢弃所有未标记的对象。这样一来,内存空间会变得连续,并消除了碎片问题。
标记-整理算法的主要优点是避免了复制算法中的空间浪费问题。但是,标记-整理算法在整理阶段需要移动对象,因此可能会导致一定的性能损耗。
垃圾收集器实现
Serial 收集器
Serial 收集器是一个单线程收集器,它在垃圾收集时,只使用一个线程去执行收集操作。在执行垃圾收集任务时,需要暂停其他所有的工作线程(称为 Stop-The-World,简称 STW),直到垃圾收集完成。
Serial 收集器适用于对内存和 CPU 资源有限的场景,以及客户端应用程序。由于它是单线程的,因此 Serial 收集器在多核处理器环境下相对效率较低。
Serial 收集器使用标记-复制算法进行新生代收集(Minor GC),使用标记-整理算法进行老年代收集(Major GC 或 Full GC)。
Parallel(Throughput)收集器
Parallel 收集器(也称为吞吐量收集器)是一个并行收集器。与 Serial 收集器不同,Parallel 收集器使用多个线程同时执行垃圾收集任务,这在多核处理器环境下可以高效利用 CPU 资源,从而提高垃圾收集的效率。
Parallel 收集器主要目标是提高系统的吞吐量。它默认情况下会尽量利用可用的 CPU 核心来加速垃圾收集操作。类似于 Serial 收集器,Parallel 收集器在垃圾收集期间也需要暂停其他所有工作线程。
Parallel 收集器同样使用标记-复制算法进行新生代收集,使用标记-整理算法进行老年代收集。
CMS(Concurrent Mark Sweep)收集器
CMS 收集器是一种以降低停顿时间为目标的收集器。与 Serial 和 Parallel 收集器不同,CMS 收集器在执行垃圾收集任务时,并不需要暂停所有工作线程。CMS 收集器通过并发标记-清除算法实现,其核心思想是将垃圾收集过程中的一部分工作与应用线程并发执行,从而减少单次垃圾收集引起的暂停时间。
CMS 收集器的垃圾收集过程主要分为下面几个阶段:
初始标记(Initial Mark,需要 STW):标记与根节点直接关联的对象
并发标记(Concurrent Mark):处理应用程序并发运行时标记可达对象
重新标记(Remark,需要 STW):修正并发标记阶段因应用程序运行引入的新引用关系
并发清除(Concurrent Sweep):清除不可达对象
CMS 收集器适用于对响应时间有较高要求的应用场景,比如 Web 服务器、缓存服务器等。
G1(Garbage-First)收集器
G1 收集器是一种新型的收集器,旨在替代 CMS 收集器。G1 收集器主要针对大堆内存的应用程序,它的主要目标是将程序的暂停时间控制在可预测的范围内,以便满足较严格的停顿时间要求。
G1 收集器将整个 Java 堆划分为许多连续的内存区域(Region),这些区域大小相等且大小可配置。每个区域可以是 Eden 空间、Survivor 空间或者 Old 空间的一部分。G1 收集器通过对不同区域进行优先级排序,在垃圾收集时首先处理优先级较高的区域。
G1 收集器的垃圾收集过程主要分为以下几个阶段:
初始标记(Initial Mark,需要 STW):标记与根节点直接关联的对象
并发标记(Concurrent Mark):处理应用程序并发运行时标记可达对象
最终标记(Final Mark,需要 STW):修正并发标记阶段因应用程序运行引入的新引用关系
筛选回收(Evacuation,部分需要 STW):将选定的区域中的存活对象复制到其他空闲区域,释放回收区域的空间
G1 收集器在实现上使用了标记-复制算法的变种,并引入了并发执行和增量回收的策略。这使得 G1 能够在保证较低的停顿时间的同时,也能实现较高的内存利用率。
垃圾收集优化策略
调整堆大小
合理地设置堆的初始大小(-Xms)和最大大小(-Xmx)可以避免频繁的垃圾收集。一般来说,初始大小设置较小可以降低应用启动所需的资源,而最大大小要根据应用程序的内存需求进行设置。堆大小不应该设置过大,以免造成内存浪费;也不应该设置过小,避免频繁触发垃圾收集导致性能下降。
调整新生代和老年代的比例
JVM 堆内存分为新生代(包括 Eden 空间和 Survivor 空间)和老年代,两者的大小比例会影响垃圾收集的效率和频率。通过-Xmn 参数设置新生代的大小,从而调整新生代和老年代的比例。
通常,新生代越大,Minor GC 的时间间隔越长,但 Full GC 的时间也相对较长。因此,需要根据应用程序的对象生命周期特点来设置合适的新生代和老年代比例。
选择合适的垃圾收集器
根据应用程序的特点和需求,选择合适的垃圾收集器。例如,对于需要低延迟的应用程序,可以考虑使用 CMS 或 G1 收集器。对于需要高吞吐量的应用程序,可以选择 Parallel 收集器。对于单核处理器或内存资源有限的场景,可以使用 Serial 收集器。
监控和分析垃圾收集日志
监控并分析垃圾收集日志有助于发现性能瓶颈、内存泄漏或其他 GC 问题。通过查看日志,可以了解 GC 的频率、暂停时间、收集效果等指标,从而对垃圾收集进行优化。可以使用-Xloggc 参数将 GC 日志输出到文件。
优化程序代码
减少不必要的对象创建和长生命周期对象。对于短生命周期的对象,可以尽量复用已有对象,减少创建新对象的开销,降低垃圾收集频率。例如,可以使用 StringBuilder 替代 String 进行字符串拼接操作。
对于长生命周期的对象,避免创建过多的大对象。大对象容易导致内存碎片问题,降低内存使用率。合理设计数据结构,避免不必要的引用关系,可减少长生命周期对象的数量。
使用 JVM 参数进行优化
-XX:SurvivorRatio 设置 Eden 空间和 Survivor 空间的比例。
-XX:MaxTenuringThreshold 控制对象在 Survivor 空间的晋升阈值,达到阈值后对象将进入老年代。
-XX:+DisableExplicitGC 禁止 System.gc()显式调用,避免应用程序触发频繁的垃圾收集。
-XX:+UseStringDeduplication 开启 Java 8 中的字符串重复数据清理功能,减少相同字符串在内存中的存储。
这些策略需要根据应用程序的具体需求进行调整。垃圾收集优化是一个持续的过程,需要不断地进行调整和优化,才能使应用程序在性能和资源使用方面达到最佳效果。
版权声明: 本文为 InfoQ 作者【xfgg】的原创文章。
原文链接:【http://xie.infoq.cn/article/3cbc337383364178f76d84b5f】。未经作者许可,禁止转载。
评论