are you ready~~
知识点太多,肝一篇长文~,let us go ~~
先来看张图:
上图可以看到随着java版本的迭代,不断有新的垃圾收集器面世,新的GC Collector也日渐强大~~
JDK 1.3 发布串行Serial GC、ParNew(多线程的Serial)-> JDK 1.4 发布 并行Parallel Scavenge -> JDK 1.5 发布并发CMS -> JDK 1.6 发布Parallel Old ->JDK 1.7 发布G1 -> JDK 9 默认G1,标记CMS过时 - >JDK 10 增强G1->JDK 11 引入Epsilon、ZGC->JDK 12 发布 增强G1,Shenandoah GC ->JDK 13 发布 增强ZGC -> JDK 14 删除CMS,拓展ZCC
先来简单说下jdk已经提供的几种垃圾收集、如下图:
Young Generation - 年轻代:
- Serial-串行 :
- 单线程 - 只有一个GC线程在做GC工作
- 串行 - STW(stop the world)-暂停所有用户线程
- 复制算法
- ParNew - 并行:
- Serial 的多线程版本 - 多个GC线程并行做GC工作,缩短了GC时间
- 其它和Serial一毛一样
- Parallel Scavenge -并行
- 多线程
- STW
- 复制算法
- 目标是提升吞吐量(吞吐量 = t用户/(t用户 + tGC))
- G1 - 并行 +并发
Tenured Generation - 老年代:
- CMS-Concurrent Mark Sweep
- 并发收集 - 用户线程与GC线程交替工作 - 可能产生浮动垃圾
- 标记-清除算法 - 会产生内存碎片
- Serial Old
- Serial 的老年代版
- 标记-整理算法
- STW
- Parallel Old
- Parallel Scavange 的老年代版
- 标记-整理算法
- G1
note:
并行 - parallelism
: 只是说多个GC线程并行进行垃圾收集动作,还是要STW,暂停所有用户线程的,如下图
-
Parallel 和ParNew 都是并行GC Collector,都会STW
-
Parallel 和ParNew 都是都是标记-整理算法,Parallel 更关注高Throughput-吞吐量,高效利用CPU,也称吞吐量优先收集器
吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)
-
Parallel Scavenge 提供了两个参数控制吞吐量:
-XX:MaxGCPauseMillis - GC最大暂停时间(毫秒)、JVM没有指定默认值
-XX:GCTimeRatio =99 - GC时间占比 , 只能取0到100的整数,默认99,即 1/(1+99) = 1%
- Parallel Scavenge 还提供了1个参数 - 很牛:
-XX:+UseAdaptiveSizePolicy - 启动GC自适应的调节策略、jdk8默认启用
-XX:-UseAdaptiveSizePolicy - 关闭GC自适应的调节策略
自适应嘛,顾名思义,JVM自己动态调整来提供最合适的停顿时间或最大的吞吐量,
启动之后,以下参数不用自己配啦,根据运行情况动态调整。
-Xmn: - 年轻代大小、一般为1/4 (-Xmx - 最大堆内存大小)
-XX:NewRatio:2(默认)- 新生代与老年代的比例,即Young = 1/3 (Heap)、Old = 2/3 (Heap)
-XX:SurvivorRatio:8(默认) - 新生代中Eden:Survivor0:Survivor1 = 8:1:1
- 既然Parallel Scavenge 这么厉害还要ParNew干啥呢?ParNew也很重要,它可以和CMS搭配使用,Parallel Scavenge不可以和CMS搭配使用,
因为HotSpot VM 有一个分布式GC框架,Serial、Serial Old、ParNew、CMS都在这个框架内,共享了很多代码,可以任意搭配,“mix-and-match”,Parallel Scavenge 和G1采用了新框架,不兼容原本分布式GC框架。
,所以ParNew+CMS
一度非常受欢迎
并发 - concurrency:
GC线程可以和用户线程交替获得CPU时间片
-
CMS是G1之前真正实现并发GC的Collector,GC的同时,用户线程也在并发进行,用户无感知的情况下就完成了GC(当然它也有STW的阶段,G!的mixed GC 同CMS类似,后面详细介绍)
宏观上来看就是这样的、如下图: -
CMS使用的是Mark-Sweep (标记-清除)算法,会产生很多内存碎片、如下图
7种垃圾收集器详解 - 知识补充请找这位老哥 - 传送门
CMS使老年代垃圾收集器前进了一大步,缺点也很明显,HotSpot VM 团队历时6年,终于在jdk 7 发布时、G1垃圾收集器强势来袭,赋予了取代CMS的使命
G1 - Garbage First 垃圾收集器
G1- Garbage First,见名思议,垃圾优先,先回收垃圾多的region
G1 特性 - 牛在哪里:
- 可预测的停顿时间模型 - soft real-time (软实时) - 短暂停时间且可控
- 高吞吐量
- 并发+并行
- 引入Region-分区思想 - GC以Region为单位、设计了基于部分内存回收的Young GC和Mixed GC
- 引入Concurrence Refinement Thread - 并发优化线程
我们都知道java的 一大NB之处是它的自动内存管理,即GC,GC虽然叫Garbage Collection - 垃圾收集,它有两个职能,第一是内存的分配管理
(JVM-Java Virtual Machine,java虚拟机,是运行在操作系统上的虚拟计算机,只能跟OS打交道,对内存的管理也是先通过OS的系统调用申请一块内存,然后通过不同的GC算法进行管理),第二是垃圾回收
,每种垃圾回收策略都和内存分配策略息息相关。
G1之前内存划分 - 分代 - 如下图 -
- 内存划分为年轻代和老年代
- 年轻代和老年代内存空间物理连续
- Permanent - 永久代 在jdk 8 被干掉了
- 年轻代收集器都采用复制算法,通过s0、s1空间复制实现
G1将内存重新划分,引入了Region(分区)的概念、先上图:
-
G1将heap划分为2048个大小相等的Region
-
每个Region逻辑连续,物理不连续
-
GC是以Region为单位进行的
-
Region有5种类型
- Eden - 伊甸园,新生代的eden区
- Survivor - 存活区,新生的s0、s1区
- Old - 老年代
- Humongous - 巨无霸对象区,简单说一下吧,就是一个对象大小>1/2 eden,那它就是巨大对象,直接放到H区,H区可能占几个连续的Region
- free - 空闲分区
-
每个Region类型不是固定的,一个Region被GC释放后就是free分区,下次被分配为Eden、Survivor、Old Region,any type is possible ~
我们逐个来看一下,大牛们是怎么想的~~
Region 类型 -
分区类型在这定义的 - heapRegionType.hpp
// 0000 0 [ 0] Free
//
// 0001 0 Young Mask
// 0001 0 [ 2] Eden
// 0001 1 [ 3] Survivor
//
// 0010 0 Humongous Mask
// 0010 0 [ 4] Humongous Starts
// 0010 1 [ 5] Humongous Continues
//
// 01000 [ 8] Old
typedef enum {
FreeTag = 0, //空闲分区
YoungMask = 2, //年轻代
EdenTag = YoungMask, //年轻代中Eden-伊甸园 区
SurvTag = YoungMask + 1,//年轻代中Survivor - 存活 区
HumMask = 4, //巨无霸对象、占一片连续Region
HumStartsTag = HumMask, //巨无霸对象起始位置
HumContTag = HumMask + 1, //巨无霸对象连续Region
OldTag = 8 //老年代
} Tag;
还提供了一些判断方法:
public:
// Queries
bool is_free() const { return get() == FreeTag; }
bool is_young() const { return (get() & YoungMask) != 0; }
bool is_eden() const { return get() == EdenTag; }
bool is_survivor() const { return get() == SurvTag; }
bool is_humongous() const { return (get() & HumMask) != 0; }
bool is_starts_humongous() const { return get() == HumStartsTag; }
bool is_continues_humongous() const { return get() == HumContTag; }
bool is_old() const { return get() == OldTag; }
Region大小上限、下限
- heapRegionBounds.hpp
HR(Heap Region)的大小影响分配和回收的效率,HR太大回收时间长,HR太小Eden频繁占满、频繁进行分配,开销大,也不合理,所以JVM设置了上限和下限,源码如下:
panda保留了关键注释,进行了蹩脚翻译~ ,毕竟咱也六级选手呢,<{=....(嘎嘎嘎~)
private:
// Minimum region size; we won't go lower than that.
//最小堆大小,我们不会再小了~
static const size_t MIN_REGION_SIZE = 1024 * 1024; //Region最小1MB
// reason for having an upper bound. We don't want regions to get too
// large, otherwise cleanup's effectiveness would decrease as there
//设置上限的原因是,我们不想把Region设置太大,因为回收效率会因此降低
static const size_t MAX_REGION_SIZE = 32 * 1024 * 1024;// Region 最大 32MB
// The automatic region size calculation will try to have around this
//自动region大小计算会调整这个值 ,就是可变、不固定
static const size_t TARGET_REGION_NUMBER = 2048; //默认2048个Region
Region大小设置 - HEAPREGION.CPP
JVM 给了Region大小范围,我们可以通过-XX:G1HeapRegionSize
参数来设置每个Region的大小,如果你没设置怎么办呢,JVM根据实际情况动态决定,关键源码如下:
void HeapRegion::setup_heap_region_size(size_t initial_heap_size, size_t max_heap_size) {
uintx region_size = G1HeapRegionSize;
if (FLAG_IS_DEFAULT(G1HeapRegionSize)) {
size_t average_heap_size = (initial_heap_size + max_heap_size) / 2;
region_size = MAX2(average_heap_size / HeapRegionBounds::target_number(),
(uintx) HeapRegionBounds::min_size());
}
int region_size_log = log2_long((jlong) region_size);
// Recalculate the region size to make sure it's a power of
// 2. This means that region_size is the largest power of 2 that's
// <= what we've calculated so far.
region_size = ((uintx)1 << region_size_log);
// Now make sure that we don't go over or under our limits.
if (region_size < HeapRegionBounds::min_size()) {
region_size = HeapRegionBounds::min_size();
} else if (region_size > HeapRegionBounds::max_size()) {
region_size = HeapRegionBounds::max_size();
}
// And recalculate the log.
region_size_log = log2_long((jlong) region_size);
}
不要慌,我们逐步来看,
类名 - HeapRegion
俩参数 :
- initial_heap_size - 初始堆大小,就是我们熟悉的Xms 设置的最小堆内存 ,我们假设给个之值2048 (2MB)
- max_heap_size - 最大堆大小,是Xmx 设置的值,我们假设给个之值6144(大概6MB)
方法体:
uintx region_size = G1HeapRegionSize;
将我们通过参数-XX:G1HeapRegionSize
设置的值作为HR size
if (FLAG_IS_DEFAULT(G1HeapRegionSize)) {
size_t average_heap_size = (initial_heap_size + max_heap_size) / 2;
region_size = MAX2(average_heap_size / HeapRegionBounds::target_number(),
(uintx) HeapRegionBounds::min_size());
}
这步是重点,标星,嘎嘎~~,我们再来拆解一下
if (FLAG_IS_DEFAULT(G1HeapRegionSize))
是否是默认标志,就是没设置嘛size_t average_heap_size = (initial_heap_size + max_heap_size) / 2;
//取我们传的这俩参数的平均值,就是说你不是设置了堆内存范围吗【2048,6144】(panda
给的示例值啊,不是jvm的默认值,嘎嘎~),那我就当你设置了(2048+6144)/ 2 = 4096(4MB)
这么大,既然你说随便,那我给你个平均值呗,无功无过的,嘎嘎~region_size = MAX2(average_heap_size / HeapRegionBounds::target_number(), (uintx) HeapRegionBounds::min_size()); }
,
MAX 就是要取最大值嘛,取谁和谁的最大值呢,
(uintx) HeapRegionBounds::min_size()
这个上面说了,jvm给的HR界限里的最小值1MB,
average_heap_size / HeapRegionBounds::target_number()
这个就是刚才算完堆大小用平均值average_heap_size
除以Region目标个数2048就是每个Region的大小啦,
计算的大小与最小1MB取最大值,因为不能低于1MB呀~- 这就算完啦~~~
- 后面几步就是判断是否是2的次幂、是否在界限范围内
先理解到这个维度吧,逐步递进:
G1的内存分配策略就让人哇塞~~,大内存就是任性啊~,
G1 就是把堆内存给切豆腐了,不同的Region不同的策略,分而治之,一块一块的管理,
G1把内存分配策略搞介个样子,是出于GC效率考虑的,其它收集器进行GC都要整堆扫描,硬件发展这么迅速,内存已经很大了,JVM可以申请的内存也很大了,还整堆扫描,STW的时间太长了,用户忍不了啊,所以就有问题解决问题啊,切豆腐~
分区管理,每次GC部分收集(当然只有老年代是部分收集,年轻代还是all Young Region
收集的,嘎嘎~~),提高收集效率,响应用户也快,内存管理也高效,double win ~~总体还是分代思想,分为新生代和老年代,
GC分为Young GC 和 Mixed GC ,
也是以Serial Old 的Full GC 做担保机制
行话~
先来储备几个行话~~ 行走江湖不会说几句黑化怎么行~~嘎嘎~
STW - stop the world :世界静止了。。JVM停止一切用户线程
safepoint - 安全点: JVM并不能为所欲为地STW,设计安全点,当用户线程到达安全点时,主动停止
Mutator : Java应用线程,即用户线程
RSet - Remember Set : 记忆集,记录代际间引用关系,不用头大,先混个脸熟吧,嘎嘎~
Panda 白话 - G1垃圾收集器 之 RSet(Remembed Set)源码解读 - 传送门-灰常通俗白话
Refine - 优化 : 处理RSet的优化线程
Panda白话 - G1垃圾收集器 之 Refine线程 - 传送门
Evac-Evacuation - 转移:将存活对象复制到Survivor Region或Old Region过程
Reclaim - 回收 : Region中存活对象已经完成Evac,分区可以释放了
GC Root: GC的根节点,这不用说了,GC算法可达性分析法的可达就是GC Root可达
Root Set: 根集合
FGC - Fu’ll GC : 整堆回收,串行,STW,G1用Serial Old做担保机制做Full GC
Remark - 再标记
TLAB - Thread Local Allocation Buffer:线程本地分配缓冲区
我们来认真看一下TLAB,见名思议,每个线程一个本地缓冲区,这个解释听君一席话如听一席话,嘎嘎~
情况是这样的:
JVM中堆内存是线程共享的(这都不了解的话你就完犊了~~)
从堆内存分配对象需锁整堆,为不受其它线程中断和影响
那这肯定不行啊,创建个对象堆就被锁住了,创建个对象堆就被锁住了,其它线程还怎么干活了。。
所以JVM团队就设计了TLAB,
每个线程分配一块内存(Eden区的内存),每个线程创建的对象都在自己的内存分配,且不用上锁
所有TLAB内存对所有线程都是可见的,因为堆是共享的嘛,划分几块给各个线程,那它还是共享的啊,线程共享TLAB还怎么各管各的,还无锁分配对象呢?
每个线程有个数据结构,存放TLAB可用内存的start和end地址,新对象只能在这段区间分配,就做到了无锁分配,快速分配、看图说话:
T1线程用完两块TLAB,分配第三块TLAB了,第三块用完一半了,还是top 到end那么点空间可分配
T2和T4线程同理
一个Region可以有多个TLAB,一个TLAB不能占多个Region
可以通过参数-XX:TLABWasteTargetPercent: 1
设置TLAB占Eden空间百分比,默认1%
堆分配TLAB空间时还是要上锁的(G1使用CAS并行分配),同理分配对象,堆说我管你是谁呢,我是共享的,从我这分配空间就得上锁,得保证并发情况数据一致性啊
G1 - 参数设置 :
-XX:+UseG1GC - 开启G1垃圾收集器 、JDK9之后默认开启
-XX:G1HeapRegionSize - 指定每个region的大小 ,2的n次幂,1MB - 32MB (1MB、2MB、4MB、8MB、16MB、32MB)
-XX:G1NewSizePercent - 新生代占总堆最小百分比、默认值5%
-XX:G1MaxNewSizePercent - 新生代占总堆最大百分比、默认值60%
-XX:MaxGCPauseMillis - GC暂停时间,默认200ms
-XX:GCTimeRatio - GC与应用耗费时间比,G1默认是9,即GC占10%,CMS默认是99,即GC耗时占1%,
-XX:NewRatio - 年轻代占比,默认值 2,即1/3 年轻代,2/3 老年代
-XX:NewRatio 参数 和 -XX:G1NewSizePercent、-XX:G1MaxNewSizePercent 两个参数我们来说下吧,都是设置年轻代占比,那搞这么多干啥,当然各有各的用啦,
-XX:NewRatio
是固定了年轻代大小,值为2,则年轻代占总堆的1/3-XX:G1NewSizePercent、-XX:G1MaxNewSizePercent
是不固定年轻代大小,指定了范围,年轻代在总堆大小的5% 到 60% 动态调整- 我们可以通过参数
-XX:MaxGCPauseMillis
设置希望GC停顿最大时间,JVM会根据停顿预测模型来动态调整新生代大小,如果只设置了-XX:NewRatio
就会将动态调整大小false,可能就不能满足停顿预期时间了 - 如何同时设置了
-XX:NewRatio
参数 和-XX:G1NewSizePercent、-XX:G1MaxNewSizePercent
两个参数,JVM则自动忽略-XX:NewRatio
,源码如下:
if (FLAG_IS_CMDLINE(NewRatio)) {
if (FLAG_IS_CMDLINE(NewSize) || FLAG_IS_CMDLINE(MaxNewSize)) {
warning("-XX:NewSize and -XX:MaxNewSize override -XX:NewRatio");
} else {
_sizer_kind = SizerNewRatio;
_adaptive_size = false;
return;
}
}
Young GC - 年轻代垃圾收集
-
触发时机:
- 新的对象创建会放入Eden区
- Eden区满、G1会根据停顿预测模型-计算当前Eden区GC大概耗时多久
- 如果回收时间远 <
-XX:MaxGCPauseMills
,则分配空闲分区加入Eden 区存放新创建的o, - 如果回收时间接近-XX:MaxGCPauseMills,则触发一次Young GC
- 年轻代初始占总堆5%,随着空闲分区加入而增加,最多不超过60%
-
Young GC 会回收
all
新生代分区 - Eden区和Survivor 区 -
Young GC 会
STW
(stop the world),暂停所有用户线程 -
GC后重新调整新生代Region数目,每次GC回收Region数目不固定
-
回收过程:
- phase one - 扫描根
- phase two - 更新RSet
- phase three - 处理RSet
- phase four - 复制对象
- phase five - 处理引用
-
图解:
画图太慢了,借用大佬几张图这可能是最清晰易懂的 G1 GC 资料
-
-
初始状态: D -> B 、 C -> F 、 E -> C
- 年轻代 - Region A - 存放了对象 : A、 B、C
- 年轻代 - Region B - 存放了对象 :E
- 年轻代 - Region C - 存放了对象 F
- 老年代 - Region D - 存放了对象 :D
-
可以看到E->C, C -> F ,E、C、F都在新生代分区中,RSet不会记录他们的引用关系
-
D -> B ,老年代引用新生代,所以在B所在的Region A的RSet中会记录这个代间引用关系
-
第一步:选择CSet(收集集合)
- YGC收集所有新生代分区,根据停顿预测模型选择最大新生代分区数 -
第二步: GCRoot Scan(根集合扫描)
- 从GCRoot出发能直达的对象都是存活对象
- 存活对象复制到新的Survivor分区
- 存活对象的field入栈,留着后面深度递归用
panda.kongfu = new Kongfu()
,konfu属性就是panda对象的field,引用了对象Kongfu,通过kongfu存放的值可以找到Kongfu对象所在堆内存地址
-
示例图分析:
- 根可达的对象有A、D、E,D对象在老年代,不在YGC范围,所以将A、E存活对象复制到新的空闲Survivor分区Region M
- E->C ->F ,所以E的field c,C的field f 入栈
- 对象A、E复制到了新Region M,原Region中的对象A、E的对象头有点变化
- 对象头里面的指针指向新的对象(引用对象可以根据指针找到新对象)
- 对象头中锁标志位会被设置为 11 - 即可以GC
-
步骤三 : RSet Scan(扫描RSet):
- 更新RSet,即上面讲的Refine线程处理DCQS中DCQ记录的引用关系更新RSet,使RSet为最新完整信息
- 将RSet 作为GC Root进行根扫描,同步骤二
- 示例图分析: Region A 中RSet记录了对象D到对象B的引用,B为直接可达对象复制到Region M,因为D在老年代,不处理,所以D的field 不入栈
-
步骤四: Evacuation(复制):
- 将上面步骤放入栈中的field(存放的是引用对象的地址),将引用对象复制到Region M
- 示例图解:
- 栈中c、f指向的对象C、F复制到Region M
- 原对象C、F的对象头修改
- 将上面步骤放入栈中的field(存放的是引用对象的地址),将引用对象复制到Region M
-
步骤五:重构RSet、释放空间
- 更新RSet (Rset 记录引用对象地址和卡表信息,对象都重新分配了,它的地址,卡表肯定变了) Panda 白话 - G1垃圾收集器 之 RSet(Remembed Set)源码解读
- 清理卡表 - 释放空间
- 示例图解:
- CSet中Region A、B、D被回收了
- 回收后的Region作为Free Region ,下次被分配为哪种Region使用不确定
Mixed GC - 混合垃圾收集
未完待续。。。