垃圾回收就是对内存堆中已经死亡的或者长时间没有使用的对象进行清除或回收。JVM 在做 GC 之前,会先搞清楚什么是垃圾,什么不是垃圾,通常会通过可达性分析算法来判断对象是否存活。
在确定了哪些垃圾可以被回收后,垃圾收集器(如 CMS、G1、ZGC)要做的事情就是进行垃圾回收,可以采用标记清除算法、复制算法、标记整理算法、分代收集算法等。
Java 的垃圾回收过程主要分为标记存活对象、清除无用对象、以及内存压缩/整理三个阶段。不同的垃圾回收器在执行这些步骤时会采用不同的策略和算法。
一、判断对象是否存活
1、引用计数(java gc并未使用)
每个对象中含有一个引用计数器,每当有引用指向该对象时,引用计数加 1。当引用离开作用域或被置为 null 时,引用计数减 1。因此,管理引用计数是一个开销不大但是在程序的整个生命周期频繁发生的负担。垃圾回收器会遍历含有全部对象的列表,当发现某个对象的引用计数为 0 时,就释放其占用的空间(但是,引用计数模式经常会在计数为 0 时立即释放对象)。
缺点:
如果对象之间存在循环引用,那么它们的引用计数都不为 0,就会出现应该被回收但无法被回收的情况。对垃圾回收器而言,定位这样的循环引用所需的工作量极大。引用计数常用来说明垃圾回收的工作方式,但似乎从未被应用于任何一种 Java 虚拟机实现中。
2、可达性分析:
通过一组名为 “GC Roots” 的根对象,进行递归扫描,无法从根对象到达的对象就是“垃圾”,可以被回收。
这也是 G1、CMS 等主流垃圾收集器使用的主要算法。
GC Roots对象有:
虚拟机栈中的引用(方法的参数、局部变量等)
本地方法栈中 JNI(native方法) 的引用
类静态变量
运行时常量池中的常量(String 或 Class 类型)
二、垃圾收集算法
垃圾收集算法主要有三种,分别是标记-清除算法、标记-复制算法和标记-整理算法。
1、标记-清除
该算法分为“标记”和“清除”阶段:⾸先标记出所有不需要回收的对象,在标记完成后统⼀回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不⾜进⾏改进得到。这种垃圾收集算法会带来两个明显的问题:
优点是实现简单,缺点是回收过程中会产生内存碎片
2、标记-复制
可以解决标记-清除算法的内存碎片问题,因为它将内存空间划分为两块,每次只使用其中一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后清理掉这一块。
缺点是浪费了一半的内存空间
3、标记-整理
是标记-清除复制算法的升级版,它不再划分内存空间,而是将存活的对象向内存的一端移动,然后清理边界以外的内存。
缺点是移动对象的成本比较高。
三、分代收集算法
分代收集
算法是目前主流的垃圾收集算法,它根据对象存活周期的不同将内存划分为几块,一般分为新生代和老年代。
新生代用复制算法,因为大部分对象生命周期短。老年代用标记-整理算法,因为对象存活率较高。
为什么要用分代收集呢?
分代收集算法的核心思想是根据对象的生命周期优化垃圾回收。
新生代的对象生命周期短,使用复制算法可以快速回收。老年代的对象生命周期长,使用标记-整理算法可以减少移动对象的成本。
标记复制的标记过程和复制过程会不会停顿?
在标记-复制算法 中,标记阶段和复制阶段都会触发STW。
标记阶段停顿是为了保证对象的引用关系不被修改。
复制阶段停顿是防止对象在复制过程中被修改。
四、Minor GC、Major GC、Mixed GC、Full GC
Minor GC
也称为 Young GC,是指发生在年轻代的垃圾收集。年轻代包含 Eden 区以及两个 Survivor 区。
如果 Eden 区没有足够的空间时,就会触发 Young GC 来清理新生代。
Major GC
也称为 Old GC,主要指的是发生在老年代的垃圾收集。是 CMS 的特有行为。
Mixed GC
是 G1 垃圾收集器特有的一种 GC 类型,它在一次 GC 中同时清理年轻代和部分老年代。
Full GC
是最彻底的垃圾收集,涉及整个 Java 堆和方法区。它是最耗时的 GC,通常在 JVM 压力很大时发生。
Full GC 会从 GC Root 出发,标记所有可达对象。新生代使用复制算法,清空 Eden 区。老年代使用标记-整理算法,回收对象并消除碎片。停顿时间较长,会影响系统响应性能。
触发时机:
在进行 Young GC 的时候,如果发现老年代可用的连续内存空间
< 新生代历次 Young GC 后升入老年代的对象总和的平均大小
,说明本次 Young GC 后升入老年代的对象大小,可能超过了老年代当前可用的内存空间,就会触发 Full GC。
执行 Young GC 后老年代没有足够的内存空间存放转入的对象,会立即触发一次 Full GC。
System.gc()
、jmap -dump
等命令会触发 full gc。
五、垃圾收集器
JVM 的垃圾收集器主要分为两大类:分代收集器和分区收集器,分代收集器的代表是 CMS,分区收集器的代表是 G1 和 ZGC。
CMS 是第一个关注 GC 停顿时间的垃圾收集器,JDK 1.5 时引入,JDK9 被标记弃用,JDK14 被移除。
G1 在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为了默认的垃圾收集器。
ZGC 是 JDK11 推出的一款低延迟垃圾收集器,适用于大内存低延迟服务的内存管理和回收,在 128G 的大堆下,最大停顿时间才 1.68 ms,性能远胜于 G1 和 CMS。
1、Serial收集器
Serial(串⾏)收集器是最基本、历史最悠久的垃圾收集器了。⼤家看名字就知道这个收集器是⼀个单线程收集器了。它的 “单线程” 的意义不仅仅意味着它只会使⽤⼀条垃圾收集线程去完成垃圾收集⼯作,更重要的是它在进⾏垃圾收集⼯作的时候必须暂停其他所有的⼯作线程( "StopThe World" ),直到它收集结束。新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
优点:简单⽽⾼效(与其他收集器的单线程相⽐)。Serial 收集器由于没有线程交互的开销,⾃然可以获得很⾼的单线程收集效率。Serial 收集器对于运⾏在 Client 模式下的虚拟机来说是个不错的选择。
2、ParNew收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使⽤多线程进⾏垃圾收集外,其余⾏为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全⼀样。新⽣代采⽤标记-复制算法,⽼年代采⽤标记-整理算法。
它是许多运⾏在 Server 模式下的虚拟机的⾸要选择,除了 Serial 收集器外,只有它能与 CMS 收集器配合⼯作。
3、Parallel Scavenge收集器
Parallel Scavenge 收集器关注点是吞吐量(⾼效率的利⽤ CPU)。CMS 等垃圾收集器的关注点更多的是⽤户线程的停顿时间(提⾼⽤户体验)。所谓吞吐量就是 CPU 中⽤于运⾏⽤户代码的时间与 CPU 总消耗时间的⽐值。 Parallel Scavenge 收集器提供了很多参数供⽤户找到最合适-XX:+UseParallelGC 使⽤ Parallel 收集器+ ⽼年代串⾏-XX:+UseParallelOldGC 使⽤ Parallel 收集器+ ⽼年代并⾏的停顿时间或最⼤吞吐量,如果对于收集器运作不太了解,⼿⼯优化存在困难的时候,使⽤Parallel Scavenge 收集器配合⾃适应调节策略,把内存管理优化交给虚拟机去完成也是⼀个不错的选择。新⽣代采⽤复制算法,⽼年代采⽤标记-整理算法。
4、CMS收集器
CMS(Concurrent Mark Sweep)收集器是 HotSpot 虚拟机第⼀款真正意义上的并发收集器,它第⼀次实现了让垃圾收集线程与⽤户线程(基本上)同时⼯作。
CMS 在 JDK 1.5 时引入,JDK 9 时被标记弃用,JDK 14 时被移除。
CMS 是一种低延迟的垃圾收集器,采用标记-清除算法,分为初始标记、并发标记、重新标记和并发清除四个阶段,优点是垃圾回收线程和应用线程同时运行,停顿时间短,适合延迟敏感的应用,但容易产生内存碎片,可能触发 Full GC。
整个过程分为四个步骤:
初始标记:标记所有从 GC Roots 直接可达的对象,这个阶段需要 STW,但速度很快。
并发标记:从初始标记的对象出发,遍历所有对象,标记所有可达的对象。这个阶段是并发进行的。
重新标记:完成剩余的标记工作,包括处理并发阶段遗留下来的少量变动,这个阶段通常需要短暂的 STW 停顿。主要使用三色标记法
并发清除:清除未被标记的对象,回收它们占用的内存空间。
主要优点:并发收集、低停顿。
明显的缺点:对 CPU 资源敏感;⽆法处理浮动垃圾;它使⽤的回收算法-“标记-清除”算法会导致收集结束时会有⼤量空间碎⽚产⽣。
三色标记法用于标记对象的存活状态,它将对象分为三类:
白色(White):尚未访问的对象。垃圾回收结束后,仍然为白色的对象会被认为是不可达的对象,可以回收。
灰色(Gray):已经访问到但未标记完其引用的对象。灰色对象是需要进一步处理的。
黑色(Black):已经访问到并且其所有引用对象都已经标记过。黑色对象是完全处理过的,不需要再处理。
三色标记法的工作流程:
①、初始标记(Initial Marking):从 GC Roots 开始,标记所有直接可达的对象为灰色。
②、并发标记(Concurrent Marking):在此阶段,标记所有灰色对象引用的对象为灰色,然后将灰色对象自身标记为黑色。这个过程是并发的,和应用线程同时进行。
此阶段的一个问题是,应用线程可能在并发标记期间修改对象的引用关系,导致一些对象的标记状态不准确。
③、重新标记(Remarking):重新标记阶段的目标是处理并发标记阶段遗漏的引用变化。为了确保所有存活对象都被正确标记,remark 需要在 STW 暂停期间执行。
④、使用写屏障(Write Barrier)来捕捉并发标记阶段应用线程对对象引用的更新。通过遍历这些更新的引用来修正标记状态,确保遗漏的对象不会被错误地回收。
5、G1收集器
G1 (Garbage-First) 是⼀款⾯向服务器的垃圾收集器,主要针对配备多颗处理器及⼤容量内存的机器. 以极⾼概率满⾜ GC 停顿时间要求的同时,还具备⾼吞吐量性能特征.
G1 在 JDK 1.7 时引入,在 JDK 9 时取代 CMS 成为默认的垃圾收集器。
G1 是一种面向大内存、高吞吐场景的垃圾收集器,它将堆划分为多个小的 Region,通过标记-整理算法,避免了内存碎片问题。优点是停顿时间可控,适合大堆场景,但调优较复杂。
G1 收集器的运行过程大致可划分为这几个步骤:
①、并发标记,G1 通过并发标记的方式找出堆中的垃圾对象。并发标记阶段与应用线程同时执行,不会导致应用线程暂停。
②、混合收集,在并发标记完成后,G1 会计算出哪些区域的回收价值最高(也就是包含最多垃圾的区域),然后优先回收这些区域。这种回收方式包括了部分新生代区域和老年代区域。
选择回收成本低而收益高的区域进行回收,可以提高回收效率和减少停顿时间。
③、可预测的停顿,G1 在垃圾回收期间仍然需要「Stop the World」。不过,G1 在停顿时间上添加了预测机制,用户可以 JVM 启动时指定期望停顿时间,G1 会尽可能地在这个时间内完成垃圾回收。
特点:
1、并⾏与并发:G1 能充分利⽤ CPU、多核环境下的硬件优势,使⽤多个 CPU(CPU 或者CPU 核⼼)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执⾏的 GC 动作,G1 收集器仍然可以通过并发的⽅式让 java 程序继续执⾏。
2、分代收集:虽然 G1 可以不需要其他收集器配合就能独⽴管理整个 GC 堆,但是还是保留了分代的概念。
3、空间整合:与 CMS 的“标记--清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
4、可预测的停顿:这是 G1 相对于 CMS 的另⼀个⼤优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型,能让使⽤者明确指定在⼀个⻓度为 M 毫秒的时间⽚段内。
CMS与G1区别
CMS 适用于对延迟敏感的应用场景,主要目标是减少停顿时间,但容易产生内存碎片。
G1 则提供了更好的停顿时间预测和内存压缩能力,适用于大内存和多核处理器环境。
6、ZGC
ZGC 是 JDK 11 时引入的一款低延迟的垃圾收集器,最大特点是将垃圾收集的停顿时间控制在 10ms 以内,即使在 TB 级别的堆内存下也能保持较低的停顿时间。
它通过并发标记和重定位来避免大部分 Stop-The-World 停顿,主要依赖指针染色来管理对象状态。
适用于需要超低延迟的场景,比如金融交易系统、电商平台。
六、如何选择合适的垃圾收集器
如果应用程序只需要一个很小的内存空间(大约 100 MB),或者对停顿时间没有特殊的要求,可以选择 Serial 收集器。
如果优先考虑应用程序的峰值性能,并且没有时间要求,或者可以接受 1 秒或更长的停顿时间,可以选择 Parallel 收集器。
如果响应时间比吞吐量优先级高,或者垃圾收集暂停必须保持在大约 1 秒以内,可以选择 CMS/ G1 收集器。
如果响应时间是高优先级的,或者堆空间比较大,可以选择 ZGC 收集器。
评论区