Java的垃圾回收(GC)是其自动内存管理的核心,负责回收无用对象所占用的内存。研发经验少或涉足此区域比较少的程序员顶多看了点理论,却没有实际的实践经验无从选择,因此肖哥根据自身的实际经验给大家分享分享。

关注公号Solomon肖哥弹架构获取更多精彩内容 ,需要调优案例的可以留言

JVM优化系列 ,喜欢的读者 , 一定要订阅哦

欢迎 点赞,收藏,关注 。

一、Java GC设计目的

释放程序员手动清理资源繁琐的工作,减少疏忽导致内存泄漏等问题,因此Java GC的主要目标是自动管理对象的生命周期,防止内存泄漏,同时最小化对应用程序性能的影响。不同的GC算法和收集器适用于不同的应用场景和性能要求。 整体GC思路如下:

二、GC算法及其收集器详解

1. Serial收集器

功能:单线程执行的GC,专注于新生代(第五部分有解释)。

  • 优点:内存占用小,适合内存受限的环境。
  • 缺点:GC时会暂停所有应用线程(STW),不适合高并发环境。
  • 改善方案:对于小型应用,通常不需要改善,因为STW时间较短。如果需要,可以增加堆内存或升级到多核处理器。

设计目的:为早期单核处理器设计,简化内存管理。

合适业务:小型应用或单核设备、小型的个人博客系统、小型在线商城的后端服务,用户量不大、小型企业的内部管理系统 等。

适合回收数据特点:例如,用户会话信息、购物车中的商品对象。

工作流程图

内部回收原理图

2. Parallel收集器(吞吐量优先)

功能:多线程执行的GC,专注于新生代(第五部分有解释)。

  • 优点:提高GC速度,提升吞吐量。
  • 缺点:STW时间可能较长,对延迟敏感的应用可能受影响。
  • 改善方案:调整-XX:ParallelGCThreads参数来优化线程数,减少STW时间。

设计目的:为多核处理器设计,提高GC的并行度。

合适业务:需要高吞吐量的应用、大型在线教育平台的视频处理服务、需要处理大量数据的后台批处理系统 等。

适合回收数据特点:例如,视频处理过程中产生的临时数据。

工作流程图

内部回收原理图

Parallel GC有两种组合方式:

  1. 使用 -XX:+UseParallelGC 参数来启用 Parallel Scavenge(新生代)和 PSMarkSweep(老年代,Serial Old的并行版本)收集器组合。
  2. 使用 -XX:+UseParallelOldGC 参数来启用 Parallel Scavenge 和 Parallel Old 收集器组合,这样新生代和老年代的垃圾回收都是并行的。

Parallel Scavenge(PS)收集器的主要目标是吞吐量最大化,它通过牺牲一些STW时间来换取更高的总吞吐量70。Parallel Old GC使用“标记-整理”算法来优化老年代的垃圾回收

3. CMS收集器(响应时间优先)

功能:以最小化停顿时间为目标的并发GC,专注于老年代(第五部分有解释)。

  • 优点:减少GC引起的停顿时间。
  • 缺点:可能产生内存碎片,CPU资源占用较高。
  • 改善方案:调整-XX:CMSInitiatingOccupancyFraction参数来控制CMS触发的堆内存占用阈值,减少内存碎片。

设计目的:为了降低GC对应用响应时间的影响。

合适业务:需要快速响应的应用、股票交易平台,需要快速处理交易数据、需要快速响应的在线交易平台 等。

适合回收数据特点:例如,交易记录、价格变动信息。

工作流程图

内部回收原理图

阶段说明

  1. 初始标记(CMS-initial-mark)  - 这个阶段会暂停所有应用线程,并标记出所有直接可达对象。
  2. 并发标记(CMS-concurrent-mark)  - 在这个阶段,垃圾回收线程和应用线程并发执行。垃圾回收线程会遍历堆中的对象图,找出并标记出存活的对象。
  3. 重新标记(CMS-remark)  - 这个阶段会再次暂停所有应用线程,修正并发标记阶段中由于应用线程继续运行而可能产生标记错误的对象。
  4. 并发清除(CMS-concurrent-sweep)  - 最后,垃圾回收线程并发清除所有未被标记为存活的对象

4. G1收集器

