一. 如何自己设计一个垃圾回收器

1. 怎样找到需要回收的垃圾?

我们都知道有两种算法:即 引用计数算法可达性分析算法

  • 引用计数法:

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

存在的问题:不能解决循环引用

  • 可达性分析算法:
就是通过一系列的“GC Roots”,也就是根对象作为起始节点集合,从根节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链,如果某个对象到GC Roots间没有任何引用链相连。用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。所以此对象就是可以被回收的对象。

GCRoot 包含:

  • class 里面的static变量
  • JNI本地方法
  • 栈上的局部变量

2. 怎样清除垃圾?

3. 垃圾分代

二. 垃圾收集器的进化史

1. 古典时代的垃圾回收算法:

  • Serial

    • 年轻代 Serial
    • 老年代 Serial Old
  • Parallel

    • 年轻代 Parallel Scavenge
    • 老年代 Parallel Old

2. 中古时代的垃圾回收算法:

  • CMS: Concurrent Mark Sweep

    • 低延时的系统
    • 不进行Compact
    • 用于老年代
    • 配合Serial/ParNew使用

3. 现代垃圾回收算法:

  • G1:Garbage first

4. 未来时代的垃圾回收算法:

  • ZGC
  • Shonandoah

三. G1垃圾回收器

  1. G1垃圾收集器是一款低延时的垃圾收集器
  2. 从长远看是是会取代CMS垃圾收集器
  3. 在JDK1.9中已经成为默认的垃圾收集器
  4. 低延时比高吞吐量有更高的价值
  5. G1被设计的非常容易调优

1. G1内存分布

对以下的概念熟悉吗?

  • Eden
  • Survivor
  • Young Generation
  • Tenured
  • Old Generation

没错,这个图就是远古时代的GC,从今天开始,大家要忘记他,下图是G1垃圾回收算法的内存分布图:

Region 概念

E、S、O Region

在G1垃圾回收算法中,堆内存会被切分成为很多个固定大小区域(Region),每个是连续范围的虚拟内存。堆内存中一个区域(Region)的大小可以通过-XX:G1HeapRegionSize参数指定,大小区间最小1M、最大32M,总之是2的幂次方。默认把堆内存按照2048份均分。每个Region被标记了E、S、O和H,这些区域在逻辑上被映射为Eden,Survivor和老年代Old。

“Humongous” Region

除了这几个region之外,G1垃圾收集器还有一个特别的region,叫 “Humongous” region 这个region是用类存储大对象的,当一个对象占用的空间超过region的 50% 大小的时候,这个对象会被分配到Humongous region中去

三种GC

  • Fully young Gc
  • Old GC
  • Mixed GC

2. G1 Young GC

  1. 当JVM启动的时候,程序运行时的内存分配到Eden region, 当所有的Eden region满了的时候,JVM开始进行 Young GC
  2. 程序在运行的时候不仅会为对象分配内存,并且程序会无时无刻不在修改对象的指针引用,这个时候存在的问题是老年代的对象可能引用年轻代的对象, 所以G1垃圾收集器必须追踪这些跨代之间的对象引用

    ( Old | Humongous ) -> (Eden | Survivor) pointers

2.1 跨代引用的解决

跨代的对象引用如下如所示:

为了解决跨代对象引用,回收年轻代的空间需要扫描整个老年代的堆空间,G1引进了Card Table 和 Rememberd Set(RS)

  • Card Table:
G1会把每个Region分成若干个Card Table表,表中每个entry覆盖512Byte的内存空间,当对应的内存空间发生改变时,标记为dirty
  • RememberSet:

指向Card Table中的entry,可以找到具体的内存区域

用额外的空间维护引用信息

大约占用整个堆的5%到10%

多个Region之间remember set 的引用关系图:

由于程序存在并发问题,G1使用了 write barrier 去追踪对象引用的更新,object.field = <reference>, 每当有一个引用被赋值的时候,会标记Card为dirty, 然后将Card存入到Dirty Card Queue,dirty card queue有四种颜色:white, green, yellow, red

  • 白色:

    天下太平,无事发生

  • Green zone(-XX:G1ConcRefinementGreenZone=N)

    Refinement线程开始被激活,开始更新RS

  • Yellow zone(-XX:G1ConcRefinementYellowZone=N)

    全部Refinement线程开始激活

  • Red zone(-XX:G1ConcRefinementRedZone=N)

    应用线程也参与排空队列的工作

