目录
- 一、G1垃圾回收器
- G1垃圾回收阶段(3个)
- ① Young Collection
- ② Young Collection + CM
- ③ Mixed Collection
- Young Collection 跨代引用
- Remark
- G1—垃圾回收器优化
- 字符串去重
- 类卸载
- 巨型对象
- 动态调整阈值
一、G1垃圾回收器
定义:
Garbage First(优先回收那些垃圾较多的区域以达到暂停时间短的目标)
JDK 9以后默认使用,而且替代了CMS 收集器
适用场景
● 同时注重吞吐量和低延迟(响应时间),默认的暂停目标是200ms
● 超大堆内存(内存大的),会将堆内存划分为多个大小相等的区域(每个区域差不多为1248M,每个区域都可以独立的作为伊甸园、幸存区、老年代)
● 整体上是标记+整理算法,两个区域之间是复制算法
相关参数:JDK8 并不是默认开启的,所需要参数开启
G1垃圾回收阶段(3个)
新生代伊甸园垃圾回收—–>内存不足(老年代内存超过预值),新生代回收+并发标记—–>回收新生代伊甸园、幸存区、老年代内存——>新生代伊甸园垃圾回收(重新开始)
① Young Collection
分区算法region
分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间
E:伊甸园 S:幸存区 O:老年代
● 会STW
【当类加载时,新创建的对象会分配到E(伊甸园)区,当其被逐渐占满会触发新生代垃圾回收】
【新生代垃圾回收将幸存的对象拷贝到幸存区(复制算法)】
【当幸存代区对象也较多时并且存活年龄超过一定时间会触发新生代垃圾回收,幸存区部分对象会晋升至老年代,将不够年龄的对象再次拷贝到另一个幸存区】
② Young Collection + CM
CM:并发标记(由根对象出发沿着引用链标记其他对象)
● 在 Young GC 时会对 GC Root 进行初始标记(找到根对象)
● 在老年代占用堆内存的比例达到阈值时,对进行并发标记,不会影响到用户的工作线程(不会STW),阈值可以根据用户来进行设定
-XX:InitiatingHeapOccupancyPercent=percent
(默认45%)
③ Mixed Collection
会对E S O 进行全面的回收
● 最终标记
● 拷贝存活
-XX:MaxGCPauseMills:xxx
用于指定最长的停顿时间
【E区幸存对象会被复制到Survive区中,另一些Survive区中不够年龄的也会复制其中,符合晋升条件的对象会放入老年代中】===>属于新生代垃圾回收(发生在混合收集阶段)
【老年代区域经过并发标记阶段发现部分对象无用后同样采用复制算法将对象放入一个新的老年代区===>属于新生代垃圾回收】
问:为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)
Full GC(G1何时触发Full GC)
G1在老年代内存不足时(老年代所占内存超过阈值)
● 如果垃圾产生速度慢于垃圾回收速度,不会触发Full GC,还是并发地进行清理
● 如果垃圾产生速度快于垃圾回收速度,并发收集失败,退化为一个串行的收集(触发Full GC,与CMS相同)
Young Collection 跨代引用
● 新生代回收的跨代引用(老年代引用新生代)问题
● 卡表与Remembered Set(记录外部对它的引用)
———Remembered Set 存在于E中,用于保存新生代对象对应的脏卡
————脏卡:O被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
好处 :在GC Root遍历时不用去找整个老年代,而是只需要关注脏卡对象(减小搜索范围,提高扫描根对象效率)
● 在引用变更时通过post-write barried(写屏障)+ dirty card queue(异步操作)
● concurrent refinement threads 更新 Remembered Set
Remark
重新标记阶段
● pre-write barried + satb_mark_queue(在对象引用改变前将对象加入队列并表示其为未被处理的)
在垃圾回收时,收集器处理对象的过程中
黑色:已被处理,需要保留的
灰色:正在处理中的
白色:还未处理的
【并发标记阶段对象的处理状态】
但是在并发标记过程中,有可能A被处理了以后未引用C,但该处理过程还未结束,在处理过程结束之前A引用了C,这时就会用到remark
过程如下
● 之前C未被引用,这时A引用了C,就会给C加一个写屏障,写屏障的指令会被执行,将C放入一个队列当中,并将C变为 处理中 状态
● 在并发标记阶段结束以后,重新标记阶段会STW,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它
G1—垃圾回收器优化
其优化在持续进行中,以下为JDK8~JDK9的优化,更新的优化可参考Oracle官方文档
字符串去重
JDK 8u20 字符串去重
优点与缺点
● 节省了大量内存
● 新生代回收时间略微增加,导致略微多占用CPU
-XX:+UseStringDeduplication
(开启字符串去重功能,默认打开)
String s1=new String("hello"); //char[]{'h','e','l','l','o'}
String s2=new String("hello"); //char[]{'h','e','l','l','o'}
过程
● 将所有新分配的字符串(底层是char[])放入一个队列
● 当新生代回收时,G1并发检查是否有重复的字符串
● 如果字符串的值一样,就让他们引用同一个字符串对象
● 注意,其与String.intern的区别
——intern关注的是字符串对象
——字符串去重关注的是char[]
——在JVM内部,使用了不同的字符串标
类卸载
JDK 8u40 并发标记类卸载
在所有对象经过并发标记结束后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类(从8u40开始以后)
-XX:+ClassUnloadingWithConcurrentMark
默认启用
巨型对象
JDK 8u60 回收巨型对象
● 一个对象大于region的一半时,就称为巨型对象
● G1不会对巨型对象进行拷贝
● 回收时被优先考虑
● G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
动态调整阈值
JDK 9 并发标记起始时间的调整
● 并发标记必须在堆空间沾满前完成,否则退化为FullGC
● JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent
● JDK 9 可以动态调整
—— -XX:InitiatingHeapOccupancyPercent 用来设置初始值
—— 进行数据采样并动态调整
—— 总会添加一个安全的空挡时间