之前写的GC文章太少而且逻辑也不够清晰,这次阅读了《Java性能优化权威指南》以及京东等大厂的技术文章也算对GC有了更清楚的认知,回顾之前写的内容,也觉察自己实在是不用功,文笔也很差,不过我想这也是写博客的好处之一吧,记录下当时虽然没有多少技术但依旧快乐地写下自己的见解。

参考了很多资料,写这篇博客花了我两天时间,即便这样有些难题,如对象消失对象误标跨代引用等,还有ZGC这种新的GC工具,还有GC的触发时间等等依旧没有讨论,一部分原因是篇幅太长了,但大部分原因还是我太弱了,并没有消化全懂了,只能后续再补充了。

希望这篇文章对看到的同学有所帮助。(不会浪费你们的时间就好hhhh)

GC全称为Garbage Collection的缩写,顾名思义就是垃圾回收机制,在介绍GC的算法和GC垃圾处理器之前,我们首先要知道GC是如何工作的,也就是GC是怎么判断我们的对象是垃圾的。

一、GC工作流程

GC的工作可以简单分为两步:

  1. 识别出哪些对象已经死了
  2. 清除掉已死对象

如何判断对象是否没用了,JAVA中有两种思路

1.1 引用计数法

每个对象创建后都包含一个引用计数器,当有地方引用它时,对象的引用计数器就加一,当引用结束后,引用计数器就减1,只要当引用计数器为0时,对象就会被GC判断为垃圾。

但这种方法无法解决循环引用问题,简单说就是a=b,b=a这种死锁问题,正是这个原因,目前并没有任何一家的JVM使用这种方法

1.2 可达性分析

这个算法与DFS类似,它的思路就是通过一系列的称为”GC Roots“的对象为起点,往下搜索,将走过的对象标记为活的,没有被走过的对象是垃圾的可能性就比较大。

Java虚拟机—堆内存分代和GC垃圾收集算法 - 知乎

这里所谓的GC roots,其实也不是很故弄玄虚的东西,就是所有Java线程当前活跃frame里指向GC堆里的对象引用,换句话说就是被调用的方法里引用类型的参数,局部变量等,更详细的可以参考下面的”参考文献“。

二、GC算法

首先说明下,GC算法不同于GC垃圾处理器,GC垃圾处理器可以使用不同的GC算法,可能小白会混淆(比如年轻时候的我)

2.1 标记清除算法

先标记再清除

这个算法差不多就是GC Roots的本意,但它有两个问题:

  1. 效率不高,标记和清除两个过程都要从GC Root
  2. 空间碎片,对象可能在内存空间任何地方被回收,造成很多碎片

2.2 复制算法

内存分成了两个部分,To和From,To区域用完将存活对象拷贝到From区域,To区域清理干净。这样两个区域互相配合,这样做就解决了碎片问题了。

但这个复制算法也有个问题,那就是内存利用率不高,To/From区域在某一时刻总有一块是干净的没有被利用起来。

这个算法使用在年轻代中(Hotspot JVM),将年轻代分为了Eden,Survivor From,Survivor To区域,比例为8:1:1

image-20230318172240134

2.3 标记整理算法

HotSpot老年代使用,在标记清除算法的基础上做了一些小调整,找到垃圾之后并不直接清除而是将后续的对象(存活的对象)向一端移动,并清除之外的内容垃圾。

image-20230318175646110

2.4 分代收集算法

分代收集算法是基于两个观察:

  1. 大多数分配对象的存活时间很短
  2. 存活时间久的对象很少引用存活时间短的对象

总得就是一句话就是考虑到对象的存活时间长度不一样,综合了前面几种算法的优缺点即:年轻代使用复制算法,老年代使用标记清除或者标记整理算法。

三、垃圾处理器

3.1 概述

image-20230318193423236

截至最新的jdk19目前有9种垃圾处理器,而很多八股文或者博客重点讲的CMS其实并没有作为过默认处理器,并且在jdk14的时候移除出去了,但是CMS的思想也被ZGC吸收了,我们先介绍下其他常见的垃圾处理器。

image-20230318194925479

3.2 Serial

