0

0

说一下jvm 有哪些垃圾回收算法?

幻夢星雲

幻夢星雲

发布时间:2025-10-31 01:01:24

|

792人浏览过

|

来源于php中文网

原创

jvm垃圾回收算法主要有标记-清除、复制和标记-整理三种,分别适用于不同内存区域。标记-清除易产生碎片,复制算法以空间换时间,适合新生代,标记-整理则解决碎片问题,适合老年代。jvm结合多种算法,基于对象生命周期差异实现分代回收,提升性能。现代gc器如g1、zgc、shenandoah通过区域化管理、并发处理和读屏障等技术,在大堆场景下实现低延迟与高吞吐的平衡。选择合适的gc器需根据应用类型、堆大小、对象分配速率和硬件资源综合考量,并通过日志分析与调优持续优化。

说一下jvm 有哪些垃圾回收算法?

JVM的垃圾回收算法主要有三种基本类型:标记-清除(Mark-Sweep)、复制(Copying)和标记-整理(Mark-Compact)。它们各自有其优缺点,并且在实际的JVM实现中,往往会结合使用,以适应不同的内存区域和回收需求。

解决方案

要说JVM的垃圾回收,首先得明白它核心的那几个套路。在我看来,所有复杂的GC器,骨子里都离不开这三种基本算法的影子,只是它们被包装得更精巧,或者说,有了更高级的优化策略。

标记-清除(Mark-Sweep)算法 这是最基础的一种,理解起来也直观。它分两步走:

  1. 标记(Mark):从根对象(GC Roots)开始,遍历所有可达的对象,把它们标记出来。这些被标记的对象就是“活”的,不能被回收。
  2. 清除(Sweep):遍历整个堆,把所有没有被标记的对象(也就是不可达的“死”对象)清除掉。 这种算法有个明显的优点,它不需要移动对象,所以效率相对高,而且不会占用额外的空间。但缺点也挺突出:它会产生大量的内存碎片。想想看,一块大内存被回收后,可能会留下很多小块的“坑”,如果后续需要分配一个大对象,即使总内存是够的,也可能因为没有连续的足够空间而触发另一次GC,甚至导致OOM。这就像你的硬盘,删了很多文件,但碎片化严重,找个大文件都费劲。

复制(Copying)算法 这个算法的思路就完全不同了,它更像是一种“以空间换时间”的策略,特别适合那些生命周期短的对象。它将可用内存分成大小相等的两块,每次只使用其中一块。当这块内存用完了,就将还“活着”的对象复制到另一块空闲的内存上,然后把当前使用的这块内存全部清理掉。

  1. 复制(Copy):将当前已用空间中的存活对象,全部复制到另一块未使用的空间。
  2. 清空(Empty):将当前已用空间直接清空。 它的优点是显而易见的:不会产生内存碎片,而且复制过程中,只要移动堆顶指针就能分配内存,效率很高。但它的缺点也很明显:内存利用率只有50%,因为你总要留一块备用空间。这在寸土寸金的生产环境里,有时候是不能接受的。所以,它通常只用于新生代(Young Generation),因为新生代的对象大部分都是朝生夕死的,存活率很低,复制的成本相对可控。

标记-整理(Mark-Compact)算法 标记-整理是标记-清除的升级版,它在标记之后,并没有直接清除,而是多了一步“整理”。

  1. 标记(Mark):同样,从GC Roots开始,标记出所有存活的对象。
  2. 整理(Compact):将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 这种算法解决了标记-清除的内存碎片问题,而且内存利用率高,因为它不需要像复制算法那样预留空间。但它的缺点也很明显:对象移动的成本很高,尤其是在老年代这种对象数量多、存活率高的区域,每次GC都移动大量对象,会造成较长时间的STW(Stop-The-World),也就是应用暂停。这对于那些对响应时间要求高的应用来说,是致命的。

为什么JVM垃圾回收需要多种算法?它们如何协同优化性能?

