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