1.垃圾收集算法介绍
垃圾收集算法有很多,并且各个虚拟实现的方式也有所不同,并且其中大量的设计程序细节,所以这里只讨论算法的基本思路。
常见的垃圾收集算法:1.标记-清除算法 2.复制算法 3.标记-整理算法 4.分代收集算法
下面我们开始逐一介绍每一种算法
1.1 标记-清除算法
标记-清除算法是最基础的收集算法,如同他的名字一样,有标记和清除两部分构成。
首先,标记出所有需要回收的对象,在标记完成后,统一回收所有被标记的对象(标记在java虚拟机02中已经提到)。之所以说他是最基础的算法是因为后续的所有算法都是基于这种
思路并对其不足进行改进的而得到的。它最主要的不足有两个:一个是效率问题:标记和清除的效率并不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多会导致
以后程序在运行过程中需要分配较大对象时无法找到足够连续的内存而再一次触发垃圾收集。
1.2 复制算法
为了解决效率问题,一种称为“复制”的收集算法出现了。 他将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将其中还存活的对象复制到另一块上面去,
然后再把已经使用过的那一块内存空间全部清理掉。这样使得每次都是对整个半区进行回收,内存分配时也就不用考虑内存碎片等复杂情况。只需要移动堆顶指针,按顺序分配即可。
实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,未免太高了一点。
现在的虚拟机一般都采用这种回收算法来回收新生代。IBM研究表明,新生代中98%的对象都是“朝生夕死”的,所以并不需要按照1:1的比例进行分配,而是将内存分为较大的Eden空间和两块较小
的Survivor空间。每次使用Eden和其中一块Survivor空间。当回收时,将Eden和Survivor中存活的对象复制到另一块Survivor空间上,最后清理掉Eden和用过的Survivor空间。
HotSpot虚拟机默认Eden和Survivor空间比是 8:1 当Survivor空间不够用时,需要依赖其他内存进行分配担保(这里指老年代)
1.3 标记-整理算法
复制收集算法在对象存活率较高的时候就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就要有额外的空间进行分配担保,以应对内存中所有对象都100%存活的机端情况,所以老年代一般不能直接使用这种算法。
根据老年代的特点,有人提出了另一种“标记-整理”算法,标记过程仍与“标记-清除”算法相同,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
1.4分代收集算法
当前商业的虚拟机的垃圾收集都采用“分代收集”算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把java堆分成新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就是用复制算法。只需要付出少量存活对象的复制成本就可以完成收集。
而老年代中因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来回收。
2.HotSpot 的算法实现
2.1 枚举根节点
从可达性分析中 GC Roots结点找引用链这个操作为例,可作为GC Roots的结点主要是全局性的引用(常量或类静态属性)与执行上下文(例如栈帧中的本地变量表)中,现在应用仅仅方法区就有数百兆,如果逐个检查里面的引用,
那么必然会耗费很多时间。另外,可达性分析对时间的敏感还体现在GC停顿上(GC必须在一个能确保一致性的快照中执行,就像整个系统被冻结在一个时间点,只有等待GC完成才能继续运行)。即使是在号称不会发生停顿的CMS
收集器中,枚举根节点时也是需要停顿的。
由于当前主流的虚拟机都是准确式GC,所以当执行系统停顿下来后,并不需要一个不漏的检查完所有执行上下文和引用位置,虚拟机应该是有办法知道哪些地方存放着对象引用。在HotSpot实现中是使用OopMap数据结构达到这个目的。
2.2 安全点
在OopMap的协助下,HotSpot可以快速准确完成GC Roots枚举,但一个很现实的问题随之而来:可能导致引用关系的变化。或者说OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap那么将需要大量的额外空间
这样GC 的空间成本将会变得很高。
事实上,HotSpot也的确没有为每条指令都生成OopMap,之前已经提到,只是在特定位置记录了这些信息,这些特定的位置被称为安全点。即程序执行时并非所有位置都可以停顿下来进行GC ,只有在安全点才可以进行GC操作。
所以,安全点的选定基本上是以程序“是否具有让程序更长时间执行”的特征进行选取的,因为每一条指令的执行时间都非常短暂,程序不太可能因为指令流太长这个原因而过长时间运行。长时间执行最明显的特征就是指令序列复用。
例如:方法调用、循环、异常跳转等, 所以具有这些功能的指令才会产生安全点。
对于安全点,另一个需要考虑的问题就是当GC 发生时,要求所有线程都要跑到最近的安全点停下来(不包括JNI线程)。这里有两种方案进行选择: 抢先式中断和主动式中断。
抢先式中断:不需要线程执行代码主动去配合,在GC发生时,首先把所有进程中断,如果发现有线程中断的地方不在安全点上,就恢复线程让他跑到安全点上。(现在几乎没有虚拟机使用这种方式)
主动式中断:当GC需要中断线程的时候,不直接对线程执行操作,仅仅是简单的设置一个标志,各个线程在执行时会主动去轮询这个标志,发现标志中断为真时就自己中断挂起。轮询标志的地方和安全点是重合的。
2.3 安全区域
安全点似乎已经完美的解决了如何进入GC的问题,但实际情况却不一定。在程序执行时,可能不长时间就会遇到可进入的安全点,但是如果在程序不执行的时候呢(例如sleep或者block),这时候线程无法响应jvm的中断请求而走到
安全的地方进行中断挂起。对于这种情况就要使用安全区域来解决。
安全区域是指在一段代码段中,引用关系不会发生变化。在这个区域的任意地方开始GC都是安全的,我们也可以把安全区域看作是安全点的拓展。
当线程进入安全区域时,会产生一个标识,在这段时间里,JVM要发起GC 就不用管已经标识的线程了。
3.垃圾收集器
3.1 Serial 收集器
Serial收集器是最基本、发展历史最悠久的收集器。是单线程的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。简称Stop The Word
Serial收集器依然是虚拟机运行在Client模式下默认新生代收集器,对于运行在Client模式下的虚拟机来说是一个很好的选择。
3.2 ParNew 收集器
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The Worl、对象分配规则、回收策略等都与Serial 收集器完全一样。
ParNew收集器是许多运行在Server模式下的虚拟机中首选新生代收集器,其中有一个与性能无关但很重要的原因是,除Serial收集器之外,目前只有ParNew它能与CMS收集器配合工作。
3.3 Parallel Scavenge 收集器
Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器
该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)
停顿时间越短就越适合需要与用户交互的程序,良好
的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器提供两个参数用于精确控制吞吐量,分别是控制最大垃圾收起停顿时间的
-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数
Parallel Scavenge收集器还有一个参数:-XX:+UseAdaptiveSizePolicy。这是一个开关参数,当这个参数打开后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用MaxGVPauseMillis参数或GCTimeRation参数给虚拟机设立一个优化目标。
自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别
3.4Serial Old 收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。
如果在Server模式下,主要两大用途:
(1)在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
(2)作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用
3.5 Parallel Old 收集器
Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在1.6中才开始提供。
3.6 CMS 收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求
CMS收集器是基于“标记-清除”算法实现的。它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:
(1)初始标记
(2)并发标记
(3)重新标记
(4)并发清除
其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”.
CMS收集器主要优点:并发收集,低停顿。
CMS三个明显的缺点:
(1)CMS收集器对CPU资源非常敏感。CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大,为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器变种。所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想
(2)CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中蓝年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK1.6中,CMS收集器的启动阀值已经提升至92%。
(3)CMS是基于“标记-清除”算法实现的收集器,手机结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间变长了。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,标识每次进入Full GC时都进行碎片整理)
3.7 G1 收集器
G1收集器是当今收集器发展最前沿成果之一。
G1收集器的优势:
(1)并行与并发
(2)分代收集
(3)空间整理 (标记整理算法,复制算法)
(4)可预测的停顿(G1处处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经实现Java(RTSJ)的来及收集器的特征)
使用G1收集器时,Java堆的内存布局是整个规划为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合。
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在真个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获取的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的又来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽量可能高的灰机效率
G1 内存“化整为零”的思路
在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下步骤:
(1)初始标记
(2)并发标记
(3)最终标记
(4)筛选回收
4.理解GC日志
阅读GC日志是处理java虚拟机内存问题的基本技能,他只是一些认为确定的规则,没有技术含量。
每一种收集器的日志都是由他们自身的实现所决定的,换而言之,每个收集器的日志格式都就可以不一样,但虚拟机设计者为了方便用户阅读,将各个收集器的日志都维持一定的共性。
下面我们来看一段GC日志:
33.125:[GC [DefNew : 3324K ->152K(3712K), 0-0025925 secs ] 3324K -> 152K(11904K),0.0031680 SECS] 100.667:[Full GC [TENURED: 0K-> 210K(10240K),0.0149142 SECS] 4630k ->210k(19546k),[Perm : 2999K ->299K(21248K), 0.0150007 secs] [Times:user = 0.01 sys = 0.00 ,real = 0.02 secs]
最前面的数字 33.125 和 100.667 代表发生GC 的时间,这个数字的含义是从java虚拟机启动到现在的秒数。
GC 日志开头的“[ GC” 和 "[FULL GC" 说明了这次垃圾收集的停顿类型,而不是用来区分新生代GC 还是老年代GC的。 如果有Full说明说明这次GC是发生了 Stop The Word 的。
接下来的 [DefNew 、[Tenured 、[Perm 表示GC 发生的区域,这里显示的区域名与使用的垃圾收集器是密切相关的
例如:Serial收集器中 新生代名称为:Default New Generation 所以显示的是 [DefNew 。
后面方括号内部的 3324K - > 152K (3712K) 的含义是: GC前该区域使用的容量 - > GC后该区域所使用的容量 (该区域内存总容量)
而在方括号之外的 3324K -> 152K(11940K) 的含义是:GC前java堆已使用的容量-> GC后java堆已使用的容量 (java堆总容量)
再往后 0.0031680 secs表示该内存区域GC所占用的时间。有的收集器会给出更具体的参数如 Times:user = 0.01 sys = 0.00 ,real = 0.02 secs 表示用户态消耗cpu时间 内核消耗cpu时间 和从开始到结束消耗的总时间。
5.垃圾收集器中参数总结