你可能会问,既然有这么多算法,为什么不选一个最好的用到底?我的理解是,没有“最好”的算法,只有“最合适”的算法。JVM之所以需要多种垃圾回收算法,并让它们协同工作,核心原因在于对象的生命周期差异和性能目标的多样性。

设想一下,你有一个繁忙的办公室,有些文件(对象)刚用完就扔了,有些文件要长期保存。如果用同一种方式处理,效率肯定不高。JVM正是基于这种“分代假设”(Generational Hypothesis)来设计的:绝大多数对象都是朝生夕死的,而少数对象会长期存活。

因此,JVM把堆内存分成了不同的区域,最常见的就是新生代(Young Generation)和老年代(Old Generation)。

  • 新生代:这里的对象生命周期短,GC频繁。为了追求高效率和避免碎片,通常会使用复制算法。因为存活对象少,复制成本低,而且能够快速回收大量空间。新生代通常还会细分为一个Eden区和两个Survivor区(From和To),这就是复制算法的具体实现。对象先在Eden区分配,GC时,存活对象被复制到其中一个Survivor区,下次GC再复制到另一个,经过几次GC仍存活的对象才会被晋升到老年代。
  • 老年代:这里的对象生命周期长,GC不那么频繁,但每次GC涉及的对象数量多。如果用复制算法,成本太高,而且老年代的内存利用率要求更高。所以,老年代通常会选择标记-整理算法来解决碎片问题,或者使用标记-清除算法(如果对碎片不那么敏感,或者后续有整理的机制)来降低STW时间。当然,现代的GC器,比如CMS、G1,对老年代的回收做了大量优化,试图在吞吐量和延迟之间找到更好的平衡。

这种分代协同的策略,本质上是一种优化:用最适合的算法处理最合适的区域。新生代用复制,追求高吞吐量和低延迟;老年代用标记-整理或更复杂的算法,解决碎片和长期存活对象的回收问题。它们就像是流水线上的不同工位,各司其职,共同完成了垃圾回收这个大任务,以最小的代价维持了应用的持续运行。

现代JVM垃圾回收器:从CMS到ZGC,技术演进与核心特性解析

