GC垃圾收集器和收集算法

【学习整理】GC垃圾收集器

垃圾收集算法-分代收集理论

针对年轻代,老年代的特点使用不同的算法。

年轻代变化快,复制算法转移快速

老年代很多不变,使用标记-清除、整理

算法包括:复制算法、标记整理算法、标记清除算法

在这里插入图片描述

分代收集理论

根据对象存活周期不同,将Java堆内存分为新生代、老年代,根据各个年代选择合适的垃圾收集算法。

比如新生代中,有99%的对象是需要回收的,只有1%是存活的,可以使用复制算法

老年代的存活比率是比较高的,而且没有额外的空间进行分配,所以必须选择标记-清除标记-整理 算法进行垃圾收集。

注意:标记-清除,标记-整理 比复制算法慢10倍以上

标记-复制算法

为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的 内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对 内存区间的一半进行回收。

在这里插入图片描述

标记-清除算法

算法分为“标记”和“清除”阶段:标记存活的对象, 统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 。它是最基础的收集算法,比较简单

但存在问题:

1、效率问题(标记对象如果太多,效率不高)

2、空间问题(标记清除后产生大量不连续的碎片)

在这里插入图片描述

标记-整理算法

根据老年代的特点特出的一种标记算法,标记过程和标记-清除一样,但后续步骤不是直接对可回收对象回收,二手让存活对象向一端移动,,然后直接清理掉边界以外的内存

垃圾收集器

在这里插入图片描述

年轻代:Serial(串行),Parallel,ParNew

老年代:Serial,Parallel,CMS

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

Serial(串行)收集器,原始的单线程收集器,工作时SWT(“Stop The World”),直到收集结束。

新生代复制算法,老年代-标记整理算法
在这里插入图片描述

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

JDK8默认的新生代和老年代收集器

Parallel收集器是Serial收集器的多线程版本,在不同的cpu核数下选择不同收集器,(单核用Serial减少线程切换的开销),也是会SWT

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

在这里插入图片描述

ParNew收集器(-XX:+UseParNewGC)

ParNew收集器其实跟Parallel收集器很类似,区别主要在于它可以和CMS收集器配合使用。

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

在这里插入图片描述

它是许多运行在Server模式下的虚拟机的首要选择,除了Serial收集器外,只有它能与CMS收集器(真正意义上的并发收 集器,后面会介绍到)配合工作。

CMS收集器-用于老年代(-XX:+UseConcMarkSweepGC(old))

CMS(Concurrent mark sweep)并发-标记-清除收集器,他是hotSpot VM 第一款真正意义上的并发收集器,他第一次实现了用户线程和GC线程同时工作。过程分为4个步骤:

1、初始标记(STW):暂停所有其他线程(SWT),并记录下gc-roots 直接能引用的对象,速度很快

2、并发标记:并发标记阶段,就是从GC Roots的直接关联对象开始遍历整个对象的过程,和用户线程同时进行,会产生标记过的对象又被用户线程使用,所以需要下一步的重新标记

3、重新标记(STW):解决并发标记阶段,用户线程操作的变动对象,使用三色标记里的(增量更新算法)再次标记。时间比初始标记长,远比并发标记小

4、并发清理:和用户线程同时进行,清除未标记的区域,此时用户线程新加是对象将标为黑色(不做清理)

5、并发重置:重置本次GC过程中的标记数据

在这里插入图片描述

优点:减少STW时间
缺点:
1、并发标记、并发清理阶段会产生垃圾,需要等到下一次GC处理-
2、使用标记清除算法会产品内存碎片(可通过参数配置让jvm执行完标记清除后做碎片的整理)
3、同时运行,GC清理和老年代数据生成,万一老年代空间不够,会Full-GC,也就concurrentmode failure此时会进入stop the world,用serial old垃圾收集器来回收-

CMS的相关核心参数

\1. -XX:+UseConcMarkSweepGC:启用cms

\2. -XX:ConcGCThreads:并发的GC线程数

\3. -XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少碎片)

\4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认是0,代表每次FullGC后都会压缩一

\5. -XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)

\6. -XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设

定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整

\7. -XX:+CMSScavengeBeforeRemark:在CMS GC前启动一次minor gc,目的在于减少老年代对年轻代的引

用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段

\8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW

\9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;

三色标记

在并发标记的过程中,因为标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。 这里我们引入“三色标记”来给大家解释下,把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色

黑色: 表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描

过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过 灰色对象) 指向某个白色对象。

灰色: 表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。

白色: 表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若

在分析结束的阶段, 仍然是白色的对象, 即代表不可达。

在这里插入图片描述

多标-浮动垃圾

在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过

