JVM垃圾收集

垃圾收集算法

 分代收集理论

堆内存分为年轻代和老年代,两个内存区域分别实现各自的垃圾收集器,可以更有针对性,更高效的清理垃圾。

比如年轻代的minorGC 会有99%的内存会被清理,使用复制删除算法效率更高。而老年代没有额外的空间去使用复制算法,使用标记清除、标记整理进行垃圾收集。标记-清除、标记-整理算法效率比复制算法慢10倍。

复制算法

 将内存对半分,一半作为保留内存,将标记为非垃圾对象复制到另一边,之后直接对原来一半内存空间清理。一般用在年轻代。

标记清除算法

 标记存活对象(对象头)或则垃圾对象,将垃圾对象清空。

存在一些问题:

1.堆内存很大的话GC过程会很慢,效率不高。

2.存在碎片内存。

标记整理算法

 将非垃圾对象标记并按顺序转移到空内存,其他内存空间作为垃圾清理掉。

 垃圾收集器

 

 Serial垃圾收集器(-XX:+UseSerialGC -XX:+UseSerialOldGC)

 也叫串型垃圾收集器,可以使用在年轻代,老年代。

年轻代使用复制算法,老年代使用标记整理算法

缺点很明显,每次GC都是单线程,STW时间长,用户体验不佳。

 

 Parallel垃圾收集器(-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代))

相比于Serial将单线程GC改为多线程并行的方式,在多核CPU下可以减小STW的时间。JDK1.8默认使用这种收集器

年轻代使用标记复制算法,老年代使用标记整理算法

 ParNew垃圾收集器(-XX:+UseParNewGC)

和Parallel很类似,用于年轻代的垃圾收集。主要区别:除了Serial收集器外,只有ParNew可以和CMS垃圾收集器配合使用。

新生代采用复制算法,老年代采用标记-整理算法。

 

 CMS垃圾收集器(-XX:+UseConcMarkSweepGC(old))

相较于Parallelold垃圾收集器,CMS更加注重STW的时间控制(更注重用户体验)。

 初始标记:从 GC Root出发,只标记它的直接引用,该该过程会STW。

并发标记:遍历所有GC Root,并标记,该过程不会停止用户线程。

重新标记:主要处理并发标记过程中漏标的情况,将他们重新标记,该过程会STW。

并发清理:清理未被标记的垃圾对象,该过程不会停止用户线程。

并发重置:将之前的非垃圾对象的标记清除。

使用垃圾清理算法。

存在问题:

1.并发标记过程由于用户线程未停止,会存在多标(浮动垃圾)、漏标的问题。针对多标(浮动垃圾),一般的处理方式是将对象留到下一轮GC去清理。对于漏标的情况,会采用 增量更新 的方法进行处理。

2.由于使用垃圾清理算法,存在碎片空间。

3.由于是和用户线程并行,会对整个GC过程的时间消耗有所增加。

4.当在并发清理或者并发标记时,又一次内存满了,再次触发full gc,此时会STW,使用serial old进行垃圾处理。

G1垃圾收集器

G1是专门为多核大容量的机器设计的垃圾收集器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。

G1将内存空间划分为很多个小块区域(region),JVM最多有2048个region。比如堆空间大小如果为4G ,划分为2048个region的话每个region占用2M左右空间。

默认年轻代占用总空间5%,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,最多不允许超过60%。

一个region可能为old经过GC后变成Eden区。

G1有专门存放大对象的内存区域Humongous。

G1也有自己的大对象的判断,默认对象超过一个region空间50%就会认为是大对象,存放在Humongous区,如果对象过大会用连续几个region空间来存放。Humongous一般用来存放短期存活的大对象,以此来减轻Old区GC时的压力。

在full GC时不仅仅会清理old区数据,同时会清理Humongous。

初始标记:同CMS初始标记。

并发标记:同CMS并发标记。

最终标记:同CMS重新标记。

筛选回收:G1垃圾收集会根据参数设置的最大停顿时间,动态计算在指定时间内有效收集垃圾的最大收益,只收集一部分垃圾,剩余的垃圾在下一次GC时清理。

回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片(注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本)。

G1垃圾收集器的特点

1.充分利用多核CPU并行处理。提升用户体验。

2.虽然说物理地址上没了分代的概念,但是逻辑上还是延续的分代。

3.和CMS一样都致力于提升用户的体验。除此之外,G1可以用户化定制最大的停顿时间,动态的优化垃圾回收效率。

4.整体上看采用的标记-整理算法,局部看是采用复制算法(CMS采用的标记-清除算法),G1垃圾收集器几乎不会出现内存碎片问题。

G1垃圾收集分类

