jvm特性(3)( 收集算法和收集器的概念)
二. 垃圾收集器算法实现
Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、版本的虚拟机所提供的垃圾收集器都可能会有很大差别,
并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现
1. Java堆内存结构
接下来讨论的收集器基于JDK1.7 Update 14 之后的HotSpot虚拟机(在此版本中正式提供了商用的G1收集器,之前G1仍处于实验状态),
但是在正式讨论收集器之前,需要介绍一下Java堆内存结构—— 适用于非G1收集器外的垃圾收集器:
根据Java对象的生命周期长短把Java堆内存分为老年代和年轻代,然后年轻代又被划分为:一个Eden区和两个大小同等的Survivor区。
2. 垃圾收集器
了解Java堆内存中的大致划分,因为随后讲解HotSpot虚拟机中的各个收集器就是存在于此内存中
虚拟机中所包含所有的收集器:
如上图所示,展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。
虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
- 虽然在对各个收集器进行比较,但并非是为了挑出一个最好的收集器。
- 因为目前并无完美的收集器出现,只是选择对具体应用最适合的收集器。
- 若真有完美收集器的存在,HotSpot虚拟机就没必要如上图所示实现那么多的收集器了 。
在后续几个收集器的介绍中,涉及到了并发和并行的收集器:
- 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent): 指用户线程与垃圾收集线程同时执行(并行or交替执行),用户程序在继续执行,而垃圾收集程序运行在另一个CPU上。
2.1 Serial 收集器
(1)特征
Serial收集器是最基本、发展历史最悠久的收集器,曾经(JDK1.3之前)是虚拟机新生代收集的唯一选择。
- 它是一个单线程收集器,但“单线程”并非指该收集器只会使用一个CPU或一条收集线程去完成垃圾收集工作,
- 更重要的是它在进行垃圾收集时,必须暂停其他所有的工作线程,直至Serial收集器收集结束为止。
这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉
(2)图解运行过程
以下是Serial / Serial Old 收集器的运行过程:
(3)优势
也许你认为Serial收集器这种工作方式根本没有存在的必要,其实不尽然,它依然是虚拟机运行在Client模式下的默认新生代收集器。
优于其他收集器的地方:
- 简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,
- Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。
需要注意:从Serial收集器到Parallel收集器,再到最前沿成功的G1收集器,用户线程的停顿时间越来越少,但是仍无法完全消除。
(4)适用场景
- 其实实际情况并没有那么糟糕,在用户的桌面应用场景中,分配给虚拟机管理的内存一般不会很大,
- 收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本不会再大了),停顿时间完全可以控制在几十毫秒最多一百毫秒以内,
- 只要不频繁发生,这点停顿时间可以接收。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。
2.2 Serial Old 收集器
(1)特征
Serial Old 是 Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。
(该收集器的运行过程同Serial收集器的相同)
(2)适用场景
此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途:
- 在JDK1.5 以及之前版本中与Parallel Scavenge收集器搭配使用。
- 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。
2.3 ParNew 收集器
(1)特征
在理解完Serial收集器后,得知了它的一个明显的特征——单线程。
ParNew收集器就是它的多线程版本,除了此特征其余地方与Serial收集器相同,两者共用了相当多的代码。
(2)图解运行过程
(3)不同使用场景中的优势与缺陷
单CPU的环境
- ParNew 收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,
- 该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越。
多CPU下的环境
- 随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。
- 它默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用参数设置。
2.4 Parallel Scavenge 收集器
(1)特征
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法,并且是一个并行的多线程收集器,看上去与ParNew收集器类似,
其实此收集器的特点是它的关注点与其他收集器不同:
- 在讲述第一个Serial Old收集器时已经提过,后续产生的收集器它们都是为了尽可能缩短垃圾收集时用户线程的停顿时间,
- 而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。
(2)吞吐量(Throughput)
吞吐量就是CPU用于运行用户代码的时间与CPU总消耗的比值,运算公式为:
吞吐量 = 用户运行时间 / (运行用户代码时间 + 垃圾收集时间)
举个例子说明:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
(3)以下是Parallel Scavenge / Parallel Old 收集器的运行过程:
(4)适用场景
- 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。
- 而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
PS:吞吐量和停顿时间不是一样吗,停顿少吞吐量肯定大呀。
(停顿少,可以gc采用并行。总体时间是变长的;吞吐量大,可(应用和gc)并发,但单个执行延长/缓慢。总体时间是变短的)
2.5 Parallel Old 收集器
(1)特征
Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
(此收集器的运行过程与Parallel Scavenge收集器相同)
(2)Parallel Old 产生之前的收集器组合问题
结构图中的收集器不要忘记这些收集器之间的连线,代表这些收集器可以互相搭配使用,也是很重要的一点。接下来要讲的也与这个收集器组合有关。
此收集器在JDK1.6 中才出现,在此之前,Parallel Scavenge收集器处于一个很尴尬的位置,原因如下:
- 如果新生代选择了Parallel Scavenge收集器,那么老年代除了Serial Old收集器外别无选择,
- 可是Serial Old收集器在服务端应用性能上的效果不太理想,而导致Parallel Scavenge收集器在整体应用上无法获得吞吐量最大化的效果。
- 由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬件比较高级的环境中,此组合还不如Parallel Scavenge搭配CMS收集器。
(3)Parallel Old的产生解决组合问题
- 可是在Parallel Old收集器 出现后,“吞吐量优先”收集器终于有了比较理想的应用组合,
- 在注重吞吐量及CPU资源敏感的场合,可以有限考虑Parallel Scavenge搭配Parallel Old收集器。
2.6 CMS(Concurrent Mark Sweep) 收集器
(1)特征
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。该收集器名字中的“Mark Sweep”已经体现出它是基于“标记-清除”算法实现的。
(2)解析运行过程
此收集器的运作过程相对于前面几种较为复杂,整个过程分为4个步骤:
1,初始标记 (CMS initial mark) 仅仅只是标记一下GC Roots 能直接关联到的对象,速度很快。 2,并发标记(CMS concurrent mark) 该阶段进行 GC Root Tracing的过程。 3,重新标记(CMS remark) 修改并发标记期间因用户程序继续运作而导致标记产生变动的那部分对象的标记记录。 此阶段停顿时间会比初始标记阶段稍长一些,但远比并发标记时间短。 4,并发清除(CMS concurrent sweep)
- 这4个步骤中的 初始标记、重新标记 过程仍需要停止所有工作线程。
- 由于整个过程中耗时最长的并发标记 和 并发清除过程收集器线程都可以与用户线程一起工作,
所以总体而言,CMS收集器的内存回收过程是与用户线程一起并发执行的。
(3)以下是CMS 收集器的运行过程:
(4)优点与缺点
主要优点:并发收集、低停顿,但是远远不到完美程度,不可避免的有以下3个缺点:
1,并发导致总吞吐量降低 CMS收集器对CPU资源非常敏感,实际上,面向并发设计的程序都对CPU资源比较敏感。 在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说是CPU资源)而导致应用程序变慢,总吞吐量会降低。 2,无法处理浮动垃圾导致 Full GC 由于CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。 由于CMS并发清理阶段用户线程还在运行,伴随程序运行自然会有新的垃圾不断生成,
此部分垃圾出现在标记之后,CMS无法再当次收集中处理,只好留给下一次GC时再清理,此部分的垃圾称为“浮动垃圾”。 3,本质算法导致大量空间碎片 CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。
空间碎片过多时,将会给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象。
(5)适用场景
目前很大一部分的Java应用集中在互联网站或B/S系统的服务端上,
这类应用尤其重视服务的响应速度,希望系统停顿时间最短,来给用户带来较好体验,而CMS收集器最适合这类需求。
2.7 G1(Garbage-First) 收集器
(1)特征
G1收集器是最前沿的成果之一,未来可以替换JDK 1.5中发布的CMS收集器,特点如下:
1,并行与并发:
G1 能充分利用多CPU、多核环境下的硬件优势,使用多个CPU来缩短“Stop The World”停顿时间,
部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。 2,分代收集:
虽然G1不可不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次GC的旧对象来获取更好的收集效果。 3,空间整合:
与CMS收集器采用的“标记-清除”算法不同,G1采用的是“标记-整理”算法,意味着G1运行期间不会产生内存空间碎片,收集后能提供规整的可用内存。
此特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。 4,可预测的停顿:
这是G1相对CMS的一大优势,G1除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
在G1之前的收集器进行收集的范围都是整个新生代或老年代,而G1使用时,Java堆的内存布局与其他收集器有很大区别,
- 它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,
- 但新生代和老年代不再是物理隔离,而都是一部分Region(不需要连续)的集合。
(2)G1收集器的运作大致可划分以下几个步骤:
1,初始标记 (Initial Marking)
仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,
让下一阶段用户程序并发运行时,能在Region中创建对象,此阶段需要停止线程,但耗时很短。
2,并发标记(Concurrent Marking)
从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
3,最终标记(Final Marking)
修改并发标记期间因用户程序继续运作而导致标记产生变动的那部分对象的标记记录。此阶段需要停顿线程,但是可并行执行。
4,筛选回收(Live Data Counting and Evacuation)
首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。
此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
(3)以下是G1 收集器的运行过程:
(4)适用场景
根据G1收集器的特征,最适用于面向服务端应用来进行垃圾收集。
总结:
- 其实内存回收与垃圾收集器在很多时候都是影响系统性能、并发能力的主要因素之一,
- 虚拟机之所以提供多种不同的收集器,也是因为只有根据实际应用需求、实现方式来选择最优的收集方式才能获取最高的性能。
了解收集器需要注意:
- 对哪个区域(新/老/整体)
- 采用哪种收集算法(复制,清除,整理,分代)
- 使用哪种方式(串行,并行,并发)
来源:周志明《深入理解Java虚拟机》