如果说前面讲的是GC算法的“基本功”,那么现代JVM的垃圾回收器就是这些基本功的集大成者,并且加入了大量创新,旨在解决传统GC器面临的痛点。我们追求的无非是两点:高吞吐量(单位时间内处理更多任务)和低延迟(响应时间快)。但这两者往往是鱼和熊掌,难以兼得。

  • CMS(Concurrent Mark Sweep)收集器: CMS是HotSpot JVM中第一个真正意义上的并发收集器,它的目标是降低GC时的停顿时间(latency),特别适用于对响应时间敏感的Web应用。它主要用于老年代。 它的核心思想是:在标记和清除阶段,尽可能地让GC线程和应用线程并发执行,减少STW时间。它有几个关键步骤:初始标记(STW,但很快)、并发标记(与应用并发)、重新标记(STW,修正并发标记期间对象的变化)、并发清除(与应用并发)。 听起来很美,但CMS也有它的问题:

    1. 浮动垃圾(Floating Garbage):在并发清除阶段,应用还在运行,可能会产生新的垃圾,这部分垃圾只能等到下次GC才能清理,这就是浮动垃圾。
    2. 内存碎片:CMS是基于标记-清除算法的,所以它会产生内存碎片。当碎片过多时,如果需要分配大对象,可能无法找到连续空间,导致提前触发Full GC,而Full GC是STW的。
    3. 对CPU资源敏感:并发执行意味着需要更多的CPU核心来支持GC线程。
  • G1(Garbage-First)收集器: G1是Oracle在JDK 7中推出的,旨在取代CMS,成为下一代低延迟、高吞吐量的收集器。它的设计理念非常独特:它将整个Java堆划分为多个大小相等的独立区域(Region)。每个Region都可以独立地作为Eden、Survivor或者Old区。 G1的核心优势在于:

    1. 可预测的停顿时间:G1允许用户指定一个GC停顿的目标时间(例如,不超过200毫秒)。G1会根据这个目标,选择回收价值最高(垃圾最多)的Region进行回收,这就是“Garbage-First”的由来。
    2. 避免内存碎片:G1在回收Region时,采用的是复制和标记-整理算法的混合模式。它会把存活对象从一个或多个Region复制到新的Region中,这本身就带有整理的效果,因此碎片问题得到了很好的缓解。
    3. 分代和非分代并存:G1依然保留了分代的概念,但它的区域划分让它在处理大对象(Humongous Object)时更灵活,可以直接在老年代区域分配。
  • ZGC和Shenandoah收集器: 这是JDK 11之后出现的,代表了JVM GC的最新发展方向,它们的目标是将GC停顿时间控制在极低的水平(通常是10毫秒以内,甚至更低),即便是在TB级别的堆内存下也能保持。它们的核心技术突破在于使用了着色指针(Colored Pointers)读屏障(Read Barriers)。 简单来说,它们通过在指针中编码GC状态信息,并在每次对象访问时插入读屏障来检测并处理并发GC操作,从而实现了几乎完全并发的垃圾回收。这意味着GC的绝大部分工作可以与应用线程并发执行,STW时间极短,几乎可以忽略不计。

    • ZGC:由Oracle开发,目标是极低的延迟。它利用了多重映射(Multi-Mapping)技术,将虚拟内存区域映射到不同的物理内存区域,实现并发移动对象。
    • Shenandoah:由Red Hat开发,也追求极低延迟。它使用转发指针(Forwarding Pointers)来实现并发移动对象。 这些收集器对于超大规模堆内存、对延迟极其敏感的应用(如金融交易系统、实时大数据处理)来说,是革命性的。当然,它们也并非没有代价,例如对CPU和内存带宽的消耗可能会略高,并且在某些特定场景下可能需要更精细的调优。

选择合适的JVM垃圾回收器:针对不同应用场景的决策与调优考量

选择一个合适的垃圾回收器,就像是为你的汽车选择合适的轮胎,得看路况和驾驶习惯。没有万能的答案,只有最适合你应用场景的那个。

DoMore.ai
DoMore.ai

DoMore.ai 是一个个性化的 AI 工具目录

下载

在做决策时,我通常会考虑以下几个核心因素:

  1. 应用类型和性能目标

    • 吞吐量优先型(Throughput-oriented):如果你跑的是批处理任务、大数据分析,或者不需要实时响应的后台服务,那么你可能更关心单位时间内能处理多少数据,而不是单次请求的响应时间。这种情况下,可以考虑使用ParallelGC(JDK 8默认)或G1。它们在保证较高吞吐量的同时,也能提供相对可接受的停顿。
    • 延迟优先型(Latency-sensitive):如果你在做Web服务、API网关、实时交易系统、游戏服务器,或者任何对用户体验响应时间有严格要求的应用,那么GC停顿必须尽可能短。这时候,G1CMS(虽然有些过时,但特定场景仍有人用)、以及最新的ZGCShenandoah就是你的首选。
  2. 堆内存大小

    • 小到中等堆(几百MB到几个GB):对于这类堆,ParallelGC通常表现不错,因为它实现简单,吞吐量高。G1也可以胜任,并且能提供更好的停顿控制。
    • 大到超大堆(几十GB到TB级别):当堆内存达到这个量级时,传统的Full GC停顿会变得无法接受。G1是很好的通用选择,因为它能有效地管理大堆。而如果对延迟有极致要求,ZGCShenandoah则是几乎唯一的选择,它们能够将GC停顿时间稳定在个位数毫秒,即便堆很大。
  3. 对象分配和晋升速率

    • 如果你的应用频繁创建大量临时对象(高分配速率),并且这些对象很快就死亡,那么新生代的GC会很频繁。这种情况下,确保新生代有足够的大小,并且复制算法能高效工作很重要。
    • 如果大量对象存活时间长,或者晋升到老年代的速度很快,那么老年代的GC压力就会增大。这时就需要关注老年代收集器的效率和停顿。
  4. 硬件资源

    • CPU核心数:并发GC器(如CMS、G1、ZGC、Shenandoah)需要更多的CPU核心来支持GC线程与应用线程的并发执行。如果CPU资源有限,可能需要权衡。

