Garbage Collection 垃圾回收机制
GC的作用区域:方法区与堆
- Minor GC
- Full GC / Major GC
GC面试题
- JVM内存模型以及分区,需要详细到每个区放什么
- 堆里面的分区:Eden、Survivor From To,老年代、各自的特点
- GC的三种收集方式:标记清除,标记整理,复制算法的原理和特点,分别用在什么地方
- MinorGC与FullGC分别发生在什么时候
确定垃圾的方法
- 引用计数法 易产生循环引用问题,导致垃圾无法被有效回收
- 可达性分析 采用根搜索算法(GC Roots Tracing),以一系列GC Roots的点作为起点向下搜索,在一个对象到任何GC Roots都没有引用链相连时,说明对象已无效 主要针对栈引用对象、方法区静态引用对象、JNI引用对象、方法区常量引用对象
引用计数法
- 给对象添加一个引用计数器,每当有一个地方引用他,计数器就加1,每当一个引用失效,计数器减1,为0就回收
- 对象之间相互引用的话会产生循环引用问题,导致他们不能被正确回收,所以用的很少了
可达性分析
解决循环引用的弊端
- 先定义一些GC Roots对象,然后以这些GC Roots对象为起点,向下搜索。如果在GC Roots和一个对象之间没有可达路径,则称该对象是不可达的
- 不可达对象要至少经过两次标记才能判定其能否被回收,如果在两次标记后该对象若能然是不可达的,则会被垃圾收集器回收
- 栈引用对象、方法区静态引用对象、JNI引用对象、方法区常量引用对象都可以作为GC Roots
可达性分析——跨代引用问题
如果引用跨越了新生代和老年代,那么对 GC Roots 的遍历就会延伸到老年代中,这个遍历的消耗是巨大的,显然需要一些机制来合适的处理这个问题
这个问题的解决基于
快带收集理论
:弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的
强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡
跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数
因此,不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录 每一个对象是否存在及存在哪些跨代引用,只需在
新生代
上建立一个 全局的数据结构(该结构被称 为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描Remember Set 的实现:
在 HotSpot 的解决方案里,是使用一组称为OopMap的数据结构来达到这个目的,简单粗暴的存储对象的引用,但后来觉得这样太浪费空间了
为了节省资源,只选取一些关键的指令,当这些指令执行时,维护 OopMap (Remember Set),就像赛车游戏里记录时间的检查点一样,这样的指令被称为
安全点
,GC 算法就是在安全点暂停用户线程的(Stop the World)如果指令不能执行,比如线程被挂起了,那就要借助
安全区
,安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区域看作被扩展拉伸了的安全点。当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,那样当这段时间里虚拟机要发起垃圾收集时就不必去管这些已声明自己在安全区域内的线程了。当线程要离开安全区域时,它要检查虚拟机是否已经完成了根节点枚举(或者垃圾收集过程中其他需要暂停用户线程的阶段),如果完成了,那线程就当作没事发生过,继续执行;否则它就必须一直等待,直到收到可以离开安全区域的信号为止为节省空间 OopMap 不记录对象的引用,而是记录
卡精度
每个记录精确到一块内存区域,该内存区有对象含有跨代指针,这样延伸到老年代的 GC Roots 遍历就可以被限制在老年代的一小部分中,这时的 OopMap 被称为卡表 Card Table
,卡表中指向的内存区域有跨代指针就标记为 1,称这个元素为变脏 Dirty
,没有就标记 0何时更新维护 Remember Set 的值:应当在发生赋值操作之后立刻更新,但对于编译好的代码,就是一堆二进制机器码,没办法做这件事,于是使用了
写屏障
技术,它跟 Volatile、缓存一致性协议、JMM 的写屏障不是一个东西,这里的写屏障是类似 AOP 的东西,切在所有赋值操作后面执行更新卡表
的操作
并发可达性分析
- 在根节点枚举这个步骤中,由于GC Roots相比起整个Java 堆中全部的对象毕竟还算是极少数,且在各种优化技巧(如OopMap)的加持下,它带来 的停顿已经是非常短暂且相对固定(不随堆容量而增长)的了
- 可从GC Roots再继续往下遍历对象 图,这一步骤的停顿时间就必定会与Java堆容量直接成正比例关系了:堆越大,存储的对象越多,对象图结构越复杂,要标记更多对象而产生的停顿时间自然就更长,这听起来是理所当然的事情
- 要知道包含“标记”阶段是所有追踪式垃圾收集算法的共同特征,如果这个阶段会随着堆变大而等比例增加停顿时间,其影响就会波及几乎所有的垃圾收集器,同理可知,如果能够削减这部分停顿时间的话,那收益也将会是系统性的,因此要并发
三色标记法——说明并发环境下可达性分析存在的问题
白色
:表示对象尚未被垃圾收集器访问过。显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达黑色
:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过。黑色的对象代 表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无须重新扫描一遍。黑色对 象不可能直接(不经过灰色对象)指向某个白色对象。灰色
:表示对象已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描过并发可达性问题的问题——两种
- 两种问题都是因为一边标记、一边用户线程又在不停地产生新对象新引用
- 本来应该被删的垃圾,被标记错了,没删掉
- 没啥事,就是影响了垃圾回收效率,但是没错,下一次 GC 删了就行
- 本来不能删,被标记错了,被删掉了(本来是黑的,被弄成白的删了)
- 致命级错误:必然导致程序出错
- 触发第二种致命错误必须同时满足两个条件:
- 插入了一条或多条从黑色对象到白色对象的新引用; (遍历时应当从黑色节点遍历过这些新的白色节点,但是由于白色节点时新添加的,所以它并不会被遍历,也就是说新添加了引用,但是标记算法并没有检测到)
- 删除了全部从灰色对象到该白色对象的直接或间接引用。(可以理解为快照性,就是一旦 GC Roots 遍历开始,虽然用户线程在继续工作,但遍历在逻辑上应当是对着开始时的一个堆快照进行的,否则就会出现所描述的这种情况,就是本来是会被标记到的,但是还没标记到这个节点时,用户线程就把这节点给干掉了,就好比对着地图找地方,结果地图会动,地方直接消失了)
- 要实现并发可达性分析进行标记,打破上述两个条件之一即可
- 对于第一条件——
增量更新
:从添加白色节点的黑色节点处开始再遍历一次- 对于第二条件——
原始快照
:记录这些被删除的节点,重新遍历他们- CMS 垃圾收集器的
重新标记
阶段,就是在使用增量标记
算法解决并发问题,因为 CMS 重新标记之前的阶段就是并发标记
四大GC算法
所谓
标记
,就是执行可达性分析
标记清除算法
最基础的垃圾回收算法
标记清除算法分为标记和清除两个阶段
- 标记:标记所有需要清除的对象
- 清除:清除可回收的对象并释放其所占的内存空间
效率低:内存碎片多:由于标记清除算法清理所占内存后并没有重新整理可用内存空间,因此如果回收的小对象很多,就会因此内存碎片化问题,继而引起大对象找不到足够大的连续内存空间存放的问题
复制算法
可以解决标记清除算法内存碎片化问题
- 将可用内存一分为二,1区域和2区域
- 将新生成的对象存放在1区域,如果1区域满了就进行一次标记,标记后仍存活的对象全部复制到2区域
- 直接清空整个1区域
优点:
- 复制算法的内存效率高且易于实现
缺点:
直接废了一半内存
如果对象很多很大,复制过程很消耗资源
使用场景:
- 多用于朝生夕死的对象,小对象
- 新生代、从Eden、Survivor From 复制到Survivor To的Minor GC
标记整理算法
结合了标记清除和复制算法的优点
- 标记阶段:与标记清除算法一样
- 整理阶段:将存活对象移动到内存的一端,然后清除另一端的对象,释放内存
分代收集算法
根据对象存活周期的不同将内存分为几块,可以根据不同的代采取合适的垃圾回收算法
- 新生代:多采用复制算法,将小的、存活时间短的对象从Eden、Survivor From 复制到 Survivor To。安全、高效的回收新生代大量的短生命周期对象
- 老年代:存放生命周期较长的对象和大对象,因而每次只有少量非存活的对象被回收,因而在老年代采用标记清除算法或标记整理算法
垃圾回收器
垃圾回收算法是方法论,垃圾回收器是垃圾回收算法的实现
- 针对新生代的垃圾回收器:
- Serial (单线程复制算法)
- ParNew (多线程复制算法)
- Parallel Scavenge (多线程复制算法)
- 针对老年代的垃圾回收器:
- CMS (多线程标记清除算法)
- Serial Old (单线程标记整理算法)
- Parallel Old (多线程标记整理算法)
- 新一代垃圾回收器:G1 多线程标记整理算法
新生代垃圾回收器
Serial收集器:单线程复制算法
Serial串行收集器是最基本、历史最悠久的垃圾收集器。
Serial是单线程垃圾收集器,它的单线程不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是,它在进行垃圾收集工作的时候必须暂停其他所有工作线程,称为Stop the World
,直到自己把垃圾收集完了
Serial垃圾收集器采用了单线程复制算法,特点是简单、高效,对于单CPU运行环境来说,没有线程交互开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器是Java虚拟机运行在Client模式下的新生代默认垃圾收集器
JVM参数-XX:UseSerialGC
,老年代Serial Old
ParNew垃圾收集器:多线程,复制算法
- 如果 CPU 核心太少,可能比 Serial 还慢
ParNew垃圾收集器是Serial垃圾收集器的多线程实现,同样采用复制算法。它采用多线程模式工作,除此之外和Serial几乎一模一样。
ParNew垃圾收集器在垃圾收集过程中会暂停其他工作线程,是Java虚拟机在Server模式下的新生代默认垃圾收集器
ParNew垃圾收集器默认开启CPU同等数目的线程进行垃圾回收,在Java应用启动时可通过-XX:ParallelGCThresholds
参数调节ParNew垃圾收集器的工作线程数
除了Serial外,只有ParNew能和老年代垃圾收集器CMS配合使用,CMS是真正意义上的并发垃圾收集器
并行与并发
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能交替执行),用户程序仍在运行,而垃圾收集器运行在另一个CPU上
JVM参数-XX:UseParNewGC
启动ParNew收集器,只影响新生代,不影响老年代
Parallel Scavenge 收集器:多线程,复制算法
为提高新生代垃圾收集效率而设计的垃圾回收器,基于多线程复制算法实现,在
系统吞吐量
上有很大的优化。
- Parallel Scavenge 的新生代 Eden、Survivor From、Survivor To 不是卡死的 8:1:1,它支持动态自适应
Parallel Scavenge垃圾收集器通过自适应调节策略提高系统吞吐量,提供了三个参数用于调节、控制垃圾回收的停顿时间和吞吐量。
-XX:MaxGCPauseMillis
:控制最大垃圾收集停顿时间-XX:GCTimeRatio
:控制吞吐量大小UseAdaptiveSizePolicy
:是否开启自适应策略
JVM 参数: -XX:+UseParallelGC
或 -XX:+UseParallelOldGC
(可互相激活)使用 ParallelScavenge 收集器。
老年代垃圾收集器
Serial Old收集器:单线程,标记整理算法
Serial Old垃圾收集器是Serial垃圾收集器的老年代实现,同Serial一样采用单线程执行,不同的是,Serial Old针对老年代长生命周期的特点基于标记整理算法实现。
Serial Old垃圾回收器是JVM运行在Clint模式下的老年代默认垃圾回收器
新生代的Serial垃圾回收器和老年代的Serial Old垃圾回收器可搭配使用,分别针对JVM新生代和老年代进行垃圾回收。在新生代采用Serial垃圾回收器基于复制算法进行垃圾回收,未被回收的对象再老年代被Serial Old垃圾回收器基于标记整理算法回收
Parallel Old垃圾回收器:多线程,标记整理算法
Parallel Old垃圾回收器采用多线程并发进行垃圾回收,它根据老年代长生命周期的特点,基于多线程的标记整理算法实现。
Parallel Old垃圾回收器在设计上优先考虑系统吞吐量,其次考虑停顿时间等因素,如果系统对吞吐量要求较高,则可优先考虑新生代的Parallel Scavenge垃圾回收器和老年代的Parallel Old垃圾回收器配合使用。
新生代的Parallel Scavenge垃圾回收器基于复制算法进行垃圾回收,老年代的Parallel Old垃圾回收器基于标记整理算法回收垃圾
JVM参数-XX:UseParallelOldGC
:使用Parallel Old收集器,设置该参数后,新生代Parallel Scavenge + 老年代Parallel Old
CMS 垃圾回收器:并发:标记清除算法
Concurrent Mark Sweep 并发标记清除 CMS 是一种以获得最短回收停顿时间为目标的收集器
适合应用在互联网服务器或者B/S服务器上,这类应用尤其重视服务器响应速度,希望系统停顿时间最短
CMS非常适合堆内存大、CPU核心数量多的服务器端应用,也是G1收集器出现前大型应用的首选收集器
CMS收集器的工作机制相对复杂,垃圾回收过程包含如下4个步骤:
- 初始标记:只标记和GC Roots直接相关的对象,速度很快,需要暂停所有工作线程
- 并发标记:和用户线程一起工作,执行GC Roots跟踪标记过程,不需要暂停工作线程
- 重新标记:在并发标记过程中用户线程继续运行,导致在垃圾收集过程中部分对象的状态发生变化,为了确保这部分对象状态的正确性,需要对其重新标记并暂停工作线程
- 并发清除:和用户线程一起工作,执行清除GC Roots不可达对象的任务,不需要暂停工作线程
JVM 参数: -XX:+UseConcMarkSweepGC
,开启该参数后会自动将 -XX:+UseParNewGC
打开;
开启该参数后,使用 ParNew(Young区用)+CMS(Old区用)+ Serial Old 的收集器组合,Serial Old将作为CMS出错的后备收集器。
优点:
- 并发收集,停顿短
缺点:
- 并发执行,对CPU压力大
- 由于并发,CMS在收集时会增加内存的占用,因为内存空间的碎片问题可能导致总可用空间依然较大,但无法为大对象分配连续的内存空间的情况,CMS必须在老年代堆内存用尽之前尽快完成垃圾回收,
- 否则CMS回收失败,将触发担保机制,Serial Old收集器出场,Stop the World并进行一次 Full GC,进行标记整理,从而产生较大停顿时间
- 采用标记清除算法产生大量内存碎片
- 老年代空间随着应用市场增加被逐步耗尽,最后不得不进行Serial Old的GC
-XX:CMSFullGCsBeForceCompaction
(默认为0即每次进行内存整理)来指定多少次CMS收集之后,进行一次压缩Full GC
G1 Garbage First 垃圾回收器
核心思想
核心思想
仍然基于分代思想,但衡量标准不再是它属于哪个分代,而是哪块内存中存放的垃圾数量最多,回收收益最大,这就是G1收集器的Mixed GC模式
- G1 堆内存布局:G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演
新生代
的Eden空间、Survivor空间,或者老年代
空间。收集器能够对扮演不同角色的 Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的 旧对象都能获取很好的收集效果。 - G1 大对象存储:Region中还有一类特殊的Humongous区域,专门用来存储大对象。G1认为只要大小超过了一个 Region容量一半的对象即可判定为大对象。每个Region的大小可以通过参数
-XX:G1HeapRegionSize
设定,取值范围为1MB~32MB,且应为2的N次幂。而对于那些超过了整 Region容量的超级大对象, 将会被存放在N个连续的Humongous Region之中,G1的大多数行为都把Humongous Region作为老年代的一部分来进行看待 - 虽然G1仍然保留
新生代
和老年代
的概念,但新生代和老年代不再是固定的了,它们都是 一系列区域(不需要连续)的动态集合。G1收集器之所以能建立 可预测的停顿时间模型,是因为它将 Region 作为单次回收的最小单元,即每次收集到的内存空间都是 Region 大小的整数倍,这样可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集 - 垃圾 Region 优先级:思路是让G1收集器去跟踪各个 Region 里面的垃圾堆积的“价值”大小,价值即回收所获得的空间大小以及回收所需时间的经验值,然后在后台 维护一个
优先级列表
,每次根据用户设定允许的收集停顿时间(使用参数-XX:MaxGCPauseMillis
指定,默认值是200毫秒),优先处理回收价值收益最大的那些 Region,这也就是Garbage First
名字的由来
实现问题
跨 Region 引用
- 使用
Remember Set
维护卡表
- 但 G1 的 Region 使得 Remember Set 数目大增
- 同样,由于 Region 的概念,分代引用的数目也大增
- 根据经验,G1至少要耗费大约相当于Java 堆容量10%至20%的额外内存来维持收集器工作
并发可达性分析标记问题
- 不同于 CMS 的增量更新算法,G1 使用了原始快照算法
- 同样会消耗额外的堆空间,可能会失败触发 Full GC
如何建立可靠的停顿预测模型
- 用户通过
-XX:MaxGCPauseMillis
参数指定的停顿时间 只意味着垃圾收集发生之前的期望值,但G1收集器要怎么做才能满足用户的期望呢?G1收集器的停顿 预测模型是以衰减均值(Decaying Average)为理论基础来实现的,在垃圾收集过程中,G1收集器会记 录每个Region的回收耗时、每个Region记忆集里的脏卡数量等各个可测量的步骤花费的成本,并分析得 出平均值、标准偏差、置信度等统计信息。这里强调的“衰减平均值”是指它会比普通的平均值更容易 受到新数据的影响,平均值代表整体平均状态,但衰减平均值更准确地代表“最近的”平均状态。换句 话说,Region的统计状态越新越能决定其回收的价值。然后通过这些信息预测现在开始回收的话,由 哪些Region组成回收集才可以在不超过期望停顿时间的约束下获得最高的收益
G1 的大致运作过程
- 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,并且修改 TAMS 指针 (指向并发标记阶段新生成的对象) 的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际并没有额外的停顿
- 并发标记(Concurrent Marking):从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理 SATB 记录 (原始快照算法)下的在并发时有引用变动的对象
- 最终标记(Final Marking):对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少量的 SATB 记录
- 筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个 Region 构成回收集,然后把决定回收的那一部分 Region 的存活对象
复制
到空的 Region 中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动(整理)
,是必须暂停用户线程
,由多条收集器线程并行完成的
整体上采用标记整理算法,局部复制算法
优缺点
G1垃圾回收器通过内存区域独立划分使用和根据不同优先级回收各区域垃圾的机制,确保了G1垃圾收集器在优先时间内获得最高的垃圾收集效率。相对于CMS,G1有两个突出的 改进
:
- 基于标记整理算法,不产生内存碎片
- 可以精确的控制停顿时间,在不牺牲吞吐量的前提下实现最短停顿垃圾收集
缺点:
- 维护 Remember Set (基于卡表)的成本大增
- 基于快照算法的并发可达性分析修正耗费额外的堆空间,并且由于 Region 的存在,写屏障(AOP)同时做了前后切面
- 在小内存上效率不如 CMS,内存的经验临界点在
6~8GB
,大于这个范围才会相比 CMS 展现出优势
如何选择合适的垃圾回收器
1、单CPU或小内存,单机程序 -XX:+UseSerialGC
2、多CPU,需要最大吞吐量,如后台计算型应用 -XX:+UseParallelGC
或者 -XX:+UseParakkekOldGC
3、多CPU,追求低停顿时间,需要快速响应如互联网应用-XX:+UseConcMarkSweepGC
或 -XX:+ParNewGC
参数 | 新生代垃圾收集器 | 新生代算法 | 老年代垃圾收集器 | 老年代算法 |
---|---|---|---|---|
-XX:UseSerialGC | Serial | 复制 | Serial Old | 标记整理 |
-XX:UseParNewGC | ParNew | 复制 | Serial Old | 标记整理 |
-XX:UseParallelGC/-XX:UseParallelOldGC | Parallel Scavenge | 复制 | Parallel Old | 标记整理 |
-XX:UseConcMarkSweepGC | ParNew | 复制 | CMS+Seraial Old(替补) | 标记清除 |
-XX:UseG1GC | G1 整体上标记整理 | G1 局部复制算法,不会产生内存碎片 |