GC机制--简单扼要总结

143 阅读18分钟
  1. 要回收的内存?

方法区还有堆内存,主要是堆内存。程序计数器,虚拟机栈,本地方法栈都是随线程朝生夕死的,所以这些区域不需要过多考虑

  1. 判断对象存活的方法有哪些?

  • 引用计数法

给对象添加引用计数器,当一个地方引用,则计数器加1,反之减1,当计数器的值为0,则该对象为可回收对象

  • 可达性分析

通过一系列的称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索的路径称为引用链,当一个对象到“GC Roots”没有任何引用链相连的话,也就是GC Roots到这个对象不可达时,证明此对象已经不可用,可以被回收了

  • 二次标记

在可达性分析算法中,发现对象不可达并不一定会回收,一个对象的死亡必须经历二次标记才会真正宣告死亡。发现一个对象为不可达状态时,会对对象进行一次标记,如果此时调用对象的finalize()方法,会将该对象放入专门的队列中,如果在此队列中,对象没有成功拯救自己,将会被GC进行二次标记并移除队列等待回收

  1. GC Roots有哪些?

  • 虚拟机栈中引用的对象,比如:各个线程被调用的方法中使用到的参数、局部变量等。
  • 本地方法栈内引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁synchronized持有的对象
  1. 常见垃圾回收算法有哪些?

  • 标记-清除算法

    • 从GC Roots出发,标记所有被引用的对象。没有被引用上的则会被加入到空闲链表中
    • 优点:非常基础和常见、易于理解
    • 缺点:效率不算高;进行GC 时要停止整个应用程序;清理后的空闲内存不是连续的,产生内存碎片,还需要维护一个空闲列表
  • 复制算法

    • 将内存分为大小相等的两部分,每一只用一部分,当这一部分快满了,则将这一部分中所有存活的对象移到另一部分内存中,并把这一部分的内存一次性清理。幸存区对象的复制其实就是采取的这种算法;
    • 优点:复制过去后,保证空间的连续性,不会出现碎片问题
    • 缺点:内存的使用率只有二分之一,将对象移动的过程,效率也比较低
  • 标记-整理算法

    • 跟标记清除算法那一样的思路,只不过清除的时候,会把所有存活的对象向一端移动,然后清除
    • 优点:对于标记-清除算法来说,消除了内部碎片;对于复制算法来说,增加了内存的使用率;
    • 缺点:从效率来说,都低于前两种算法。移动对象的同时,还需要调整引用地址。(有时候刷leetcode的时候是很烦不能使用额外空间的,施展空间就少了)
  • 分代收集算法

    • 根据对象存活周期的不同将内存划分为几块(新生代或老年代),然后根据每个年代的特点采用最合适的收集算法。比如在新生代中,每次都有大量对象死去,就选择复制算法;而在老年代中对象的生存率高,没有额外的空间为它进行分配担保,所以采用标记-清除算法(CMS)或标记整理算法来进行回收。
  1. 垃圾回收器分类

  • 按线程分,可以分为串行垃圾回收器和并行垃圾回收器
  • 按工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器
  • 按碎片处理方式,可以分为压缩式垃圾回收器和非压缩式垃圾回收器
  • 按工作的内存区间分,又可分为年轻代垃圾回收器和老年代垃圾回收器
  1. 7种经典的垃圾收集器

  • 串行回收器:Serial、Serial Old
  • 并行回收器:ParNew、Parallel Scavenge、Parallel old
  • 并发回收器:CMS、G1

Serial、Serial Old回收器:串行回收

Serial收集器采用复制算法、串行回收和stop-the-world机制执行内存回收,Serial Old收集器同样也采用了串行回收和Stop the World机制,只不过Serial是在年轻代、而Serial Old是在老年代采用的算法,使用的是标记-压缩算法

优点:简单又高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率;

所有用户线程达到安全点后,单个GC线程去执行垃圾回收,再恢复用户线程

ParNew 回收器:并行回收

如果说 Serial GC 是年轻代中的单线程垃圾收集器,那么 ParNew 收集器则是 Serial 收集器的多线程版本。Par 是 Parallel 的缩写,New:只能处理的是新生代

ParNew 收集器在年轻代中同样也是采用复制算法、"Stop-the-World"机制。

在单个CPU的环境下,ParNew收集器不比Serial收集器更高效。

Parallel 、Parallel Old回收器:吞吐量优先

Parallel Scavenge收集器也采用了复制算法,并行回收和“Stop the World”机制;Parallel Old采用了标记-压缩算法;

和ParNew不同的是,采用框架不同,而且Parallel注重吞吐量优先,自适应调节策略也是一个重要区别;

自适应调节策略:虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量;

CMS回收器:低延迟

CMS(Concurent-Mark-Sweep)收集器,这是虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程同时工作。

