JVM垃圾回收详解

对JVM垃圾回收算法及垃圾回收器的宏观讲解,先了解宏观,再去纠结细节。

垃圾定位方式

引用计数法

给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。

这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。

Root Searching:根可达算法

以被称为GC Roots的对象作为起点向下搜索,这些节点所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连,则证明此对象是不可用的,需要被回收。

垃圾回收算法

Mark-Sweep(标记清除)

该算法分为“标记”和“清除”两个阶段:

  • 标记处所有不需要回收的对象;
  • 标记完成后统一回收没有被标记的对象。

缺点:

  • 效率问题
  • 碎片问题

Copying(拷贝)

该算法将内存分为大小相同的两块,每次使用其中一块。当这块内存占满后,将依旧存活的对象复制到另一块内存区域,再将这一块区域全部清理掉。

缺点:

  • 空间利用率问题
  • 复制消耗资源

Mark-Compact(标记压缩)

该算法分为“标记”和“压缩”两个阶段:

  • 标记处所有不需要回收的对象;
  • 标记完成后让存活的对象向一端移动,并清理掉端边界以外的内存区域。

缺点:

  • 效率问题
  • 移动消耗资源

堆内存逻辑分区

将堆内存区域分为三个区域,对对象进行分代管理:

  • Eden(伊甸):对象刚诞生时放入伊甸区,发生YGC时Eden中的存活对象会被复制到Survivor区中;
  • Survivor(幸存者):幸存者区分为大小相等的两块内存,当其中一个Survivor区中有存活对象,此时若发生YGC,则会将Eden与该Survivor区中所有存活对象放入另一块Survivor区中;
  • Tenured(终身):

Eden与Survivor属于年轻代,Tenured属于老年代:

  • 年轻代与老年代默认比例为1:2,该比例可以通过参数指定;
  • Eden与两个Survivor区的默认比例为8:1:1,该比例同样可以通过参数指定。

发生在年轻代的GC叫YGC,发生在老年代的GC叫OGC,年轻代与老年代全发生的GC叫FGC。

  • 由于YGC发生非常频繁,故年轻代使用效率较高的Copying算法,YGC一般能回收掉90%的对象;
  • 老年代使用Mark-Compact算法

垃圾回收器

GC的演化

随着内存大小的不断增长而演进:

  • 几兆~几十兆:Serial 单线程STW(Stop The World)垃圾回收器,分为年轻代、老年代;
  • 几十兆~上百兆甚至1G:Parallel 并行多线程垃圾回收器
  • 几十G:Concurrent GC

垃圾回收器种类

迄今为止,垃圾回收器共有十种

基于分代管理,产生了6种垃圾回收器:

垃圾回收器需要组合使用,一个负责年轻代,一个负责老年代

  1. SerialSerial Old

    • Serial :是最基本、历史最悠久的垃圾回收器,是单线程回收器;
    • Serial Old :Serial的老年代版本;

    对于这组回收器,当垃圾回收线程工作时,所有业务线程必须暂停一切工作,这种现象成为Stop The World——STW。这种现象是不可避免的,随着内存的增长,STW的时间会越来越长。

  2. ParNewCMS

    • ParNew :就是Serial的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样;
    • CMS :即Concurrent Mark Sweep,以获取最短回收停顿时间为目标的收集器,它非常符合在注重用户体验的应用上使用;CMS是HotSpot虚拟机第一款真正意义上的并发回收器,它第一次实现了让垃圾回收线程与业务线程基本上同时工作。
  3. Parallel ScavengeParallel Old(PS+PO,是JDK 1.8默认的)

    • Parallel Scavenge :也是使用Copying算法的多线程垃圾回收器,它的关注点是吞吐量;当手动优化存在困难的时候,可以使用该回收器配合自适应调节策略,将内存管理优化交给虚拟机完成;
    • Parallel Old :Parallel Scavenge的老年代版本;

物理上不分代

G1 :JDK 1.9以后默认,G1采用物理上分区(Region),逻辑上分代的概念;

所有的区域都可以动态的指定所属代

ZGC :完全部分代,纯分区模式;

Shenandoah

Epsilon

并发垃圾回收需要解决的问题

垃圾回收器通过根可达算法标记对象的过程中,各个对象的引用链随时都会发生变化,垃圾回收线程工作时标记好的对象,可能在它的时间片用完后状态发生变化,如果不解决这个问题,那么并发将毫无意义。

三色标记算法

三色标记法利用三种颜色对所有对象进行标记,标记规则如图所示:

但这种标记存在两种情况,会使得所标记的颜色与对象的实际状态不符合,这种现象会发生在垃圾回收线程暂停,业务线程运行的过程中。

情况一:

在业务线程运行过程中,B->C消失了,则垃圾回收线程回来继续工作的时候,会发现C找不到了。

此时的C成为浮动垃圾,虽然本次GC无法将其回收,但当GC再次发生时,C会由于根不可达而被标记为垃圾。

​ 由于浮动垃圾的存在,使用CMS时不建议在整个老年代空间占满后再进行GC,应当在老年代空间被占到一定比例后就进行GC,该比例可以通过参数调整。

​ 并且该比例不建议很大,因为业务线程运行时可能会产生大量的老年代对象,剩余的空间会被迅速占满。而当老年代的空间被占满后,CMS会发生STW,然后对老年代进行单线程的清理。

情况二:

在业务线程运行过程中,B->C消失了,但增加了A->C,但此时A已经被标记为黑色,垃圾线程回来后不会再从A开始标记,而通过B已经找不到C了,在垃圾回收线程的视角C是根不可达的,所以C会被垃圾回收线程视作垃圾。

这种情况是真正根源的问题,必须解决该问题,并行垃圾回收才有意义。

CMS的解决方案:Incremental Update

任何黑色对象指向白色对象时,通过写屏障将该黑色的对象标记为灰色,则当垃圾回收线程继续工作时,会重新标记产生变化的引用链。

写屏障:它主要实现让当前线程写入高速缓存中的最新数据更新写入到内存,让其他线程也可见。

但该方案有一个严重且隐蔽的问题:

假设有两个垃圾回收线程m1m2,一个业务线程t1。开始时m1将A及其的孩子1标记,则此时在m1视角中A是灰色的;

此时发生了情况二,即B->C消失了,但增加了A->C,则通过写屏障将A标记为灰色(A此时本来就是灰色);

然后m1回来继续工作,此时m1的视角下,A是灰色,但它只直到A的孩子2还没有标记,故m1将孩子2标记,此时对于m1来说,A的所有孩子已经标记完成,故会将A标记为黑色,此时出现了漏标C的情况。

为了解决这个问题,CMS在最后有一个阶段叫做remark,在remark阶段会将引用链从头扫描一次,这个阶段必须STW,虽然这个阶段的STW没有原来想象中的那么长,但在业务逻辑非常复杂的情况下,STW的时间可能非常长。

G1的解决方案:SATB(Snapshot At the Beginning)

当灰色对象指向白色对象的引用消失时,将这个引用的信息推到GC的堆栈,保证白色对象还能被GC扫描到。

作者

亦初

发布于

2022-05-18

更新于

2024-06-19

许可协议

评论

:D 一言句子获取中...

加载中,最新评论有1分钟缓存...