引言:CMS缺陷:
- 单核、双核情况下,效率低。
- foreground 模式(并发失败导致)下垃圾回收占用时间较长,比如:可中止预处理 默认是5s, MSC - 内存整理(Mark-Sweep-Compate)以及切换为单线程垃圾收集器- Serial Old收集器执行都是非常耗时间的操作。而且是无法人为干预的。
G1 (Garbage-First)垃圾收集器
垃圾收集器关注点是:停顿时间和吞吐量。
垃圾收集器 | 停顿时间 | 吞吐量 | 是否并发 |
---|---|---|---|
CMS | 尽可能小,不能人为控制 | 基本无法控制 | 是 |
Parallel Scavenge | 可控制 | 可控 | 否 |
G1 | 可控 | 可控 | 是 |
G1特性
引言:
使用G1收集器时,Java堆的内存布局与就与其他收集器有很大差别,它将整个Java堆划分为多个 大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
每个Region大小都是一样的,可以是1M到32M之间的数值,但是必须保证是2的n次幂 如果对象太大,一个Region放不下[超过Region大小的50%],那么就会直接放到H中 设置Region大小:-XX:G1HeapRegionSize=M 所谓Garbage-Frist,其实就是优先回收垃圾最多的Region区域
```
(1)分代收集(仍然保留了分代的概念)
(2)空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
(3)可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)
```
具体来说:
常规操作尽量避免并发,所以相对于CMS来说CPU负载较低。
分代收集,逻辑上将堆内存进行重新划分。具体如图
将内存划分为大小相等的region。region大小范围在 1~32M之间且为2的幂次方倍。系统默认会将堆内存分为2048个region,这样的话堆内存最小2G
总结:G1是根据region进行展开的,region是G1的基础。region可以进行角色的转换,如从old转成Eden,region一共有5个角色,分别是:empty space、Eden space、survivor space、old generation和Humongous。其中Humongous用于存储大对象,当对象超过region一半大小的时候认为是大对象会放到Humongous中。
region
- region分类:
JVM中对象引用分为两种:Point out(我引用谁) 和Point in(谁引用我)。
问题:G1中region和CMS中card Page 的区别和联系?
区别:
- region最小是1M,card Page 是固定大小512K。
联系:
一个region中包含了多(>=2)个card Page。
card table 是CMS为了解决跨带引用的问题 (PS:*Card Table知识点可以参考 标记整理算法--滑动整理--单次遍历算法 *);
Rset-- RemeberSet 引用集 通过Point In 来实现 Rset 引用集是为了解决跨Region引用的问题,可以理解为Card Table的升级版。
Rset 使用到那三种数据结构,作用是什么?
稀疏表 -- 记录每个region中card page 的索引位置(起始位置)。
稀疏表本质是哈希表,K-V结构,其中K是每个region的起始地址 V是一个记录了Card Page数据索引号的数组 ;作用:字典,查询每块card page 索引。
细粒度位图。 -- 记录card Page 中对象引用的变化,一个标识位对应一个card page。
细粒度位图 中记录了region中对象引用的变化,注意:region能分为多少位,他就能分为多少位。
粗粒度位图。 -- 一个标识位对应一个region,当一个region中的card page到达一定数量的时候进行使用。
总结:Rset 是三种结构搭配来使用的,不是针对单独的一种结构。注意:粗细不同时存在,粗细力度阈值 = 64G,因为G1是并发类垃圾收集器,当多线程环境下位图维护会存在写入乱序的问题,这是G1通过写屏障来进行维护。
写屏障会带来内存伪共享问题,什么是内存伪共享,如何解决?
写屏障:在并发标记阶段,如果对象引用有变化,这时候会将对象的变化进行记录防止误删重新被引用的对象,类似于Spring AOP的思想。各个垃圾收集器的具体实现可能会略有不同。 参考:https://www.jianshu.com/p/12544c0ad5c1 内存伪共享: 多核CPU的情况下有多个 L1 和 L2 缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。 MESI规定了一个cache line存在四种状态:Modified、Exclusive、Shared 和Invalid,这有点像状态机的转换,理清全部的状态较复杂,我们关注简单的: Modified:该缓存行只被缓存在该CPU的缓存中,并且是被修改过的,即与主存中的数据不一致,该缓存行中的内存需要在未来的某个时间点(允许其它CPU读取请主存中相应内存之前)写回主存。当被写回主存之后,该缓存行的状态会变成Exclusive状态。 Exclusive:该缓存行只被缓存在该CPU的缓存中,它是未被修改过的,与主存中数据一致。该状态可以在任何时刻当有其它CPU读取该内存时变成Shared状态。同样地,当CPU修改该缓存行中内容时,该状态可以变成Modified状态。 Shared:该状态意味着该缓存行可能被多个CPU缓存,并且各个缓存中的数据与主存数据一致,当有一个CPU修改该缓存行中,其它CPU中该缓存行可以被作废。 Invalid:该缓存是无效的(可能有其它CPU修改了该缓存行)。 下图中thread0位于core0,而thread1位于core1,二者均想更新彼此独立的两个变量,但是由于两个变量位于同一个cache line中,此时可知的是两个cache line的状态应该都是Shared,而对于cache line的操作core间必须争夺主导权(ownership),如果core0抢到了,thread0因此去更新cache line,会导致core1中的cache line状态变为Invalid,随后thread1去更新时必须通知core0将cache line刷回主存,然后它再从主存中load该cache line进高速缓存之后再进行修改,但令人抓狂的是,该修改又会使得core0的cache line失效,重复上演历史,从而高速缓存并未起到应有的作用,反而影响了性能。 内存伪共享参考:https://zhuanlan.zhihu.com/p/55917869
通过添加参数:-XX:+UserCondCardMark来解决(PS:可以理解为对该内存进行了加锁操作),也是G1针对并发写card Table/region的问题即区内存共享问题,是JVM调优的手段
Rset 缺点:耗内存,占比 5%~10% 。
Rset是同步还是异步?-- 异步
异步的化通常用队列来处理,Rset声明一个全局的DCQS(Dirty Card Queue Set 用来处理对象引用变化的) 和 G1BarrirSet(用来处理SAB)。
G1执行流程
- 初始标记(Initial Marking) 标记以下GC Roots能够关联的对象,并且修改TAMS的值,需要暂停用户线程(STW)
- 并发标记(Concurrent Marking) 从GC Roots进行可达性分析,找出存活的对象,与用户线程并发执行
- 最终标记(Final Marking) 修正在并发标记阶段因为用户程序的并发执行导致变动的数据,需暂停用户线程(STW)
- 筛选回收(Live Data Counting and Evacuation) 对各个Region的回收价值和成本进行排序,根据 用户所期望的GC停顿时间制定回收计划
三色标记算法 : 注意这里的色是指逻辑状态。
黑色:扫描完的
灰色:正在扫描的
白色:未扫描的。
Rset和Card Table 是三色标记算法的落地,CardTable 关注的是引用的增加--当已扫描的A增加了C引用的化,A状态变成正在扫描,会对A再次进行扫描,Rset关注的是引用的变化 -- 当已扫描的A增加了C引用的化,会将C引用放到GC Root中来对变化进行记录。
- G1垃圾回收分类:young GC、mixed GC(混合模式:回收整个young区和部分Old区)、Full GC。
其中young GC -- STW
- young GC执行流程
STW
锁定整个所有新生代的region
根扫描
更新Rset -- 目的:记录我们引用的变化
存活对象复制--将Eden和Survivor存活对象复制到空闲survivor区
重构Rset
释放锁定
如果存在大对象进行大对象的回收
动态的扩展内存
动态调整region数量
启动并发标记
初始标记:1~10
mixed GC
mixed GC = young GC + 收益高的Old Region。
参数:-XXMixedGCTarget = 8 :将Old 区默认分成8份,每次mixed GC 回收整个Young区 + 1/8Old 区。
Full GC
触发条件:没有足够的region
JDK11之后 并行执行,之前是串行执行。
G1相关参数
-XX: +UseG1GC 开启G1垃圾收集器
-XX: G1HeapReginSize 设置每个Region的大小,是2的幂次,1MB-32MB之间 -XX:MaxGCPauseMillis 最大停顿时间
-XX:ParallelGCThread 并行GC工作的线程数
-XX:ConcGCThreads 并发标记的线程数
-XX:InitiatingHeapOcccupancyPercent 默认45%,代表GC堆占用达到多少的时候开始垃圾收集,JDK8之后版本
TLAB (Thread Loacal Allocation Buffer) 线程本地缓冲区
TLAB存在位置?
TLAB 是存在于堆内存的一块线程本地缓冲区,可以理解为堆内存的高速缓冲区。
TLAB解决的问题?
为了解决访问临界区线程需要全局加锁导致效率下降的问题。
//TODO 临界区行为:解释。 JNI(Java Native Interface)意为 Java 本地调用,它允许 Java 代码和其他语言写的 Native 代码进行交互。 JNI 如果需要获取 JVM 中的 String 或者数组,有两种方式: 拷贝传递。 共享引用(指针),性能更高。 由于 Native 代码直接使用了 JVM 堆区的指针,如果这时发生 GC,就会导致数据错误。因此,在发生此类 JNI 调用时,禁止 GC 的发生,同时阻止其他线程进入 JNI 临界区,直到最后一个线程退出临界区时触发一次 GC。 参考:https://zhuanlan.zhihu.com/p/291044796 //TODO OOPS:指针压缩技术(Ordinary Object Pointers)。 //TODO Jvmti:是JVM给开发者的一整套的后门。如class加密,热部署,埋点等都会用到JVmti。
ZGC
JDK11新引入的ZGC收集器,不管是物理上还是逻辑上,ZGC中已经不存在新老年代的概念了 会分为一个个page,当进行GC操作时会对page进行压缩,因此没有碎片问题 只能在64位的linux上使用,目前用得还比较少
优势:
可以达到10ms以内的停顿时间要求
支持TB级别的内存,目前最大支持4TB
64位操作系统中:未压缩的指针是8字节,ZGC使用4字节来记录引用的变更,具体记录的内容:finalizer方法是否被使用的标记、引用集是否被标记过(Dirty Card)、三色标记状态。
这样64 - 18 - 4 = 42,目前64位的Linux下高18位是不能寻址的。
堆内存变大后停顿时间还是在10ms以内