CMS的垃圾收集算法采用标记-清除算法(老年代),新生代只能选ParNew或者Serial收集器中的一个;CMS整个过程比之前的收集器要复杂,整个过程分为4个主要阶段,即初始标记阶段、并发标记阶段、重新标记阶段和并发清除阶段

  1. 初始标记:所有工作线程都会因为Stop-the-World而出现短暂的暂停,这个阶段的主要任务仅仅是标记出GCRoots能直接关联到的对象。所以这里的速度非常快
  2. 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时比较长但是不需要停顿用户线程,可以与垃圾收集器一起并发运行。
  3. 重新标记:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短。
  4. 并发清除:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

另外,由于在垃圾收集阶段用户线程没有中断,所以在 CMS 回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS 收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在 CMS 工作过程中依然有足够的空间支持应用程序运行。要是 CMS 运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure” 失败,这时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

CMS 收集器的垃圾收集算法采用的是标记清除算法,这意味着每次执行完内存回收后,由于被执行内存回收的无用对象所占用的内存空间极有可能是不连续的一些内存块,不可避免地将会产生一些内存碎片。那么 CMS 在为新对象分配内存空间时,将无法使用指针碰撞(Bump the Pointer)技术,而只能够选择空闲列表(Free List)执行内存分配。

有人会觉得既然 Mark Sweep 会造成内存碎片,那么为什么不把算法换成 Mark Compact?

答案其实很简单,因为当并发清除的时候,用 Compact 整理内存的话,原来的用户线程使用的内存还怎么用呢?要保证用户线程能继续执行,前提的它运行的资源不受影响嘛。Mark Compact 更适合“Stop the World” 这种场景下使用

优点:并发收集;低延迟

缺点:

  • 标记清除算法会产生内存碎片,在无法分配大对象的情况下,不得不提前触发FullGC
  • CMS收集器对CPU资源很敏感。占用了一部分线程,总吞吐量会降低
  • CMS无法处理浮动垃圾。就是伴随程序运行新产生的垃圾
  • 要预留一定内存,会提前触发GC。如果在此时内存满了,会退化成Serial Old垃圾回收器;我们知道在用户量大的情况下,突然使用一个单线程,长时间Stop the World 的垃圾回收器回收垃圾,是会很崩溃的

其他:JDK9中已经标记废弃,JDK14已经删除CMS垃圾回收器

G1回收器:区域化分代式

G1是一个并行回收器,它把堆内存分割为很多不相关的区域(Region)(物理上不连续)。使用不同的Region来表示Eden、幸存区0区,幸存区1区,老年代等

G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要的经验值),在后台维护一个优先列表,每次根据允许的收集时间(默认200ms),优先回收价值最大的Region。由于这种方式的侧重点在于回收垃圾最大量的区间,所以我们给G1一个名字:垃圾优先(Garbage First)。

G1内存的回收是以region作为基本单位的。Region之间是复制算法,但整体上实际可看作标记-压缩算法,两种算法都可以避免内存碎片。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。尤其是当堆非常大的时候,G1的优势很明显

  • 可预测的停顿时间模型(即:软实时)

这也是G1相比于CMS的另一个大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,消耗在垃圾收集上的时间不得超过N毫秒。每次根据允许的收集时间,优先回收价值最大的Region;

  • G1垃圾收集器的缺点

G1无论是为了垃圾收集产生的内存占用还是程序执行时的额外执行负载都要比CMS要高;

  • 相关参数

-XX:MaxGCPauseMillis 设置期望达到的最大GC停顿时间。默认是200ms

-XX:G1HeapRegionSize 设置每个Region的大小。java堆大小划分出约2048个区域。默认值是堆内存的1/2000。

G1垃圾收集器增加了一种新的内存区域,叫做Humongous内存区域。主要用于存储大对象,如果超过1.5个region,就放到H。设置H的原因:对于堆中的对象,默认直接会被分配到老年代,但是如果它是一个短期存在的大对象就会对垃圾收集器造成负面影响。(因为短期存在的话,只有fullgc的时候才会清理老年代)

每个Region都是通过指针碰撞来分配空间

假设Java堆中内存是绝对规整的,所有被使用过的内存都被放在一 边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那 个指针向空闲空间方向挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)。

  • G1垃圾回收器的回收过程

G1的垃圾回收过程主要包括下面三个环节:

顺时针,Young gc ->Young gc + Concurrent mark ->Mixed GC顺序,进行垃圾回收

应用程序分配内存,当年轻代的Eden区用尽时开启年轻代回收过程;回收期间,G1会暂停所有应用程序线程,启用多线程执行年轻代回收。然后从年轻代区间移动存活对象到Survivor区间或者老年区间,也有可能是两个区间都会涉及。

当堆内存使用达到一定值(默认 45%)时,开始老年代并发标记过程。(此时会触发一次Young gc同时从GC Roots触发标记存活对象)

标记完成马上开始混合回收过程。和年轻代不同,老年代的 G1 回收器和其他 GC 不同,G1 的老年代回收器不需要整个老年代被回收,一次只需要扫描/回收一小部分老年代的 Region 就可以了。同时,这个老年代 Region 是和年轻代一起被回收的。

  1. Remembered Set