(被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动 垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。

另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分 对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。

漏标-读写屏障

漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决,有两种解决方案: 增量更新(

Incremental **Update) 和原始快照(Snapshot At The Beginning,SATB) 。

增量更新就是当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之 后, 再将这些记录过的引用关系中的黑色对象为根, 重新扫描一次。 这可以简化理解为, 黑色对象一旦新插入了指向 白色对象的引用之后, 它就变回灰色对象了

原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑 色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾) 以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。

写屏障

在这里插入图片描述

写屏障实现SATB

在这里插入图片描述

写屏障实现增量更新

在这里插入图片描述

读屏障

在这里插入图片描述

现代追踪式(可达性分析)的垃圾回收器几乎都借鉴了三色标记的算法思想,尽管实现的方式不尽相同:比如白色/黑色 集合一般都不会出现(但是有其他体现颜色的地方)、灰色集合可以通过栈/队列/缓存日志等方式进行实现、遍历方式可 以是广度/深度遍历等等。

对于读写屏障,以Java HotSpot VM为例,其并发标记时对漏标的处理方案如下:

CMS:写屏障 + 增量更新

G1,Shenandoah:写屏障 + SATB

ZGC:读屏障

工程实现中,读写屏障还有其他功能,比如写屏障可以用于记录跨代/区引用的变化,读屏障可以用于支持移动对象的并 发执行等。功能之外,还有性能的考虑,所以对于选择哪种,每款垃圾回收器都有自己的想法。

为什么G1用SATB?CMS用增量更新?

我的理解:SATB相对增量更新效率会高(当然SATB可能造成更多的浮动垃圾),因为不需要在重新标记阶段再次深度扫描 被删除引用对象,而CMS对增量引用的根对象会做深度扫描,G1因为很多对象都位于不同的region,CMS就一块老年代 区域,重新深度扫描对象的话G1的代价会比CMS高,所以G1选择SATB不深度扫描对象,只是简单标记,等到下一轮GC 再深度扫描。

记忆集与卡表

在新生代做GCRoots可达性扫描过程中可能会碰到跨代引用的对象,这种如果又去对老年代再去扫描效率太低了。 为此,在新生代可以引入记录集(Remember Set)的数据结构(记录从非收集区到收集区的指针集合),避免把整个 老年代加入GCRoots扫描范围。事实上并不只是新生代、 老年代之间才有跨代引用的问题, 所有涉及部分区域收集 (Partial GC) 行为的垃圾收集器, 典型的如G1、 ZGC和Shenandoah收集器, 都会面临相同的问题。

垃圾收集场景中,收集器只需通过记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代引 用指针的全部细节。 hotspot使用一种叫做“卡表”(cardtable)的方式实现记忆集,也是目前最常用的一种方式。关于卡表与记忆集的关系, 可以类比为Java语言中HashMap与Map的关系。

卡表是使用一个字节数组实现:CARD_TABLE[ ],每个元素对应着其标识的内存区域一块特定大小的内存块,称为“卡 页”。

hotSpot使用的卡页是2^9大小,即512字节

在这里插入图片描述

根可达算法

目前Java中可以作为GC ROOT的对象有:

1、虚拟机栈中引用的对象(局部变量的引用对象)

2、方法区中静态属性引用的对象

3、方法区中常量 (找到引用常量的的对象)

4、本地方法栈中引用的对象(Native对象)

补充

JVM中STW的安全点与安全区域

当JVM开始执行GC之前,会通知所有解释器将目前执行的代码进入到运行的安全点后,下一步才会执行GC。

安全点就是指代码中一些特定的位置,当线程运行到这些位置时它的状态是确定的,这样JVM就可以安全的进行一些操作,比

如GC等,所以GC不是想什么时候做就立即触发的,是需要等待所有线程运行到安全点后才能触发。

这些特定的安全点位置主要有以下几种:

\1. 方法返回之前

\2. 调用某个方法之后

\3. 抛出异常的位置

\4. 循环的末尾

大体实现思想是当垃圾收集需要中断线程的时候, 不直接对线程操作, 仅仅简单地设置一个标志位, 各个线程执行过程

时会不停地主动去轮询这个标志, 一旦发现中断标志为真时就自己在最近的安全点上主动中断挂起。 轮询标志的地方和

安全点是重合的。

安全区域又是什么?

Safe Point 是对正在执行的线程设定的。

如果一个线程处于 Sleep 或中断状态,它就不能响应 JVM 的中断请求,再运行到 Safe Point 上。

因此 JVM 引入了 Safe Region。

Safe Region 是指在一段代码片段中,引用关系不会发生变化。在这个区域内的任意地方开始 GC 都是安全的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值