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种垃圾回收器:
垃圾回收器需要组合使用,一个负责年轻代,一个负责老年代
Serial与Serial Old
- Serial :是最基本、历史最悠久的垃圾回收器,是单线程回收器;
- Serial Old :Serial的老年代版本;
对于这组回收器,当垃圾回收线程工作时,所有业务线程必须暂停一切工作,这种现象成为Stop The World——STW。这种现象是不可避免的,随着内存的增长,STW的时间会越来越长。
ParNew与CMS
- ParNew :就是Serial的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样;
- CMS :即Concurrent Mark Sweep,以获取最短回收停顿时间为目标的收集器,它非常符合在注重用户体验的应用上使用;CMS是HotSpot虚拟机第一款真正意义上的并发回收器,它第一次实现了让垃圾回收线程与业务线程基本上同时工作。
Parallel Scavenge与Parallel 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
任何黑色对象指向白色对象时,通过写屏障将该黑色的对象标记为灰色,则当垃圾回收线程继续工作时,会重新标记产生变化的引用链。
写屏障:它主要实现让当前线程写入高速缓存中的最新数据更新写入到内存,让其他线程也可见。
但该方案有一个严重且隐蔽的问题:
假设有两个垃圾回收线程m1
、m2
,一个业务线程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扫描到。