一、垃圾收集器简介
1、发展历程
-
第一阶段,Serial(串行)收集器 在jdk1.3.1之前,java虚拟机仅仅能使用Serial收集器。 Serial收集器是一个单线程的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。 -
第二阶段,Parallel(并行)收集器 Parallel收集器也称吞吐量收集器,相比Serial收集器,Parallel最主要的优势在于使用多线程去完成垃圾清理工作,这样可以充分利用多核的特性,大幅降低gc时间。 -
第三阶段,CMS(并发)收集器 CMS收集器在Minor GC时会暂停所有的应用线程,并以多线程的方式进行垃圾回收。在Full GC时不再暂停应用线程,而是使用若干个后台线程定期的对老年代空间进行扫描,及时回收其中不再使用的对象。 -
第四阶段,G1(并发)收集器 G1收集器(或者垃圾优先收集器)的设计初衷是为了尽量缩短处理超大堆(大于4GB)时产生的停顿。相对于CMS的优势而言是内存碎片的产生率大大降低。
2、种类
-
新生代
- Serial (第一代)
- PraNew (第二代)
- Parallel Scavenge (第三代)
- G1收集器(第四代)
-
老年代
- Serial Old (第一代)
- Parallel Old (第二代)
- CMS (第三代)
- G1
二、G1 介绍
1、概述
- G1收集器的最大特点
- G1最大的特点是引入分区的思路,弱化了分代的概念
- 合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷
- G1相比较CMS的改进
- 算法:基于标记-整理算法,不会产生空间碎片,分配大对象时不会因得不到连续空间而提前触发 FULL GC
- 停顿时间可控: G1可以通过设置预期停顿时间
Pause Time 来控制垃圾收集时间避免应用雪崩现象 - 并行与并发:G1 能充分利用 多核 CPU 的硬件优势来缩短 stop the world 的停顿时间
- CMS和G1的区别
- CMS 的堆分为
PermGen、YoungGen、OldGen ;而YoungGen又分了两个survivo区域 G1 的堆被分为区域(region),每个区域虽然保留了新老代概念,但收集器是以整个区域为单位收集 - G1在回收内存后会马上同时做合并空闲内存的工作、而 CMS 默认是在STW(stop the world)时做
- G1会在 Young GC 中使用、而 CMS 只能在 O 区使用
- G1收集器的应用场景
- G1垃圾收集算法主要应用在多CPU大内存的服务中,在满足高吞吐量的同时,尽可能的满足垃圾回收时的暂停时间。
- 就目前而言、CMS还是默认首选的GC策略、可能在以下场景下G1更适合:
- 服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
- 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
- 想要更可控、可预期的GC停顿周期,防止高并发下应用雪崩现象
2、G1的堆内存算法
G1之前的JVM内存模型:
G1收集器的内存模型:
-
G1 收集器将整个Java 堆划分成约 2048 个大小相同的独立 Region 块,每个Region块大小根据堆空间的实际大小而定
可以通过 -XX :G1HeapRegionSize 设定,值在 1MB~32MB 之间且为 2 的次幂
-
region 可能属于 Eden、Survivor、0ld、Humongous 区域,但一个region只可能属于一个角色 -
Humongous 作用:用来专门存放大对象,一般使用连续 region 区存储,G1的大多数行为都把H区 作为老年代的一部分来看待
3、G1 的特点、缺点
3.1 特点
①. 并行和并发
- 并行性:G1 在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力
- 并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行
②. 分代收集
- 从分代上看,G1 仍属于分代型垃圾回收器,会区分年轻代和老年代,年轻代依然有 Eden 区和 Survivor 区
但从堆的结构上看,不要求整个 Eden 区、年轻代或老年代都连续,也不再坚持固定大小和固定数量 - 将堆空间分为若干个区域(Region),这些区域中包含了逻辑上的年轻代和老年代
- 和之前的各类回收器不同,其同时兼顾年轻代和老年代
③. 空间整合
- 内存回收以 region 为基本单位,Region之间是复制算法,但整体是标记-压缩算法,可以避免内存碎片,有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC
④. 可预测的停顿时间模型(即:软实时soft real一time) 可预测的停顿时间模型:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N 毫秒,通过参数 -XX:MaxGCPauseMillis 设置
- 由于分区的原因,G1可以只选取部分区域进行内存回收,缩小回收范围,避免全局停顿情况的发生
- G1 跟踪各个 Region 中垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
- 相比于 CMS GC,G1未必能做到CMS在最好情况下的延时停顿,但最差情况要好很多
3.2 缺点
- 相较于CMS,G1 还不具备全方位、压倒性优势,如:G1 无论是为了垃圾收集产生的内存占用,还是程序运行时的额外执行负载都要比 CMS 高
- 小内存应用上,CMS 的表现大概率会优于 G1,而 G1 在大内存应用上则发挥其优势,平衡点在6-8GB之间
4、G1 独有概念
4.1 RSet 与 card
RSet 与 card 专门用来处理 Old 区到 Young 区的引用 Young 区到 Old 区的引用不需要单独处理,因为 Young 区中的对象本身变化比较大,没必要浪费空间去记录下来
RSet :用来记录外部指向本 Region 的所有引用,每个 Region 维护一个 RSetCard : JVM 将内存划分成了固定大小的 Card,类比物理内存上 page 的概念 - 每个 Region 分成多个 Card,其中绿色部分的 Card 表示该 Card 中有对象引用了其他 Card 中的对象,这种引用关系用蓝色实线表示。
- RSet 其实是一个HashTable,Key 是 Region 的起始地址,Value 是Card Table(字节数组),字节数组下标表示 Card 的空间地址,当该地址空间被引用时会被标记为 dirty_card
RSet 的更新:
G1 采用 post-write barrier 和 concurrent refinement threads 更新 RSet,减少每次给引用类型的字段赋值都要更新 RSet带来的开销
java 层面给 old 对象的 p 字段赋值 young 对象之后,jvm 底层会执行 oop_store 方法
在赋值动作的前后,JVM插入一个 pre-write barrier 和 post-write barrier
post-write barrier 的最终动作如下:
- 找到该字段所在的位置(Card),并设置为 dirty_card
- 若当前是应用线程,每个Java线程有一个 dirty card queue,把该 card 插入队列
- 除了每个线程自带的 dirty card queue,还有一个全局共享的 queue
RSet 更新操作交由多个 ConcurrentG1RefineThread 并发完成,每当全局队列集合超过一定阈值后,ConcurrentG1RefineThread 会取出若干个队列,遍历每个队列中记录的card,并进行处理,大概实现逻辑如下: 1、根据 card 的地址,计算出 card 所在的 Region 2、若 Region 不存在,或 Region 是 Young 区,或该 Region 在回收集合中,则不进行处理 3、最终使用闭合函数 G1UpdateRSOrPushRefOopClosure::do_oop_nv() 的处理该 card 中的对象
refinement threads 线程数量可以通过 -XX:G1ConcRefinementThreads 或 -XX:ParallelGCThreads 参数设置
4.2 Collection Set
收集集合(CSet):一组可被回收的分区集合。在CSet中存活的数据会在GC过程中被移动到另一个可用分区,CSet中的分区可以来自eden空间、survivor空间、老年代
GC时在CSet中的所有存活数据都会被转移,分区释放回空闲分区队列
4.3 PLAB
在 GC 线程的晋升本地分配缓冲区(PLAB)中,对象晋升到 survivor 分区或老年代分区
每个线程有独立的PLAB,作用是避免多线程竞争相同数据
4.4 TLAB
JVM使用线程本地分配缓存TLAB 这种线程专属的区间,来避免多线程冲突(无锁方式),提高对象分配效率
TLAB 本身占用了 Eden 空间,即 JVM 会为每一个线程都分配一块 TLAB 空间
5、G1 回收过程
大致流程:
- 当 Eden 区用尽时,开始年轻代回收,而 G1 的年轻代收集阶段是一个并行(多个垃圾线程)的独占式收集器
年轻代回收时,G1 暂停所有应用程序线程,启动多线程执行年轻代回收,然后移动存活对象到Survivor 或 O 区
- 当堆内存使用达到一定值(默认45%,
-XX:InitiatingHeapOccupancyPercent )时,开始老年代并发标记过程 - 标记完成马上开始混合回收过程,此时 G1 从 O 区移动存活对象到空闲区间
一次只需要扫描/回收一小部分老年代的 Region 就可以
- 若 GC 的评估失败,则提供失败保护机制,即强力回收 FULL GC(单线程、独占式、高强度)
5.1 Young GC
- 回收时机:当 Eden 空间耗尽时,G1会启动一次年轻代垃圾回收过程
- 回收对象:年轻代垃圾回收只会回收 Eden 区和 Survivor 区
回收流程:
- 第一阶段,根扫描,根引用连同 RSet 记录的外部引用作为扫描存活对象的入口
- 根是指 static 变量指向的对象,正在执行的方法调用链条上的局部变量等
- 扫描 remembered Set,看是否有老年代中的对象引用了新生代对象
- 第二阶段,更新 RSet
- 处理 dirty card queue 中的 card,更新 RSet 后,可以准确反映老年代对所在的内存分段中对象的引用
- 第三阶段,处理 RSet
- 识别被老年代对象指向的 Eden 中的对象,这些被指向的 Eden 中的对象被认为是存活的对象
- 第四阶段,复制对象,遍历对象树
- Eden 区内存段中存活的对象会被复制到 Survivor 区中空的内存分段
- Survivor 区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被复制到 old 区中空的内存分段
- 若 Survivor 空间不够,Eden 空间的部分数据会直接晋升到老年代空间
- 第五阶段,处理引用,处理 Soft、Weak、Phantom、Final、JNI Weak 等引用
- 最终 Eden 空间的数据为空,GC停止工作,而目标内存中的对象都是连续存储的,没有碎片,所以复制过程可以达到内存整理的效果,减少碎片
5.2 Concurrent Marking
-
初始标记阶段:标记从根节点直接可达的对象,这个阶段是 STW 的,并且会触发一次年轻代 GC -
根区域扫描(Root Region Scanning):G1 GC 扫描 Survivor 区直接可达的老年代区域对象,并标记被引用的对象
这一过程必须在 young GC 之前完成(YoungGC时,会动 Survivor 区,所以这一过程必须在young GC之前完成)
-
并发标记(Concurrent Marking):在并发标记阶段,若发现区域对象中的所有对象都是垃圾,则这个区域会被立即回收,此过程可能被 young GC 中断
并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)
-
再次标记(Remark):修正上一次的标记结果,是 STW -
独占清理(cleanup,STW):计算各个区域的存活对象和 GC 回收比例,并进行排序,识别可以混合回收的区域,是STW 的
这个阶段并不会实际上去做垃圾的收集
-
并发清理阶段:识别并清理完全空闲的区域
5.3 Mixed GC
- 触发时机:老年代的堆占有率达到参数
-XX:InitiatingHeapOccupancyPercent 设定的值则触发 - 回收对象:回收所有的 Young 和部分 Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区
- 回收过程:MixedGC 主要使用复制算法,把各个 region 中存活的对象拷贝到别的 region 里去,拷贝过程中若发现没有足够的空 region 能够承载拷贝对象就会触发一次 Full GC
- 并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收
- 混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区 内存分段,Survivor区内存分段。 混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。
- 由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间
- 混合回收并不一定 要进行8次。有一个阈值**-XX :G1HeapWastePercent**,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少
5.4 Full GC
- 特点:G1会停止应用程序的执行(Stop-The-World) ,使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。
- 导致 G1 Full GC 的原因可能有两个: .
- 回收时,没有足够的 to-space 来存放晋升对象
- 并发处理过程没完成空间就耗尽
|