JVM垃圾收集器
概要
垃圾回收算法是内存回收的抽象策略,而垃圾收集器是内存回收的具体实现。JVM规范对于垃圾收集器应该如何实现没有任何规定,因此不同厂商,不同版本的虚拟机所提供的垃圾收集器差别较大,这里只看HotSpot虚拟机。
垃圾收集器的分类
如果两个收集器之间存在连线,则说明它们可以搭配使用。虚拟机所处的区域则表示它是属于新生代还是老年代收集器。
1、新生代的收集器:
Serial
PraNew
Parallel Scavenge
2、老年代的收集器:
CMS
Serial Old
Parallel Old
3、回收整个Java堆(新生代和老年代):G1收集器
Serial 收集器
Serial(串行)收集器是最基本,发展历史最悠久的收集器,曾是JDK 1.3.1之前虚拟机新生代收集的唯一选择。Serial收集器是一个单线程的收集器。“单线程”的意义不仅仅是它只会使用一个CPU或一条垃圾收集线程去完成垃圾收集工作,更重要的是它在垃圾收集的时候必须暂停其他所有的工作线程(“Stop The World”:将用户正常工作的线程全部暂停掉),直到它收集结束。
(1)优点
当它进行GC工作的时候,虽然会造成Stop-The-World,但是因为它简单高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,没有线程交互的开销,专心做GC,自然可以获得最高的单线程效率。所以Serial收集器对于运行在Client模式下的应用是一个很好的选择。是虚拟机运行在client模式下的默认新生代收集器。
(2)缺点
串行收集器的缺点就是当在进行GC时,需要暂停其他工作线程,所以虚拟机的开发者也在一直缩减stop the world 的时间。
(3)特点
- 针对新生代的收集器
- 采用复制算法
- 单线程收集
- 进行垃圾收集时,必须暂停所有工作线程,直到完成,即会“Stop The World”
- Serial收集器是HotSpot虚拟机运行在Client模式下的默认新生代收集器
"-XX:+UseSerialGC" :添加该参数来显式的使用串行垃圾收集器
ParNew收集器
ParNew收集器是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为都与Serial收集器一样,包括:可用的所有控制参数,收集算法,Stop The World,对象分配规则,回收策略等。
(1)特点
ParNew收集器是许多运行在Server模式下的虚拟机首选的新生代收集器,其中一个原因是:除了Serial收集器之外,目前只有ParNew收集器能与CMS收集器配合工作。
"-XX:+UseConcMarkSweepGC" :指定使用CMS后,会默认使用ParNew作为新生代收集
"-XX:+UseParNewGC" :强制指定使用ParNew
"-XX:ParallelGCThreads" :指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同
并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能是交替执行),用户线程继续工作,而垃圾收集程序运行在另一个CPU上。
补充:为什么ParNew能与CMS收集器配合使用?
- CMS是HotSpot在JDK 1.5推出的第一款真正意义上的并发(Concurrent)收集器,第一次实现了让垃圾收集线程与用户线程(基本上)同时工作
- CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作。因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而另外独立实现;而其余几种收集器则共用了部分的框架代码
Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器。
(1)特点
- 使用复制算法
- 并行的多线程收集器
- 关注吞吐量
Parallel Scavenge收集器关注点是达到一个可控制的吞吐量。(如果高效率的利用CPU)
CMS等垃圾收集器关注点在尽可能的缩短垃圾收集时用户线程的停顿时间。(提高用户体验)
所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间的比值。(吞吐量=运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间))比如,虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那么吞吐量就是99%。
Parallel Scavenge 收集器提供了两个参数来用于精确控制吞吐量:
1、控制最大垃圾收集停顿时间: -XX:MaxGCPauseMillis 参数
2、控制吞吐量大小:-XX:GCTimeRatio 参数
“-XX:MaxGCPauseMillis” 参数允许的值是一个大于0的毫秒数,收集器将尽可能的保证内存垃圾回收花费的时间不超过设定的值(但是,并不是越小越好,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的,如果设置的值太小,将会导致频繁GC,这样虽然GC停顿时间下来了,但是吞吐量也下来了)。
“-XX:GCTimeRatio”参数的值是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,默认值是99,就是允许最大1%(即1/(1+99))的垃圾收集时间。
“-XX:UseAdaptiveSizePolicy”参数,如果这个参数打开之后,虚拟机会根据当前系统运行情况收集监控信息,动态调整新生代的比例、老年大大小等细节参数,以提供最合适的停顿时间或最大的吞吐量,这种调节方式称为GC自适应的调节策略。
Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本。
(1)特点
- 单线程收集器
- 采用标记-整理算法
(2)应用场景
- 在JDK1.5 以及之前的版本中与Parallel Scavenge 收集器搭配使用
- 作为CMS收集器的后备方案,在并发收集Concurrent Mode Failure 时使用
Parallel Old收集器
Parallel Old收集器是Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理算法”,在JDK 1.6中才出现。
(1)应用场景
注重高吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge + Parallel Old收集器。
CMS收集器
CMS(并行的标记垃圾收集器)收集器是一种以获取最短回收停顿时间为目标的收集器。
(1)特点
基于标记-清除算法实现,并发收集,低停顿。
(2)应用场景
适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下,如Web程序,或B/S服务。
(3)CMS收集器运行过程
- 初始标记:标记GC Roots能直接到的对象。速度很快但是仍然存在Stop The World 问题。
- 并发标记:进行GC Roots Tracing 的过程,找到存活对象且用户线程可并发执行。
- 重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在Stop The World 问题
- 并发清除:对标记的对象进行清除回收。
CMS收集器运行的整个过程中,最耗费时间的并发标记和并发清除过程收集器线程和用户线程是一起工作的,所以总体来说,CMS收集器的内存回收过程和用户线程是一起并发执行的。
(4)优点
- 并发收集
- 低停顿
(5)缺点
- CMS收集器对CPU资源非常敏感。虽然在两个并发阶段不会导致用户线程停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量下降。CMS默认启动的回收线程数量是(CPU数量+3)/4。
- CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC产生。由于CMS并发清除阶段用户线程还在运行,伴随着程序运行还会产生新的垃圾,这一部分垃圾出现在标记之后,CMS无法在当次收集中处理掉它们,只能留到下次再清理,这一部分垃圾称为“浮动垃圾”。也正是由于垃圾收集阶段用户线程还在运行,那么也就需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等待老年代填满之后再进行收集,需要预留一部分空间给并发收集时用户程序使用。可以通过“-XX:CMSInitingOccupancyFraction”参数设置老年代内存使用达到多少时启动收集。
- 因为采用标记-清除算法所以会存空间碎片的问题,导致大对象无法分配空间,不得不提前触发一次Full GC。
G1收集器
G1 收集器是当今收集器技术发展最前沿成果之一。
(1)相比其他收集器,具有如下特点:
- 并行与并发:G1能够重复利用多CPU,多核环境下的优势,使用多个CPU来缩短Stop The World停顿时间。
- 分代收集:与其他收集器一样,也是采用分代收集
- 空间整合:与CMS的“标记-清除”算法不同,G1从整体来看是基于“标记-整理”来实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能够提供整体的可用内存。
- 可预测停顿:G1除了追求低停顿之外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。
(2)G1收集器的运作大致可分为:
-
初始标记:仅标记GC Roots能直接到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象。(需要线程停顿,但耗时很短。)
-
并发标记:从GC Roots开始对堆中对象进行可达性分析,找出存活对象。(耗时较长,但可与用户程序并发执行)
-
最终标记:为了修正在并发标记期间因用户程序执行而导致标记产生变化的那一部分标记记录。且对象的变化记录在线程Remembered Set Logs里面,把Remembered Set Logs里面的数据合并到Remembered Set中。(需要线程停顿,但可并行执行。)
-
筛选回收:对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。(可并发执行)
使用G1收集器时,Java堆的内存布局与其他收集器有很大的区别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留着新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。
G1收集器之所以能够建立可预测的停顿时间模型,是因为它可以有计划的避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也是Garbage - First 名称的由来)。
小结
就像没有最好的算法一样,垃圾收集器也没有最好,只有最合适。实际应用过程中还是需要根据具体的应用场景选择合适的垃圾收集器。