2.2 Fully Yong GC过程

  1. STW(Evacuation Pause)
  2. 构建CS (Eden+Survivor)
  3. 扫描GC Roots
  4. Update RS: 排空Dirty Card Queue
  5. Process RS: 找到哪些对象被老年代所引用
  6. Object copy:将Eden区和其中一个Suvivor区的存活对象拷贝到另一个Survivor区,若某些对象的年龄达到了Suvivor区的最大年龄,则将此对象拷贝到Old Region
  7. Reference Processing 清除一些虚引用和软引用以及幻引用

2.3 Fully young GC优化

  • G1记录每个阶段的时间,用于自动调优
  • 记录Eden/Survivor的数量和GC时间,根据暂停目标自动调整Region的数量,暂停目标越短,Eden数量越少

    会造成吞吐量下降

-XX:+PrintAdaptiveSizePolicy

-XX:+PrintTenuringDistribution

3. Old GC

当堆的使用量达到一定的程度的时候,会触发Old GC

-XX:initialtingHeapOccupancyPercent=N

默认是45%

3.1 Old GC的运行大致分为以下几个步骤

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收

3.2 三色标记法

并发标记要解决什么样的问题?首先我们来了解一下三色标记法:

在遍历对象图的过程中,把访问都的对象按照"是否访问过"这个条件标记成以下三种颜色:

  • 白色:表示对象尚未被垃圾回收器访问过。显然,在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
  • 黑色:表示对象已经被垃圾回收器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代表已经扫描过,它是安全存活的,如果有其它的对象引用指向了黑色对象,无须重新扫描一遍。黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
  • 灰色:表示对象已经被垃圾回收器访问过,但这个对象至少存在一个引用还没有被扫描过

三色标记过程中

三色标记结束:

可以看到,灰色对象是黑色对象与白色对象之间的中间态。当标记过程结束后,只会有黑色和白色的对象,而白色的对象就是需要被回收的对象。

在可达性分析的扫描过程中,如果只有垃圾回收线程在工作,那肯定不会有任何问题。但是垃圾回收器和用户线程同时运行呢?这个时候就有点意思了。

垃圾回收器在对象图上面标记颜色,而同时用户线程在修改引用关系,引用关系修改了,那么对象图就变化了,这样就有可能出现两种后果:

  • 一种是把原本消亡的对象错误的标记为存活,这不是好事,但是其实是可以容忍的,只不过产生了一点逃过本次回收的浮动垃圾而已,下次清理就可以。
  • 一种是把原本存活的对象错误的标记为已消亡,这就是非常严重的后果了,一个程序还需要使用的对象被回收了,那程序肯定会因此发生错误。

Lost Object Problem

这时,我们和之前分析的正常扫描结束的对象图对比,就能清楚的看到,扫描完成后,原本还在被对象A引用的对象C,由于是白色对象,所以根据三色标记原则,对象C会被当成垃圾回收。

当且仅当以下两个条件同时满足时,会产生"对象消失"的问题,原来应该是黑色的对象被误标为了白色:

怎么解决"对象消失"问题呢?

  • 条件一:赋值器插入了一条或者多条从黑色对象到白色对象的新引用。
  • 条件二:赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。

由于两个条件之间是当且仅当的关系。所以,我们要解决并发标记时对象消失的问题,只需要破坏两个条件中的任意一个就行。

于是产生了两种解决方案:

  • 增量更新(Incremental Update)
  • 原始快照(Snapshot At The Beginning,SATB)。

在HotSpot虚拟机中,CMS是基于增量更新来做并发标记的,G1则采用的是原始快照的方式,使用write barrier去记住B.c = null的操作

什么是增量更新呢?

增量更新要破坏的是第一个条件(赋值器插入了一条或者多条从黑色对象到白色对象的新引用),当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。

可以简化的理解为:黑色对象一旦插入了指向白色对象的引用之后,它就变回了灰色对象。

什么是原始快照呢?

原始快照要破坏的是第二个条件(赋值器删除了全部从灰色对象到该白色对象的直接或间接引用),当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次。

这个可以简化理解为:无论引用关系删除与否,都会按照刚刚开始扫描那一刻的对象图快照开进行搜索。

4. MixedGC

选择若干个Region进行,默认1/8/的Old Region

-XX:G1MixeedGCGountTarget= N

Eden+Survivor Region, STW, Parallel,Copying

根据暂停目标,选择垃圾最多的Old Region 优先进行

-XX:G1MinxGCLiveThresholdPercent=N(默认85%)

-XX:G1HeapWastePercent=N


beyondxie
1 声望0 粉丝