功能:区域化堆内存,停顿时间可预测。

  • 优点:停顿时间可预测,适合大堆内存。
  • 缺点:相对于CMS,G1的吞吐量较低。
  • 改善方案:调整-XX:MaxGCPauseMillis参数来设置最大停顿时间目标,优化G1的停顿时间与吞吐量的平衡。

设计目的:为了平衡吞吐量和延迟,适合大堆内存。

合适业务:大数据处理、内存密集型应用、大规模用户数据分析系统、需要处理PB级别数据的大数据应用 等。

适合回收数据特点:例如,用户行为日志、大规模的数据统计信息。

工作流程图

内部回收原理图

5. ZGC收集器

功能:低延迟,可扩展到TB级别堆内存。

  • 优点:极低的停顿时间,支持超大堆内存。
  • 缺点:相对较新,可能存在一些未知的性能问题。
  • 改善方案:监控ZGC的性能,及时调整-XX:ZCollectionInterval-XX:ZHeapMax等参数。

设计目的:为了满足现代大规模应用的需求。

合适业务:云计算平台、大数据处理、云服务提供商的虚拟机内存管理 等。

适合回收数据特点:例如,虚拟机的内存镜像、大规模容器集群的状态信息。

工作流程图

内部回收原理图

阶段说明

  1. 内存布局:ZGC将堆内存分为多个区域(Region),每个区域可以是小、中或大型,以适应不同大小的对象。
  2. 染色指针:ZGC使用染色指针技术,将GC信息存储在指针的高位,从而减少对对象头的依赖。
  3. 三色标记:ZGC采用三色标记法来识别存活对象。对象可以是白色(未标记)、灰色(已访问但未完全标记其引用的对象)或黑色(已访问并完全标记其引用的对象)。
  4. 读屏障:ZGC使用读屏障来处理并发访问和修改对象引用的情况,确保在并发标记阶段正确更新对象状态。
  5. 并发标记:GC线程与应用线程并发运行,标记存活对象,减少停顿时间。
  6. 再标记:处理并发标记阶段可能遗漏的对象,通常需要短暂的停顿。
  7. 转移:将标记为存活的对象转移到新的内存区域,以减少内存碎片。
  8. 重定位:更新所有指向已转移对象的引用,确保它们指向新位置。
  9. 内存多重映射:ZGC使用内存多重映射技术,将物理内存映射为不同的虚拟内存视图,实现并发垃圾回收。

三、GC调优方式

  • 根据应用特点和业务需求选择合适的GC收集器。
  • 监控GC性能,根据监控结果调整JVM参数。
  • 避免内存泄漏,定期审查代码和内存使用情况。

四、不同场景下的调优案例

1、频繁的Minor GC和Major GC问题

场景: 服务要求低延迟和高可用性。遇到的问题包括每分钟100次Minor GC和每4分钟一次Major GC,单次Minor GC耗时25ms,单次Major GC耗时200ms,接口响应时间50ms。

优化措施

  • 增大新生代空间来降低Minor GC频率。
  • 调整新生代的Eden区大小,减少单次Minor GC时间。

结果:通过扩容新生代,Minor GC频率降低60%,服务响应时间TP90和TP99都下降了10ms以上。

2、CMS GC调优

场景: 对于一个对响应速度要求极高的系统,CMS GC算法可能导致长时间的Full GC停顿。

优化措施

  • 降低触发CMS GC的阈值,使用参数-XX:CMSInitiatingOccupancyFraction
  • 增加CMS线程数,使用参数-XX:ConcGCThreads
  • 增大老年代空间。

结果:通过上述措施,减少了Full GC的发生,降低了系统的停顿时间。

3、Feed产品的GC优化

场景: Feed产品需要处理高吞吐量和低延迟的用户请求。

优化措施

  1. 选择合适的GC算法

    • 评估并选择G1ZGC收集器,因为它们提供了可预测的停顿时间和高吞吐量。
  2. 调整JVM参数

    • 设置-XX:+UseG1GC-XX:+UseZGC启用G1或ZGC收集器。
    • 使用-XX:MaxGCPauseMillis设置最大GC停顿时间目标。
    • 使用-XX:GCTimeRatio调整吞吐量与停顿时间的平衡。
  3. 优化内存使用

    • 根据应用的内存需求,使用-Xms-Xmx设置初始堆大小和最大堆大小。
  4. GC线程任务分配

    • 使用-XX:ParallelGCThreads设置并行GC线程数,通常设置为CPU核心数。
  5. 减少对象分配/晋升率

    • 优化代码以减少临时对象的创建,例如使用对象池或重用对象。
  6. 增加各代空间的大小

    • 使用-XX:NewRatio调整新生代与老年代的比例。
    • 使用-XX:SurvivorRatio调整新生代中Eden区与Survivor区的比例。

