GC与垃圾回收器

发布于:2025-06-28 ⋅ 阅读:(13) ⋅ 点赞:(0)

GC

1. 找到内存空间里的垃圾;
2. 回收垃圾,让程序能再次利用这部分空间。

为什么要有 GC?

1. 避免内存泄漏(Memory Leaks)

  • 问题:手动管理时,程序员可能忘记释放不再使用的内存,导致内存被无效占用。长期运行的程序会因内存耗尽而崩溃。

  • GC的作用:自动追踪对象引用关系,回收无引用的“垃圾”内存,确保无用资源被释放。

2. 消除悬垂指针(Dangling Pointers)

  • 问题:提前释放内存后,若未清空指向该内存的指针,后续访问会引发未定义行为(如数据损坏或程序崩溃)。

  • GC的作用:只有确认对象无引用时才会回收,确保回收时无指针能访问该内存,从根本上避免悬垂指针。

3. 防止错误释放(Invalid Free)

  • 问题:手动释放正在使用的内存或重复释放同一内存,会导致程序崩溃或安全漏洞(如数据篡改)。

  • GC的作用:由运行时系统统一管理内存生命周期,程序员无需手动释放,杜绝误操作。

4. 提升开发效率与安全性

  • 减少心智负担:开发者无需关注内存释放细节,专注于业务逻辑。

  • 增强稳定性:GC语言(如Java、Go)减少了内存相关错误,降低了崩溃和安全漏洞的风险。

5. 适应现代编程需求

  • 复杂系统:在大型、高并发的系统中,手动管理内存容易出错且难以调试,GC通过自动化降低复杂度。

  • 安全关键领域:如Web服务或金融系统,GC避免的内存错误能减少严重事故。

GC的效率

评价 GC 算法的性能时,我们采用以下 4 个标准。

• 吞吐量
• 最大暂停时间
• 堆使用效率
• 访问的局部性

吞吐量:GC的吞吐量是:运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)。

最大暂停时间:最大暂停时间指的是“因执行GC而暂停执行应用程序的最长时间”。

堆使用效率:程序在运行过程中,单位时间内能使用的堆内存空间的大小。举个例子,GC 复制算法中将堆二等分,每次只使用一半,交替进行,因此总是只能利用堆的一半。相对而言,GC 标记-清除算法和引用计数法就能利用整个堆。

访问局限性:

具有引用关系的对象安排在堆中较近的位置,就能提高在缓存Cache中读取到想要的数据的概率,令应用程序高速运行。

常见的垃圾回收器

1.Serial-新生代


采用复制算法,GC时发生stop-the-world,使用单个GC线程。“Serial” is a stop-the-world, copying collector which uses a single GC thread.
特点:

客户端模式下的默认新生代收集器
单线程工作(它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束)
简单而高效(与其他收集器的单线程相比)
对于内存资源受限的环境, 它是所有收集器里额外内存消耗(Memory Footprint)最小的
对于单核处理器或处理器核心数较少的环境来说
,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

2.ParNew-新生代

采用复制算法,GC时发生stop-the-world,使用多个GC线程 
特点:

Serial收集器的多线程并行版本(除了同时使用多条线程进行垃圾收集之外, 其余的行为(包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等)都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码)
JDK 7之前在服务端模式下首选的新生代收集器(一个重要原因:除了Serial收集器外, 目前只有它能与 CMS 收集器配合工作)
ParNew收集器是激活CMS后(使用-XX: +UseConcMarkSweepGC选项) 的默认新生代收集器,也可以使用-XX: +/-UseParNewGC选项来强制指定或者禁用它。
ParNew收集器在单核心处理器的环境中绝对不会有比Serial收集器更好的效果
默认开启的收集线程数与处理器核心数量相同(可以使用-XX: ParallelGCThreads参数来限制垃圾收集的线程数)

3.Parallel Scavenge-新生代

采用复制算法,GC时发生stop-the-world,使用多个GC线程。 吞吐量优先收集器,可控制最大垃圾收集停顿时间 

新生代收集器
基于标记-复制算法实现
多线程收集器
吞吐量优先收集器(Parallel Scavenge收集器其他收集器不同在于:CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间, 而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量)
自适应调节策略(通过+UseAdaptiveSizePolicy参数激活)
ParNew和Parallel Scavenge基本没什么区别,都是年轻代的垃圾回收器,就是在Parallel Scavenge的基础上做了一些简单的增强使其能够较好的配合CMS的使用,ParNew是Parallel Scavenge的一个变种
ParNew响应时间优先,Parallel Scavenge吞吐量优先