新生代被老年代引用的时候,老年代会分为很多个卡,每个卡512k,所有的卡组成一张卡表CardTable,如果老年代有对象引用指新生代的对象,则会将卡标记为脏卡(但不是马上改),同样的新生代中有Remember Set 简称RSet,记录下有哪些老年代的对象引用我新生代的对象,这样就会在对eden清理的时候,再GC根节点的枚举范围加入Remembered Set,就不用去遍历整个老年代就知道有哪些老年代对象引用了我,典型的空间换时间。然后刚刚说的,如果老年代的对象要去引用新生代的对象,此时卡不会立马会标记成脏卡,而是有写屏障加到脏卡队列中,异步的操作就改变卡为脏卡

  1. G1回收过程

  • 年轻代 GC

JVM启动时,G1先准备好Eden区,当Eden空间耗尽时。G1会启动一次年轻代垃圾回收过程 。

  1. 第一阶段,扫描根。根引用连同RSet记录的外部引用作为扫描存活对象的入口
  2. 第二阶段,更新Rest 。处理dirty card queue中的card,更新RSet。此阶段完成后,Rest可以准确的反映老年代对所在的内存分段中对象的引用。
  3. 第三阶段,处理RSet。识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活对象。
  4. 第四阶段,复制对象。此阶段,对象树被遍历,Eden 区内存段中存活的对象会被复制到 Survivor 区中空的内存分段,Survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加 1,达到阈值会被会被复制到 Old 区中空的内存分段。如果 Survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间。
  5. 第五阶段,处理引用。处理Soft,Weak等引用。最终Eden空间的数据为空,GC停止工作;
  • 并发 标记过程
  1. 初始标记阶段:标记从根节点直接可达的对象。这个阶段是STW的,并且会触发一次年轻代GC。
  2. 根区域扫描:G1扫描Survivor区直接可达的老年代区域对象,并标记被引用的对象。这一过程必须在YoungGC之前完成。
  3. 并发标记:在整个堆中进行并发标记(和应用程序并发执行),若发现区域对象中的所有对象都是垃圾,那这个区域会被立即回收。同时,并发标记过程,会计算每个区域的对象活性(区域中存活对象的比例)
  4. 再次标记:由于应用程序持续进行,需要修正上一次的标记结果。是STW的。G1 中采用了比 CMS 更快的初始快照算法:snapshot-at-the-beginning(SATB)。
  5. 独占清理(cleanup,STW):计算各个区域的存活对象和 GC 回收比例,并进行排序,识别可以混合回收的区域。为下阶段做铺垫。是 STW 的。这个阶段并不会实际上去做垃圾的收集
  6. 并发清理阶段:识别并清理完全空闲的区域。
  • 混合回收

当越来越多的对象晋升到老年代 old region 时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即 Mixed GC,该算法并不是一个 Old GC,除了回收整个 Young Region,还会回收一部分的 Old Region。这里需要注意:是一部分老年代,而不是全部老年代。可以选择哪些 Old Region 进行收集,从而可以对垃圾回收的耗时时间进行控制。也要注意的是 Mixed GC 并不是 Full GC。

成功完成 并发 标记周期后, g1 gc 从执行年轻代回收切换到执行混合回收。在混合回收中, g1 gc 可选择将一些老年代区域添加到将回收的 Eden 和幸存者区域集。添加的老年区域的确切数量由几个flag 控制 。g1 gc 回收足够数量的老年代区域 (通过多次混合回收) 后, g1 将恢复到执行年轻代回收, 直到下一个并发标记周期完成。

并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分 8 次(可以通过-XX:G1MixedGCCountTarget设置)被回收

由于老年代中的内存分段默认分 8 次回收,G1 会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默认为 65%,意思是垃圾占内存分段比例要达到 65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。

混合回收并不一定要进行 8 次。有一个阈值-XX:G1HeapWastePercent,默认值为 10%,意思是允许整个堆内存中有 10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于 10%,则不再进行混合回收。因为 GC 会花费很多的时间但是回收到的内存却很少。

  • 可选:Full GC
  1. 7种经典垃圾回收器总结

垃圾收集器分类作用位置使用算法特点适用场景
Serial串行运行作用于新生代复制算法响应速度优先适用于单 CPU 环境下的 client 模式
ParNew并行运行作用于新生代复制算法响应速度优先多 CPU 环境 Server 模式下与 CMS 配合使用
Parallel并行运行作用于新生代复制算法吞吐量优先适用于后台运算而不需要太多交互的场景
Serial Old串行运行作用于老年代标记-压缩算法响应速度优先适用于单 CPU 环境下的 Client 模式
Parallel Old并行运行作用于老年代标记-压缩算法吞吐量优先适用于后台运算而不需要太多交互的场景
CMS并发运行作用于老年代标记-清除算法响应速度优先适用于互联网或 B/S 业务
G1并发、并行运行作用于新生代、老年代标记-压缩算法、复制算法响应速度优先面向服务端应用

GC 发展阶段:Serial => Parallel(并行)=> CMS(并发)=> G1 => ZGC