结果:通过GC优化,Feed 成功地降低了GC对响应时间的影响,提升了用户体验。

4、GC优化实践

场景: Java应用性能未达预期,需要GC调优。

优化措施

  1. 明确优化目标

    • 确定是降低延迟还是提高吞吐量,或两者平衡。
  2. 选择合适的GC回收器

    • 根据应用特点选择ParNew+CMSG1收集器。
  3. 调整JVM参数

    • 使用-XX:NewSize-XX:MaxNewSize设置新生代的初始大小和最大大小。
    • 使用-XX:NewRatio调整新生代与老年代的比例。
  4. 优化GC线程和内存分配

    • 使用-XX:ConcGCThreads设置并发GC线程数。
  5. 减少对象分配/晋升率

    • 分析堆转储以识别内存泄漏和大对象。
  6. 监控和调整

    • 使用JVM监控工具,如jconsole或VisualVM,实时监控内存使用和GC活动。
    • 根据监控结果调整GC策略和参数。

结果:通过GC调优,减少了GC频率和单次GC时间,提升了服务的可用性和响应速度。

5、 在线游戏平台

  • 场景:在线游戏需要快速响应玩家操作,GC延迟会影响游戏体验。使用Serial Old GC,平均响应时间300ms,最大响应时间800ms。
  • 优化目标:降低延迟。
  • 措施:选择CMS或G1 GC,设置较短的-XX:MaxGCPauseMillis值至100ms,优化内存分配策略以减少GC频率。
  • 结果: 平均响应时间降低至100ms,最大响应时间降低至300ms,提升了玩家的游戏体验。

6、 大数据处理系统

  • 场景:大数据处理任务通常在后台运行,可以容忍较长的GC暂停时间。使用CMS GC,吞吐量为80%,处理大数据任务需要8小时。
  • 优化目标:提高吞吐量。
  • 措施:选择Parallel或Parallel Scavenge GC,调整-XX:GCTimeRatio至99% 以优化吞吐量,可能增加堆大小以减少GC频率。
  • 结果:吞吐量提升至98%,处理相同数据任务时间减少至4小时。

7、 电子商务网站

  • 场景:电子商务网站需要处理大量用户请求,同时要求快速响应。使用默认的Parallel GC,网站在高峰时段响应时间不稳定。
  • 优化目标:平衡延迟和吞吐量。
  • 措施:使用G1 GC,合理设置-XX:MaxGCPauseMillis-XX:GCTimeRatio,可能需要监控并调整新生代大小和老年代大小,以平衡GC时间和频率。
  • 结果:网站平均响应时间稳定在200ms以内,吞吐量保持在90%以上,用户体验显著提升。

8、 实时监控系统

  • 场景:实时监控系统需要持续不断地处理和展示数据,对延迟非常敏感。使用CMS GC,监控数据偶尔出现延迟,最大延迟时间500ms。
  • 优化目标:最小化延迟。
  • 措施:使用ZGC或Shenandoah等低延迟GC,这些收集器设计用于提供可预测的短暂停时间。
  • 结果:监控数据的延迟时间降低至最大100ms,数据展示更加实时。

9、科学计算应用

  • 场景:科学计算应用通常运行在多核服务器上,需要最大化CPU利用率。使用Serial GC,科学计算任务完成时间需要12小时。
  • 优化目标:最大化吞吐量。
  • 措施:使用Parallel Scavenge GC,调整-XX:+UseAdaptiveSizePolicy以允许JVM自适应地调整内存分配策略。
  • 结果:计算任务完成时间缩短至6小时,CPU利用率提升至95%。