4.Serial Old-老年代垃圾回收器

采用标记整理算法,GC时发生stop-the-world,使用单个GC线程。
“Serial Old” is a stop-the-world, mark-sweep-compact collector that uses a single GC thread.
特点:

Serial收集器的老年代版本
单线程收集器
使用标记-整理算法
主要在客户端模式下使用。如果在服务端模式下,它也可能有两种用途: 一种是在JDK 5以及之前的版本中与Parallel Scavenge收集器搭配使用,另外一种就是作为CMS收集器发生失败时的后备预案,在并发收集发生Concurrent Mode Failure时使用。

5.Parallel Old-老年代 

采用标记整理算法,GC时发生stop-the-world,使用多个GC线程。
“Parallel Old” is a compacting collector that uses multiple GC threads.
特点:

Parallel Scavenge收集器的老年代版本
多线程并发收集
使用标记-整理算法
在注重吞吐量或者处理器资源较为稀缺的场合, 都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合

6.CMS 

唯一不会发生 stop the word 的回收器

CMS采用标记清理算法,是一个以低暂停时间为目标的垃圾收集器。GC时大部分时间并发执行,其中初始化标记和重新标记两个阶段仍然会发生stop-the-world,其余阶段都是并发执行。
“CMS” is a mostly concurrent, low-pause collector.
Java之CMS GC的7个阶段:https://mp.weixin.qq.com/s/vmnBlrM7pTtVuyQU-GTcPw

收集流程:
CMS整个过程的四个步骤如下,其中初始标记、 重新标记这两个步骤仍然需要“Stop The World”。
1. 初始标记-仅仅只是标记一下GC Roots能直接关联到的对象,速度很快
2. 并发标记-从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
3. 重新标记-为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些, 但也远比并发标记阶段的时间短
4. 并发清除-清理删除掉标记阶段判断的已经死亡的对象, 由于不需要移动存活对象, 所以这个阶段也是可以与用户线程同时并发的
Concurrent Mark Sweep 收集器运行示意图如下:

缺点:

CMS 收集器对处理器资源非常敏感。事实上,面向并发设计的程序都对处理器资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但却会因为占用了一部分线程而导致应用程序变慢,降低总吞吐量。
由于CMS收集器无法处理“浮动垃圾”, 有可能出现“Con-current Mode Failure”失败进而导致另一次完全“Stop The World”的Full GC的产生。
CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生,空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很多剩余空间, 但就是无法找到足够大的连续空间来分配当前对象, 而不得不提前触发一次Full GC的情况。

G1 垃圾回收器

G1将整个堆划分为多个大小相等的独立区域(Region),保留新生代和老年代的分代概念(但两者不再是物理隔离的)。
从整体来看是基于标记整理算法,从局部(两个Region之间)来看是基于复制算法。因此,可以避免产生内存空间碎片,防止发生并发模式失败。
使用多个GC线程,每次优先回收价值最大的Region。
支持可预测的停顿时间模型,从而提高收集效率,降低stop-the-world的时间。

频繁发生 GC 的原因及怎么排查?

  1. 系统并发高、执行耗时过长,或者数据量过大,导致 young gc频繁,且gc后存活对象太多,但是survivor 区存放不下(太小 或 动态年龄判断) 导致对象快速进入老年代 老年代迅速堆满。根本原因还是新生成的对象太多
  2. 发程序一次性加载过多对象到内存 (大对象),导致频繁有大对象进入老年代 造成full gc
  3. 存在内存溢出的情况,老年代驻留了大量释放不掉的对象, 只要有一点点对象进入老年代 就达到 full gc的水位了
  4. 元空间加载了太多类 ,满了 也会发生 full gc
  5. 也许, 你看到老年代内存不高 重启也没用 还在频繁发生full gc, 那么可能有人作妖,在代码里搞执行了 System.gc();

排查思路

  1. 观察年轻代 gc的情况,多久执行一次、每次gc后存活对象有多少 survivor区多大
    存活对象比较多 超过survivor区大小或触发动态年龄判断 => 调整内存分配比例 就是看是不是存活的对象太多了,回收的时候 s 区放不下,自然就进入老年代了
  2. 观察老年代的内存情况 水位情况,多久执行一次、执行耗时多少、回收掉多少内存
    如果在持续的上涨,而且full gc后回收效果不好,那么很有可能是内存溢出了 => dump 排查具体是什么玩意
  3. 如果年轻代和老年代的内存都比较低,而且频率低 那么又可能是元数据区加载太多东西了

网站公告

今日签到

点亮在社区的每一天
去签到