收集算法是内存回收的方法论,那么收集器就是收集算法的实现。
下图展示了7种作用于不同分代的收集器,如果收集器之间存在连线,就说明他们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
Serial收集器-新生代收集器
Serial收集器是最基本、最悠久的收集器。这个收集器是一个单线程收集器,在它进行垃圾收集时,必须停掉所有其他的工作线程,然后以一条收集线程进行垃圾收集,直到收集工作结束,才可以恢复其他工作线程。这对于许多应用是难以接受的。但是对Client(客户端)模式的虚拟机来说,Serial收集器是一个不错的选择,因为在桌面端应用,分配给虚拟机的内存不会太大,收集几十兆到几百兆的新生代内存停顿时间完全可以控制在几十毫秒。
参数控制:-XX:+UseSerialGC
串行收集器
ParNew收集器-新生代收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多协调线程进行垃圾收集外,其余的Serial收集器完全一样。ParNew收集器在单CPU或CPU数量少的环境中性能不会有比Serial收集器更好的结果,但是随着CPU数量的增多,它GC时对CPU资源的的有效利用还是很有好处的,所以它是许多运行在Server模式下的虚拟机的首先新生代收集器。
参数控制:
-XX:+UseParNewGC
ParNew收集器
-XX:ParallelGCThreads
限制线程数量
Parallel Scavenge收集器-新生代收集器
它看上去似乎与ParNew一样,但是它的目标是达到一个可控制的吞吐量(吞吐量 = 运行用户代码时间 / (运行用户代码时间 + GC时间))。停顿时间越短,就越适合与用户交互的程序,因为良好的响应时间可以提高用户的体验,而吞吐量则可以高效利用CPU时间尽快完成程序的计算任务,主要适合在后台运算而需要交互任务。
Parallel Old收集器-老年代收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供
参数控制: -XX:+UseParallelOldGC
使用Parallel收集器+ 老年代并行
CMS收集器-老年代收集器
CMS(Concurrent Mark Sweep)
收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
从名字(包含Mark Sweep
)上就可以看出CMS收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:
- 1、初始标记(
CMS initial mark
):仅仅只是标记一下GC Roots
能直接关联到的对象,速度很快,需要停顿。 - 2、并发标记(
CMS concurrent mark
):进行GC Roots Tracing
的过程,它在整个回收过程中耗时最长,不需要停顿。 - 3、重新标记(
CMS remark
):为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 - 4、并发清除(
CMS concurrent sweep)
:不需要停顿。
由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用ParNew
)
优点: 并发收集、低停顿
缺点: 产生大量空间碎片、并发阶段会降低吞吐量
参数控制:
-XX:+UseConcMarkSweepGC
使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection
Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
-XX:+CMSFullGCsBeforeCompaction
设置进行几次Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads
设定CMS的线程数量(一般情况约等于可用CPU数量)
G1收集器
G1是目前技术发展的最前沿成果之一,是面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有一下特点:
- 1、并行与并发:能充分利用CPU环境下的硬件优势,使用多个CPU来缩短停顿的时间。
- 2、分代收集:虽然它不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。
- 3、空间整合:整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测停顿:这是它相对CMS的一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在GC上的时间不能超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老生代,而 G1 不再是这样,Java 堆的内存布局与其他收集器有很大区别,将整个 Java 堆划分为多个大小相等的独立区域(Region
)。虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分Region
(不需要连续)的集合。
之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。它跟踪各个 Region
里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region
(这也就是 Garbage-First
名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了它在有限的时间内可以获取尽可能高的收集效率。
Region
不可能是孤立的,一个对象分配在某个 Region
中,可以与整个 Java 堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个 Java 堆才能保证准确性,这显然是对 GC 效率的极大伤害。为了避免全堆扫描的发生,每个Region
都维护了一个与之对应的 Remembered Set
。虚拟机发现程序在对Reference
类型的数据进行写操作时,会产生一个Write Barrier
暂时中断写操作,检查 Reference
引用的对象是否处于不同的 Region
之中,如果是,便通过 CardTable
把相关引用信息记录到被引用对象所属的 Region
的Remembered Set
之中。当进行内存回收时,在 GC 根节点的枚举范围中加入Remembered Set
即可保证不对全堆扫描也不会有遗漏。
七种垃圾收集器的比较
内存分配与回收策略
对象的内存分配,也就是在堆上分配。主要分配在新生代的Eden区上,少数情况下也可以直接分配在老年代中。
1.优先在Eden分配
大多数情况下,对象在新生代Eden区分配,当Eden区间不够时,发起Minor GC
。
从新生代(包括Eden和Survivor区域)回收内存被称为
Minor GC
。清理整个堆空间包括新生代和老年代被称为
Full GC
。
关于Minor GC
和Full GC
:
-
Minor GC
:发生在新生代上,因为新生代对象存活时间很短,因此Minor GC
会频繁执行,执行的速度一般也会比较快。 -
Full GC
:发生在老年代上,老年代对象和新生代对象相反,其存活时间很长,因此Full GC
很少执行,而且执行速度会比Minor GC
慢很多。
2.大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
3.长期存活的对象进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC
后仍然存活,并且能被Survivor
区容纳的话,将被移动到Survivor
空间中,并且对象年龄设为1。对象在Survivor
区中每“熬过”一次Minor GC
,年龄就增加1岁,当它的年龄增加到一定程度(默认15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold
设置
4.动态对象年龄判定
为了能更好的适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold
才能晋升老年代,如果在Survivor
空间中相同年龄所有对象大小的总和大于Survivor
空间的一半,年龄大于或者等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold
中要求的年龄。
5.空间分配担保
在发生 Minor GC
之前,JVM 先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么Minor GC
可以确认是安全的;如果不成立的话 JVM 会查看 HandlePromotionFailure
设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC
,尽管这次 Minor GC
是有风险的;如果小于,或者 HandlePromotionFailure
设置不允许冒险,那这时也要改为进行一次 Full GC
。
参考资料
https://github.com/Michaeljian/Interview-Notebook/blob/master/notes/JVM.md