概念解释:

  • 延迟:GC导致的应用程序暂停时间。对于需要快速响应的应用程序(如在线游戏、实时交易系统),较低的延迟是首选。
  • 吞吐量:应用程序运行时间占总时间的比例(包括GC时间)。对于批处理作业或后台处理任务,高吞吐量是首选,因为它们可以容忍较长的GC暂停时间。

五、专注区域类型划分

专注于新生代的垃圾回收器:

  • 特点:新生代是新创建对象的存放区域。由于这些对象的生命周期通常较短,新生代的垃圾回收器需要高效地处理大量临时对象的创建和销毁。
  • 挑战:新生代中的对象大多数会很快变得不可达,因此需要快速识别并清除这些对象。
  • 策略:通常使用复制算法,将存活的对象从Eden区复制到Survivor区,然后根据对象的年龄逐渐晋升到老年代。

1. Serial收集器

  • 功能:简单高效的单线程GC,适合新生代。
    • 优点:资源占用小,适合单核处理器或小数据集。
    • 缺点:在GC时会暂停应用线程,不适合多线程或高并发环境。

2. Parallel收集器

  • 功能:使用多个线程进行GC,提高新生代的回收效率。
    • 优点:提高GC速度,提升吞吐量,适合多核处理器。
    • 缺点:GC时会有STW,但通常比Serial收集器短。

专注于老年代的垃圾回收器:

  • 特点:老年代是长期存活对象的存放区域。由于这些对象的生命周期较长,老年代的垃圾回收器需要优化长时间运行的应用的内存使用,减少GC的频率和影响。
  • 挑战:老年代的对象存活时间长,GC需要更细致地处理对象间的关系和内存碎片问题。
  • 策略:通常使用标记-清除-整理算法,以减少内存碎片并优化内存使用。

3. CMS收集器

  • 功能:以最小化停顿时间为目标的并发GC,适合老年代。
    • 优点:通过并发标记和清除阶段减少GC引起的停顿时间。
    • 缺点:可能产生内存碎片,需要额外的整理阶段。

4. G1收集器

  • 功能:区域化堆内存管理,可预测的停顿时间,适用于整个堆,包括新生代和老年代。
    • 优点:停顿时间可预测,适合大堆内存。
    • 缺点:吞吐量可能不如Parallel收集器。

5. ZGC收集器

  • 功能:低延迟,可扩展到TB级别堆内存,适用于整个堆。
    • 优点:极低的停顿时间,适合处理大量数据。
    • 缺点:相对较新,可能需要调整参数以优化性能。

六、新老代业务选择举例

适合新生代的业务数据(短生命周期):

  1. 用户会话信息 - 用户与应用的单次交互。
  2. 购物车项 - 用户浏览电商网站时的临时购物车条目。
  3. 网页缓存数据 - 快速访问的网页内容。
  4. 临时计算结果 - 批处理任务中的中间结果。
  5. 日志记录信息 - 应用程序生成的临时日志条目。
  6. 数据库查询缓存 - 短期内需要的数据库结果集。
  7. 用户输入数据 - 表单提交的临时数据。
  8. API请求响应 - 短期内需要返回的数据。
  9. 临时文件上传 - 用户上传但还未处理的文件。
  10. 线程局部变量 - 方法执行期间的局部状态。
  11. 异常对象 - 捕获但未处理的异常实例。
  12. 测试数据 - 单元测试中使用的一次性数据。
  13. 一次性任务结果 - 执行后不再需要的计算结果。
  14. 用户界面元素 - 界面上临时显示的数据。
  15. 游戏场景对象 - 视频游戏中的短暂存在对象。

适合老年代的业务数据(长生命周期):

  1. 用户账户信息 - 用户的长期存储账户数据。
  2. 配置数据 - 应用的长期配置信息。
  3. 应用程序状态 - 长时间运行的应用程序状态。
  4. 持久化缓存数据 - 长时间有效的缓存条目。
  5. 大型报表数据 - 长时间处理和查看的报表。
  6. 长期会话数据 - 长时间登录的用户会话。
  7. 应用程序日志文件 - 长时间累积的日志信息。
  8. 用户个人设置 - 用户的长期个性化设置。
  9. 历史交易记录 - 金融交易的历史数据。
  10. 长期订阅信息 - 用户的长期订阅数据。
  11. 企业资源规划数据 - 长期需要的企业资源数据。
  12. 长期用户行为分析 - 用户行为的长期跟踪分析。
  13. 系统性能指标 - 长期监控的性能指标。
  14. 数据库连接池对象 - 长时间持有的数据库连接。
  15. 大型文件存储 - 长期存储的大量数据文件。