该收集器的特点就是单线程,同时工作的时候要暂停其他用户线程,是JVM运行在Client端默认的新生代收集器。

3.3 ParNew

该收集器的特点就是多线程,不过工作时也要暂停其他的用户线程,是JVM运行在Server端的默认新生代收集器,除此之外,观察概述部分的第二张图可知,它是目前唯一一个和CMS配合的新生代收集器。

3.4 Parallel Scanvenge

该收集器和ParNew很像(整个过程与ParNew是一样的),但是这个收集器更注重吞吐量这个指标(吞吐量的计算公式为 $$Throughput= \frac{Time_{用户线程}}{Time_{用户线程}+Time_{GC线程}}$$)。

3.5 Serial Old

该收集器使用在老年代上,是Serial收集器的老年代版本,它的特点也是单线程,工作时要暂停其他线程,是JVM client端默认的老年代收集器。

如果用在JVM Server端,那么它的用途有两种

  1. 配合Parallel Scanvenge
  2. 作为CMS的”帮手“,帮助CMS发生Concurrent Mode Failure时使用。

3.6 Parallel Old

该收集器是Parallel Scanvenge的老年代,同样,这个收集器关注的是吞吐量,它只有和Parallel Scanvenge配合

3.7 CMS

虽然CMS早在java9中被废弃,在JAVA14中被移除了,原因也很简单,那就是CMS太复杂了,代码量太多难以维护,但是学习CMS思想还是很重要的。

image-20230318225155484

可以很明显看出,CMS有四个阶段:

  1. 初始标记阶段:这个阶段仅仅标记GC ROOT中的对象,速度很快

  2. 并发标记阶段:这个阶段的特点就是可以与用户线程同时一起工作,这个阶段就是GC ROOT的追踪工作

  3. 重新标记阶段:这个阶段是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录(非垃圾变成垃圾,垃圾变成非垃圾),但是因为有了前面并发阶段的工作,这个阶段“停顿感”会少很多。

  4. 并发清除阶段:与前面不同的是这里使用的是标记-清除算法,而不是像之前使用的是标记-整理算法,如果读者对其好奇而自己去百度的话,往往都指向了CMS的论文,但是我这里找不到下载该篇论文的渠道,因此,我大胆地思考了一下给出我的理解(不定真):CMS的设计研发的初衷就是为了让用户尽量小地被GC导致的“停顿感”的影响,而标记整理肯定是比标记清除要做的工作更多,这违背了初衷了,因此CMS使用的是标记清除算法,同时也减少一下CMS的代码量。

    以上都是我自己的猜想,具体地请各位同学去看论文,如果可以的话,能给我发一份更是感激不尽。

​ 它的优点就是允许GC线程和用户线程并发工作,但是这种模式也存在几个问题:

  1. 因为允许GC线程和用户线程同时工作,那么显然可以猜到,CPU资源非常敏感
  2. 最后并发清理阶段并没有暂停用户线程,如果在这个阶段产生垃圾,这次CMS就无法处理,我们称这种垃圾叫做浮动垃圾
  3. 标记-清除算法可能导致很多内存碎片
  4. 大量浮动垃圾以及内存碎片很容易导致Concurrent Model Failure,这个现象发生的原因是老年代分配对象时没有空间了,所以会触发一次时常感人的Full GC(可以参考概述的第二张图,这次FullGC由Serial Old来执行)

3.8 G1收集器

G1是面向服务端的垃圾回收器,是目前JDK中默认的垃圾回收器,我个人觉得研发G1垃圾收集器的团队真是非常聪敏,与上面几个垃圾收集器相比,可以说是打破脑洞的变化,尤其是在计算机领域讲究迭代开发,而G1却打破常规。

虽然上面垃圾回收器有各自的特点,但是却有几个共同点:

  1. 新生代、老年代是独立且连续的内存块,相互间界限清楚。
  2. 新生代都是使用复制算法
  3. 老年代必须扫描整个老年代区域
  4. 都是尽可能少地进行GC,在老年代时串行/并行是内存不足时触发GC,CMS是快耗尽时触发GC

