一. 如何自己设计一个垃圾回收器
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垃圾回收器
- G1垃圾收集器是一款低延时的垃圾收集器
- 从长远看是是会取代CMS垃圾收集器
- 在JDK1.9中已经成为默认的垃圾收集器
- 低延时比高吞吐量有更高的价值
- 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
- 当JVM启动的时候,程序运行时的内存分配到Eden region, 当所有的Eden region满了的时候,JVM开始进行 Young GC
程序在运行的时候不仅会为对象分配内存,并且程序会无时无刻不在修改对象的指针引用,这个时候存在的问题是老年代的对象可能引用年轻代的对象, 所以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过程
- STW(Evacuation Pause)
- 构建CS (Eden+Survivor)
- 扫描GC Roots
- Update RS: 排空Dirty Card Queue
- Process RS: 找到哪些对象被老年代所引用
- Object copy:将Eden区和其中一个Suvivor区的存活对象拷贝到另一个Survivor区,若某些对象的年龄达到了Suvivor区的最大年龄,则将此对象拷贝到Old Region
- 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的运行大致分为以下几个步骤
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。