小结

新生代和老年代的垃圾回收策略是不同的,因为它们处理的对象生命周期不同。新生代的对象通常很快死亡,而老年代的对象可能存活很长时间。因此,选择合适的垃圾回收器和调整JVM参数对于优化应用程序性能至关重要。例如,对于需要快速响应的交互式应用,可能需要使用CMS或G1收集器来减少老年代的GC停顿时间;而对于处理大量临时数据的批处理应用,可能需要使用Parallel收集器来提高新生代的GC效率。

七、JVM不同区域设计思想

  1. 新生代(Young Generation)

    • 目的:存储新创建的对象。大多数对象的生命周期短,因此新生代的垃圾回收频繁且速度快。
    • 回收策略:通常使用复制算法,将存活的对象复制到老年代。
  2. Eden区

    • 目的:新生代的一个子区域,大多数新对象首先分配在这里。
    • 回收策略:Eden区的对象在Minor GC时被回收或晋升到Survivor区。
  3. Survivor区

    • 目的:作为Eden区和老年代之间的过渡,存储从Eden区复制过来的存活对象。
    • 回收策略:Survivor区的对象在一定年龄后晋升到老年代。
  4. 老年代(Old Generation)

    • 目的:存储长时间存活的对象,这些对象从新生代晋升上来。老年代的对象通常占用更多内存,并且垃圾回收的频率较低。
    • 回收策略:使用标记-清除或标记-清除-整理算法。
  5. 永久代(Permanent Generation, PermGen) (Java 8之前)

    • 目的:存储类的元数据,如类定义、常量池、方法等。PermGen在JDK 8之前使用。
    • 回收策略:由于存储的是元数据,通常垃圾回收不频繁。
  6. 元空间(Metaspace) (Java 8及之后)

    • 目的:替代PermGen,存储类的元数据。元空间位于本地内存(Native Memory)而不是堆内存中。
    • 回收策略:由于元空间不在堆内存中,其垃圾回收与堆内存的GC不同。
  7. 堆外内存(Off-Heap Memory)

    • 目的:存储一些大对象,如直接内存(DirectByteBuffer)等,以减少对堆内存的压力。
  8. 代码缓存(Code Cache)

    • 目的:存储JIT编译器生成的本地机器代码,以加速方法的执行。
  9. G1区域(G1 Regions)

    • 目的:G1收集器将堆内存划分为多个大小相等的区域,每个区域可以独立进行垃圾回收,以实现更细粒度的回收控制。
    • 回收策略:G1收集器根据区域的回收价值和成本进行选择性回收。
  10. Z/ZGC/Shenandoah区域

    • 目的:这些是低延迟垃圾回收器,设计用于处理大规模堆内存,同时保持垃圾回收的低延迟。
    • 回收策略:并发执行大部分垃圾回收工作,减少停顿时间。

八、GC监控工具

  1. jstat

    • jstat 是JDK自带的虚拟机统计信息监控工具,它可以显示本地或远程虚拟机进程中的类加载、内存、垃圾收集、JIT编译等运行数据。
  2. jconsole

    • JConsole 是Java自带的一款监控工具,可以实时监控JVM的运行状态、内存使用情况、垃圾回收情况等信息,并可通过远程连接监视远程服务器上的VM。
  3. VisualVM推荐,工具友好)

    • VisualVM 是一个多合一故障管理工具,提供了一个图形化界面,可以查看本地及远程Java应用程序的性能和资源使用情况,包括内存、垃圾回收、线程和CPU分析等。
  4. jmap

    • jmap(Memory Map for Java,内存映像工具)用于生成堆转储的快照,一般是heapdump或者dump文件,帮助分析内存使用情况。
  5. jstack

    • jstack 用于生成虚拟机当前时刻的线程快照,可用于分析线程状态和性能瓶颈。
  6. jinfo

    • jinfo 可以实时地查看和调整虚拟机的各项参数,如内存设置、GC配置等。
  7. 其他云原始监控 (推荐)

    • 这里不过多介绍,产品特性差不多,收费,功能不错