而G1则不同,顾名思义,Garbage First “ 垃圾优先” 蕴含着”尽可能多的收集垃圾“,那么G1 GC的时刻就并不像之前的收集器那样了(对标第四点),它内部采用启发式算法,老年代出现具有高收益的分区时就可以GC了(对标第三点),第二点就是G1采用了内存分区的思路,将内存划分为一个个相等大小的内存分区,回收时则以分区为单位进行回收,存活的对象复制到合适的另一分区中,这样不在内存中做严格的新生代和老年代区别(物理上),不需要完全独立的survivor(From、to)区做复制(对应第二点),但逻辑上还是有划分的(见下面的图) (对应第一点)。

除此之外,G1的收集都要STW(就是停顿),但是由于年轻代和老年代界限模糊,采用了Mixed混合方式收集,也就是两条:1.可以只收集年轻代 2.可以同时收集年轻代和部分老年代 这样即使堆空间很大,也可以缩小范围,从而降低停顿。

JVM之G1回收器和常见参数配置_Qqun954715313的博客-CSDN博客

可以看出G1将内存划分了不同的区域,每个区域有自己的划分年代,有四个年代,分别是Eden,Survivor,Old,Humongous,Humongous存放的是巨大的对象,这个区域存放的对象要么被回收要么就一直存在,不会被移动。

G1的内存划分形式,决定了G1同时需要管理新生代和老年代。根据回收区域的不同,G1分为两种回收模式:

  1. young GC:只回收新生代,有的文章叫做Minor GC,下面都叫做Young GC
  2. Mixed GC:回收新生代和部分老年代

3.8.1 Young GC

Young GC与之前讲的其他垃圾收集器一样,也是先标记,再整理,只不过Young GC全程都会STW,同时, Young GCMixed GCConcurrent Mark过程的第一阶段initial Mark一样,因此,可以认为mixed GC总是搭载着Young GC。

Young GC主要是对Eden区进行GC,他在Eden区被耗尽的时候被触发

  1. 首先扫描所有的Eden区进行标记
  2. 将Eden区存活的对象复制到Survivor区,如果Survivor区不够,则Eden对象晋升到Old区,清空Eden区,完成一次Young GC。

3.8.2 Mixed GC

Mixed GC负责是的清除全部的新生代和部分老年代,它分成了四个过程:

  1. 初始标记:标记从GC Root可以直达的对象,这个阶段会STW
  2. 并发标记:从GC Root开始对Heap中的对象进行标记,收集各个区域存活对象的信息,这个阶段会允许用户线程和CG线程一起工作。
  3. 最终标记/重新标记:标记并发标记中发生变动的对象
  4. 清理:清除没有存活对象的区域,加入可分配的区域中。之后使用复制算法整理区域

此外,G1收集器还有其他有意思的设定,比如内存分配和回收策略,尽可能少的触发Full GC,这部分可以参考参考文献中京东技术G1文章。

3.8 ZGC

老实说我没有看懂,学会了再回来记录。

四、其他

初学的人可能会搞不清很多术语,比如看一些博客或者经典书籍会有Minor GC,Major GC,Full GC,Young GC,Mixed GC等等,这里给出我的理解。

首先,GC可以分成两种 Partial GC和Full GC,顾名思义一个是回收部分堆内存,一个是整个堆内存都进行回收。

4.1 Partial GC

  1. Young GC: 只回收新生代的垃圾
  2. Old GC: 只回收老年代的垃圾,目前只有CMS的concurrent collection使用Old GC
  3. Mixed GC:只有G1有这个模式,收集整个young gen以及部分old gen的GC。
  4. Minor GC:只有G1有这个模式,相当于Young GC。

4.2 Full GC

收集整个堆,包括young gen、old gen、perm gen(如果存在的话)等所有部分的模式。Major GC:通常是跟Full GC是等价的。

五、 参考文献及备注

  1. [GC Roots](YourKit Java Profiler help - GC roots)

  2. JVM 垃圾收集器Serial +Serial Old+ParNew+Parallel Scavenge+Parallel Old+CMS+G1 - Java天堂 (javatt.com)

  3. 一分钟了解垃圾回收器GC中的CMS

  4. 《深入理解JVM》G1篇

  5. 京东技术G1