调优策略的一些考量点:

  • JVM参数是关键:不要盲目调优,但了解常用的GC参数是必要的。
    • -Xms-Xmx:设置堆的初始和最大大小。通常建议设为相同值,避免运行时堆的动态扩展和收缩带来的额外开销。
    • -XX:+UseG1GC:启用G1收集器。
    • -XX:MaxGCPauseMillis=200:为G1设置目标停顿时间。
    • -XX:NewRatio-Xmn:调整新生代和老年代的比例或直接指定新生代大小。这对于控制新生代GC频率和对象晋升速度很有用。
    • -XX:+PrintGCDetails-XX:+PrintGCDateStamps:开启详细GC日志,这是GC调优的基石。没有日志,一切都是盲猜。
  • 监控和分析:使用JMX、VisualVM、Arthas、GCViewer等工具来监控GC行为、分析GC日志。通过观察GC频率、停顿时间、内存使用趋势,才能找到瓶颈所在。
  • 从小步快跑:不要一次性调整太多参数。每次只改动一个或一组相关参数,然后观察效果。
  • 压力测试:在生产环境部署前,务必进行充分的压力测试,模拟真实负载,观察GC行为是否符合预期。

总之,选择和调优JVM垃圾回收器是一个持续迭代的过程。它需要你理解应用本身的特性,结合GC算法的原理,通过参数调整和数据分析,最终找到一个最适合你系统的平衡点。这就像是打磨一件工具,越了解它,越能让它发挥出最大的效用。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

432

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

600

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

723

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

723

2023.08.10

页面置换算法
页面置换算法

页面置换算法是操作系统中用来决定在内存中哪些页面应该被换出以便为新的页面提供空间的算法。本专题为大家提供页面置换算法的相关文章,大家可以免费体验。

486

2023.08.14

oracle清空表数据
oracle清空表数据

当表中的数据不需要时,则应该删除该数据并释放所占用的空间。本专题为大家提供oracle清空表数据的相关文章,帮助大家解决该问题。

271

2023.08.16

Oracle中declare的使用
Oracle中declare的使用

Oracle DECLARE语句是PL/SQL编程语言中用于声明变量、常量、游标或异常的关键字。它的主要作用是在程序中定义这些对象,以便在后续的代码中使用。DECLARE语句的语法简单明了,可以根据需要声明多个对象。通过使用这些声明的对象,可以进行各种操作,如计算、查询数据库、处理异常等 。

220

2023.09.15

oracle怎么分页
oracle怎么分页

实现分页的步骤:1、使用ROWNUM进行分页查询;2、在执行查询之前进行设置分页参数;3、使用"COUNT(*)"函数来获取总行数,并使用"CEIL"函数来向上取整计算总页数;4、在外部查询中使用"WHERE"子句来筛选出特定的行号范围,以实现分页查询。想了解更多oracle怎么分页的文章,可以来阅读本专题先的文章。

245

2023.09.18

Swift iOS架构设计与MVVM模式实战
Swift iOS架构设计与MVVM模式实战

本专题聚焦 Swift 在 iOS 应用架构设计中的实践,系统讲解 MVVM 模式的核心思想、数据绑定机制、模块拆分策略以及组件化开发方法。内容涵盖网络层封装、状态管理、依赖注入与性能优化技巧。通过完整项目案例,帮助开发者构建结构清晰、可维护性强的 iOS 应用架构体系。

3

2026.03.03

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
SQL 教程
SQL 教程

共61课时 | 4.2万人学习

Java 教程
Java 教程

共578课时 | 76万人学习

oracle知识库
oracle知识库

共0课时 | 0.6万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号