GC(Garbage Collection),指的是垃圾收集,本文主要介绍了JVM中GC的一些算法以及相关GC收集器。
本文主要介绍了如下三个方面:
- 如何确定内存中的对象是‘‘垃圾’’
- GC回收算法
- GC收集器
本文相关虚拟机为HotSpot虚拟机
一、如何确定内存中的对象是‘‘垃圾’’
判断对象是否为‘‘垃圾’’,即判断对象是否存活,通常来讲,主要有两种办法:
1、引用计数判断:
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,效率较高,但是无法解决对象相互循环引用的问题。
2、可达性分析(Reachability Analysis):
通过一系列的称为GC Roots的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。不可达对象。
贴一张csdn上copy来的图
上图中的obj567虽然互相有关联,但是他们到GC Roots是不可达的,所以它们会被判定为可回收的对象。
在Java语言中,可作为GC Roots的对象包括下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
在介绍gc之前,先贴一张jvm内存模型,便于理解
顺便再介绍一下 Stop-The-World
Stop-the-world会在任何一种GC算法中发生。Stop-the-world意味着 JVM 因为要执行GC而停止了应用程序的执行。当Stop-the-world发生时,除了GC所需的线程以外,所有线程都处于等待状态,直到GC任务完成。 GC优化很多时候就是指减少Stop-the-world发生的时间。
二、GC相关算法
1、标记-清除(Mark-Sweep)算法
最基础的收集算法,分为两个阶段:标记和清除。
首先标记出所有需要回收的对象,在标记完成后统一清除所有被标记的对象。
主要缺点:一个是效率问题,标记和清除两个过程效率都不高;另外一个是空间问题,标记清除后会产生大量不连续的内存碎片,空间碎片太多后可能会导致以后在程序运行过程中需要分配较大对象时候,无法找到足够的连续内存而不得不提前触发另一次GC操作。
2、复制(Cpoying)算法
复制算法将内存空间划分成大小相等的两块,每次只使用其中的一块,当这一块的内存用完了,就将还存活着的对象复制到另一块内存上面,然后把之前已经使用过的空间一次清理掉。这样使得每次都是对真个半区进行内存回收,内存分配的时候也不用考虑内存碎片等问题
实现简单,运行高效,缺点是浪费了一半的内存空间,而且存活对象越多,效率越差。
现在绝大多数JVM都采用这种算法来回收新生代,在老年代一般不能直接选用这种算法。
3、标记-整理(Mark-Compact)算法
标记-整理算法的标记过程,和上面标记-清除算法的标记一样,但是后续步骤不是直接回收对象,而是让所有存货的对象都向一端移动,接着直接清理掉端边界以外的内存。
4、分代收集算法
分代收集算法是目前大部分JVM的GC算法。
GC分代的基本假设:绝大部分对象的生命周期都非常短暂,存活时间短。
核心思想:根据对象存活的生命周期,将内存划分成若干个不同的区域
将堆Heap划分成:Young 和 Tenured
在新生代(young)中,每次进行gc都有大量的对象需要回收,而在老年代(Tenured)中则是每次gc回收的时候只收集少量的对象,老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
目前,大部分GC收集器对Young都采取Copy算法。
Heap中分为Eden区和Survivor区,其中,Survivor区又分为:Survivor1和Survivor2。
进行GC的时候,首先把Eden和Survivor1中的存活的对象copy到Survivor2中,然后清理掉Eden和
Survivor1这两块的空间,下一次GC同样,Eden,Survivor2 -> Survivor1,清理掉Survivor2,如此循环。
在以上的步骤中重复几次依然存活的对象,就会被移动到老年代。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。JDK7默认是15,熬过15次后就晋升到了老年代,关于jvm中相关对象的晋升的参考这里。
老年代Tenured一般采用Mark-Compact算法。
Minor GC 和 Full GC
- 新生代GC(Minor GC / Young GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。
- 老年代GC(Major GC ):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
- Full GC 是清理整个堆空间—包括年轻代和老年代
三、GC收集器
GC收集器其实就是GC算法的具体实现
本文讨论的收集器基于JDK1.7 Update 14 之后的HotSpot虚拟机
图中展示了7中做用于不同分代的收集器,如果两个收集器之前存在连线,说明它们可以互相搭配使用,虚拟机所处的区域,表示他们是属于新生代收集器还是老年代收集器。
3.1、Serial收集器
Serial收集器是最基本的收集器,在jdk1.3.1之前是虚拟机新生代收集的唯一选择。
基本原理:单线程,只用1个线程去进行回收,在进行回收的同时,必须暂停其他所有工作线程,直到它收集结束,产生stop-the-world时间较长。
新生代、老年代使用串行回收
新生代:复制算法,老年代(Serial Old收集器):标记-整理算法
参数控制:-XX:+UseSerialGC 串行收集器
3.2、Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。
作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
3.3、ParNew收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。
新生代并行,老年代串行;
新生代复制算法、老年代标记-整理算法
ParNew收集器的工作过程如下图(老年代采用Serial Old收集器):
ParNew收集器除了使用多线程收集外,其他与Serial收集器相比并无太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作
ParNew 收集器在单CPU的环境中不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用-XX:ParallerGCThreads参数设置
从ParNew收集器开始,先解释一下两个名词:并发和并行
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。
3.4、Parallel Scavenge收集器
Parallel Scavenge收集器类似ParNew收集器,是一个并行的多线程新生代收集器,它也使用复制算法。Parallel收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制GC的时间不大于多少毫秒或者比例;
-XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为GC自适应的调节策略(GC Ergonomics)
自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
新生代:复制算法。老年代(Parallel Old 收集器):标记-整理
参数控制:-XX:+UseParallelGC 使用Parallel收集器+ 老年代串行
3.5、Parallel Old 收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。前面已经提到过,这个收集器是在JDK 1.6中才开始提供的。
参数控制: -XX:+UseParallelOldGC 使用Parallel收集器+ 老年代并行
Parallel Scavenge/Parallel Old收集器配合使用的流程图
3.6、CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字上(“Mark Sweep”)就可以看出它是基于“标记-清除”算法实现的。
CMS收集器工作的整个流程分为以下4个步骤:
- 初始标记(CMS initial mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
- 并发标记(CMS concurrent mark):进行GC Roots Tracing的过程,在整个过程中耗时最长。
- 重新标记(CMS remark):为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”。
- 并发清除(CMS concurrent sweep)
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通过下图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间:
优点:并发收集、低停顿
缺点:
- 对CPU资源非常敏感 其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但会因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4个时(比如2个),CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了50%,其实也让人无法接受。
- 无法处理浮动垃圾(Floating Garbage) 可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK5的默认设置中,CMS收集器当老年代使用了68%的空间后就会被激活,可以使用参数控制:-XX:CMSInitiatingOccupancyFraction的值来提高百分比,JDK6中已经提高到92%,如果出现Concurrent Mode Failure,则虚拟机会启动备用方案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿的时间就很长了,所以说参CMSInitiatingOccupancyFraction设置的太高容易导致大量的“Concurrent Mode Failure”失败,导致性能降低。
- 标记-清除算法导致的空间碎片 CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,cms收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于在cms收集器顶不住要进行Full GC的时候开启内存碎片的合并整理过程,空间碎片问题是解决了,但是会带来另外一个问题,停顿时间又双叒叕变长了。。。。于是又提供了另外一个参数:-XX:+CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,进行一次带压缩的(默认是0,即每次进入Full GC都进行压缩)。
参数控制:
-XX:+UseConcMarkSweepGC :使用CMS收集器
-XX:+UseCMSCompactAtFullCollection :Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理,默认0
-XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)
3.7、G1收集器
G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,G1具备如下特点:
- 并行与并发 G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
- 分代收集 与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
- 空间整合 G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
- 可预测的停顿 这是G1相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
对G1 不是很了解,还在学习中。。。。
总结
本文主要介绍了4种gc算法和6种垃圾收集器
把垃圾收集器按照作用于新生代和老年代来划分的话,
作用于新生代的垃圾收集器有:
- Serial 收集器:复制算法
- ParNew 收集器:复制算法
- Parallel Scavenge 收集器:复制算法
作用于老年代的垃圾收集器有:
- Serial Old 收集器:标记-整理
- Parallel Old 收集器:标记-整理
- CMS收集器:标记-清除
还是表格来的比较清晰
收集器 | 串行、并行or并发 | 新生代/老年代 | 算法 | 目标 | 适用场景 |
---|---|---|---|---|---|
Serial | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单CPU环境下的Client模式 |
Serial Old | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单CPU环境下的Client模式、CMS的后备预案 |
ParNew | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多CPU环境时在Server模式下与CMS配合 |
Parallel Scavenge | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
Parallel Old | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
CMS | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或B/S系统服务端上的Java应用 |
G1 | 并发 | both | 标记-整理+复制算法 | 响应速度优先 | 面向服务端应用,替换CMS |
参考资料: