目录
1、Serial
2、ParNew
3、Parallel Scavenge
4、Serial Old
5、Parallel Old
6、CMS
1、为什么需要两次“stop the world”
2、CMS的并发带来的问题
3、CMS的触发时机
4、CMS的缺陷
5、为什么CMS用清除算法
7、G1
1、Region
2、设计Region的意义
3、G1的三种模式
4、Mixed GC的运行过程
5、Card Table
6、三色标记法
7、STAB
8、G1的特点
8、JDK 默认垃圾收集器
规范中并未规定垃圾收集器如何实现,所以不同的虚拟机都提供了各种收集器,让用户根据需求组合使用。
两个收集器之间存在连线,表示它们可以搭配使用。提供这么多种组合,还是因为每款收集器各有优劣,至今没有十全十美的收集器。
这是最基础的收集器,用于新生代,名字的意思是“串行”。
这是一个单线程工作的收集器,单线程指的是它只使用一条线程来进行垃圾收集。
标记-复制
算法。
它的原理和流程比较简单,但由于停止线程这个动作是虚拟机主动发起的,用户线程不可知,突然就被停掉,不是很好。
后续垃圾收集器的一大目标,就是缩短用户线程的停顿时间,但始终没有办法不停顿。
这是Serial的多线程并行版本,默认开启的收集线程数和cpu数量一样,可以同时使用多条线程进行并行的垃圾收集。
除了Serial外,只有ParNew能与CMS配合工作。CMS具有划时代意义,后来ParNew可以视为合并成了CMS的专用新生代收集器。
标记-复制
算法。
也是针对新生代,支持多线程并行收集,特点是关注点不同。
CMS等收集器的关注点是,尽可能缩短垃圾回收时用户线程的停顿时间
而Parallel Scavenge的关注点是,保证一个可控制的吞吐量
,保证用户体验。
(这里的吞吐量指的是,单位时间内(运行用户代码+进行垃圾收集),用户代码执行时间的占比)
由于关注吞吐量,所以Parallel Scavenge也被称为“吞吐量优先收集器”。
适合注重吞吐量,或者处理器资源稀缺的场景。
标记-复制
算法。
这是Serial的老年代版本,也是单线程的,使用标记-整理
算法。
这是Parallel Scavenge的老年代版本,支持多线程并发收集,使用标记-整理
算法。
CMS(Concurrent Mark Sweep)是一种以达到“最短回收停顿时间
”为目标的收集器。适合关注响应速度的服务器。
它比一般的标记-清除算法要复杂一些,分为以下4个阶段:
重新标记
:Stop The World,对标记期间产生的对象存活性的再次判断,修正对这些对象的标记,执行时间相对并发标记短。CMS是清理老年代的
。
1、每阶段做的事情
初始标记
工作模式:JDK7之前单线程,JDK8之后多线程
目的:标记存活对象
包括两部分:
此阶段会stw,为了缩短停顿时间,可以开启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled,同时调大并发标记的线程数,不过不要超过CPU的核数
并发标记
目的:顺着初始标记阶段标记出的存活对象,找出所有的存活对象
因为是和用户线程并发执行,所以期间可能发生这些动作:
对于这些对象,都需要重新标记当前的最新状态,否则就可能漏标存活对象。
为了提高重新标记的效率,避免重新扫描整个老年代,此阶段在发现上述行为后,会把该对象所在的card标记为脏卡,后续只需要扫描所有脏卡来处理。
因此,并发标记阶段完成后,老年代的所有存活对象并不会都被标记,还有一部分以脏卡的形式被记录,等待后续处理。
预清理阶段
目的:扫描所有脏卡,检查脏卡内所有对象的引用关系,标记存活对象
可终止的预处理
目的:这个阶段去尝试进行重新标记阶段的工作,因为重新标记阶段会stw,这样能减少一些停顿时间
此阶段最大持续时间为5秒,因为它期待这5秒内能发生一次young gc,清理新生代,进而减少下阶段扫描跨代引用的时间
重新标记
目的:完成对整个老年代的所有存活对象的标记,stw
用到三色标记里的增量更新算法做重新标记
调优:
这个阶段虽然目的是标记老年代,但是需要扫描整个堆,因为新生代可能存在对老年代的跨代引用。
为了以高效率,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前进行一次young gc。
这样,只需要扫描较小的新生代,大大提高效率。
并发清理
在“初始标记”阶段,CMS会快速扫描一下能和GC Roots直接关联到的对象,之后就会解除暂停。
之后和用户线程并发执行,进行对象的可达性分析。
但是,在用户线程执行的过程中,引用关系很可能产生了变化,于是就再次stop the world,修改这些引用发生变化的对象的标记。
比如,一个对象在第一次被判断为了“死亡”,之后用户线程又重新与它建立了引用关系,那么第二次会将它修改为“存活”。
注意:未被“初始标记”阶段标记的对象,在“重新标记”阶段不会被标记为垃圾对象。
这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。
由于整个过程中耗时最长的“并发标记”和“并发清除”过程,收集器线程都可以与用户线程一起工作。所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点:并发收集、低停顿
在并发阶段,它虽然不会导致用户线程停顿,但是占用了一部分CPU的运行资源,导致应用程序变慢了,降低总吞吐量。
CMS默认启动的回收线程数是(处理器核心数 + 3)/4。当处理器核心不足4个时,CMS对用户程序的影响较大。
CMS收集器不能像其他收集器那样,等待老年代几乎满了才进行收集。这是因为CMS需要预留足够的内存给用户线程使用
。
默认的策略是,老年代使用了68%的空间后,就会开始回收。
浮动垃圾
”,可能导致这次GC没有产生足够的空间,不得不触发一次Full GC标记-清除
算法,会产生大量的空间碎片
。如果影响到对象分配,就不得不触发一次Full GC来整理内存。什么是浮动垃圾
在“初始标记”第一次判断时,该对象不是垃圾。但到“重新标记”第二次判断的期间,这个对象变为了垃圾,那么本次垃圾回收就无法处理它,只能等到下一次GC时才有机会将它回收,这种对象就是浮动垃圾。
因为CMS考虑的是,尽量减少垃圾收集让用户线程停顿的时间,但是工作量无法减少,那么就考虑在某些阶段,让用户线程和垃圾收集并发执行。
正因如此,在用户线程正常执行时,CMS不能去擅自修改任何对象的地址,否则会导致用户线程无法定位到对象。
复制算法和整理算法都需要改变对象的内存地址,所以不适合。
G1是一款面向服务器的高性能垃圾收集器,主要针对具有多核处理器和大内存的机器。
JDK 1.9时,G1作为了默认的垃圾收集器,同时CMS被标记为“不推荐使用”。
G1在以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。
G1堆内存的布局和其他收集器不同。
G1不再进行固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分成多个大小相等的独立区域(Region)
。
region 的大小是一致的,数值是在 1M 到 32M 字节之间的一个 2 的幂值数,JVM 会尽量划分 2048 个左右的 region。
每个Region都可以根据需要,扮演Eden、Survivor或者老年代空间。
G1可以对扮演不同角色的Region采用不同的收集策略,这样无论是新创建的对象,还是年龄较大的对象,都能获得很好的收集效果。
Region中还有一类特殊的Humongous区域,专门存储大对象。
如果一个对象超过Region容量的50%,它就会被存放在N个连续的Humongous Region中,G1的大多数行为都会把它当做老年代的一部分来看待。
由于堆内存被零散拆分了,所以需要维护一个空闲列表,来记录所有可用的region。
G1中依然存在新生代、老年代的概念,但它们不是连续的,而是一系列Region的动态集合。
G1能建立起可预测的停顿时间模型的基础,就是选择将Region作为单次回收的最小单元。
具体的做法是,它会根据各个Region里面的垃圾堆积的“价值”,维护一个优先级列表。价值包括两个方面:
每次垃圾回收时,根据用户指定的允许停顿时间,优先回收那些价值大的Region,保证了有限时间内的较高效率
。
相当于垃圾回收的思路转变了:
Young GC
当所有eden region被耗尽无法申请内存时,就会触发一次young gc。会暂停用户线程,发起多个垃圾回收线程。
存活的对象会被拷贝到survivor region,或者晋升到old region中。被清理的region会被放入空闲列表中,等待下次被使用
Mixed GC
之前的所有垃圾收集器,都是要么针对新生代,要么针对老年代,要么针对整堆进行垃圾回收的。
当越来越多的对象晋升到old region中,达到设定的阈值后,就会触发一次Mixed GC,回收掉高价值的目标
Full GC
如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次full gc
G1的full gc算法就是单线程执行的serial old gc,会导致用户线程被长时间暂停,所以需要避免Full GC。
初始标记:
并发标记:从GC Roots出发,对堆中对象进行可达性分析。和用户线程一起并发执行(只有并发标记阶段能和用户线程并发执行)
最终标记
:stop the world,处理并发阶段结束后遗留的STAB记录
筛选回收
:
在传统垃圾回收中遇到的跨代引用问题,G1中也同样存在。
由于G1把整个堆拆成了很多个region,所以每个region的不同对象之间是有互相引用的依赖关系的,而且同代引用也会发生在不同region之间。
如果进行回收之前需要遍历所有region来做到准确的垃圾回收,效率极低。
G1也是采用了Remember Set记忆集的思路,做了一个Card Table。
卡表中存放了各个Region之间的引用关系,这样就可以只去扫描相关的Region,不需要全体扫描。
G1和CMS一样,在并发标记阶段使用了三色标记法:
漏标问题
正常的引用关系是:
CMS和G1如何解决漏标问题
产生漏标问题的条件有两个:
所以要解决漏标问题,打破两个条件之一即可。
为什么G1不使用增量更新机制
因为如果把黑色节点标记为灰色节点,之后还要二次查找,效率低。
G1保存了卡表,里面存放了各个Region之间的引用关系。
G1使用原始快照(STAB)算法来解决,Snapshot-At-The-Beginning。
具体做法是:
要达到GC与用户线程并发运行,必须要解决回收过程中新对象的分配,所以G1为每一个Region区域设计了两个名为TAMS(Top at Mark Start)的指针,从Region区域划出一部分空间用于记录并发回收过程中的新对象。这样的对象认为它们是存活的,不纳入垃圾回收范围。
从整体来看是基于“标记整理”算法
实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法
实现的,所以不会产生内存碎片
。除了追求低停顿外,还能建立可预测的停顿时间模型
,能让使用者明确指定允许的停顿时间。jdk1.7 默认垃圾收集器:Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.8 默认垃圾收集器:Parallel Scavenge(新生代)+Parallel Old(老年代)
jdk1.9 默认垃圾收集器:G1