young gc:G1收集器Eden区满了不一定会立刻触发young gc,而是会根据设置的最大停顿时间评估一下young gc时间是否已经到达(如200ms),直到达到这个值或者Eden区占用空间打到指定的阈值就会触发young gc

mixed gc:混合GC,不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,主要采用复制算法,将存活对象复制到另外的region区域中,当复制过程中发现没有足够的空间了则会触发full gc

full gc:STW,采用单线程标记-整理整个堆空间。空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。

垃圾标记算法底层实现

三色标记算法

在并发标记过程中主要使用三色标记算法标记对象。

 黑色:代表存活对象,并且内部属性已经全部标记结束,在并发清理期间不会对该对象进行任何处理。

灰色:代表对象中至少存在一个及以上没有被扫描。

白色:垃圾对象。

多标-浮动垃圾

在并发标记过程中,由于用户线程也在执行,可能会存在对象在标记时是非垃圾对象,但是之后的用户线程又将对象引用置空了,会出现多标的情况(浮动垃圾)。

浮动垃圾一般由并发重置后由下一轮GC进行处理。

漏标-读写屏障

如上图所示,在并发标记过程中,当B.C扫描结束,B.D还未进行扫描,用户线程将B.C引用置空,然后又将A和D创建一个引用,此时D对象不会被扫描到,会被当做垃圾对象。针对这种漏标的情况其实是很严重的问题。一般解决方案是 增量更新,和原始快照(STAB)。

增量更新:在用户线程创建A,D引用的时候,需要将该引用维护到一块内存区域,重新标记的时候处理,可以理解为将A标记为灰色在重新扫描期间标记。

原始快照:在用户线程将B.D赋值为null(置空)时,将B.D的引用记录下来,重新标记的时候将把D标记为黑色,避免D在本次GC中被当作垃圾清理掉。

那么JVM是怎么实现增量更新、原始快照的呢?

JVM底层使用C++的写屏障实现该功能,可以理解为在赋值之前或之后进行一些处理,类似AOP思想。

 写屏障
给某个对象的成员变量赋值时,其底层代码大概长这样:

1 /**
2 * @param field 某对象的成员变量,如 a.b.d
3 * @param new_value 新值,如 null
4 */
5 void oop_field_store(oop* field, oop new_value) {
6  *field = new_value; // 赋值操作
7 }


所谓的写屏障,其实就是指在赋值操作前后,加入一些处理(可以参考AOP的概念):

1 void oop_field_store(oop* field, oop new_value) {
2  pre_write_barrier(field); // 写屏障‐写前操作
3  *field = new_value;
4  post_write_barrier(field, value); // 写屏障‐写后操作
5 }


写屏障实现SATB
当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d = null),我们可以利用写屏障,将B原来成员变量的引用
对象D记录下来:

1 void pre_write_barrier(oop* field) {
2  oop old_value = *field; // 获取旧值
3  remark_set.add(old_value); // 记录原来的引用对象
4 }


写屏障实现增量更新
当对象A的成员变量的引用发生变化时,比如新增引用(a.d = d),我们可以利用写屏障,将A新的成员变量引用对象D
记录下来:

1 void post_write_barrier(oop* field, oop new_value) {
2  remark_set.add(new_value); // 记录新引用的对象
3 }

大多数垃圾收集器的可达性分析算法都是借鉴的三色标记算法。

不同垃圾收集器针对并发标记的漏标处理方式不同:

CMS:增量更新+写屏障

G1:STAB+写屏障

ZGC:读屏障

记忆集和卡表

在新生代做GCRoots可达性扫描过程中可能会碰到跨代引用的对象,这种如果又去对老年代再去扫描效率太低了。
为此,在新生代可以引入记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个
老年代加入GCRoots扫描范围。事实上并不只是新生代、 老年代之间才有跨代引用的问题, 所有涉及部分区域收集
(Partial GC) 行为的垃圾收集器, 典型的如G1、 ZGC和Shenandoah收集器, 都会面临相同的问题。
垃圾收集场景中,收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代引
用指针的全部细节。
hotspot使用一种叫做“卡表”(cardtable)的方式实现记忆集,也是目前最常用的一种方式。关于卡表与记忆集的关系,
可以类比为Java语言中HashMap与Map的关系。
卡表是使用一个字节数组实现:CARD_TABLE[ ],每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡
页”。
hotSpot使用的卡页是2^9大小,即512字节


一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变
脏,否则为0.
GC时,只要筛选本收集区的卡表中变脏的元素加入GCRoots里。
卡表的维护
卡表变脏上面已经说了,但是需要知道如何让卡表变脏,即发生引用字段赋值时,如何更新卡表对应的标识为1。
Hotspot使用写屏障维护卡表状态

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值