JVM垃圾回收算法与垃圾收集器

垃圾回收算法

垃圾回收器都是在不同垃圾回收场景使用合理的垃圾回收算算法进行实现的。具体垃圾回收算法有以下几种。

1、标记-清除:标记所有需要被回收的对象,然后回收。次算法效率低,并且产生内存碎片,由于老年代中存活的对象多,在老年代中进行使用。

2、 复制算法:将内存划分为等大小的两块,每次使用其中的一块,回收时,将存活的对象复制到没有使用的一块内存中,然后对使用的内存一次性就行清理。实现简单,运行高效,但是存在大量内存的浪费。由于新生代中存活的对象少,新生代中使用这种算法将E区存活的对象复制到S区。

3、标记整理算法:让所有存活的对象往一侧移动,然后清楚另一侧。老年代中使用这种算法,避免产生内存碎片。

java中的垃圾回收器的实现及其使用场景。

Serial收集器

属于单线程垃圾收集器,在进行垃圾收集的时候,必须停止其他所有的用户正常工作线程。这对很多的应用来说都是难以接受的。从jdk1.3-1.7,hotspot虚拟机开发团队一直致力于开发能够消除或者减少用户线程停顿的收集器,至今优秀的收集器不断出现,用户线程停顿的时间不断缩短,但是仍不能够进行消除。

在实际的开发中,Serial依然是虚拟机运行在Client模式下的默认的新生代收集器。在于单个CPU的环境下,Serial收集器由于没有线程交互的开销,可以获得最高的垃圾收集效率。并且在用户桌面应用场景中,由于分配给虚拟机管理的内存一般不会太大,所以垃圾收集的停顿时间完全可以控制在一百多毫秒以内,只要垃圾手机不过于频繁,对用户来说是可以接受的。

Serial old收集器

serial old是serial的老年代版本,同样是一个单线程的收集器,采用的是标记整理算法,此收集器的主要意义是给在client模式下的虚拟机使用。在server中,他的用途为:在jdk1.5之前和parallel scavenge收集器搭配使用。二是作为Cms收集器的后备方案,在并发收集器发生Concurrent Mode Failure时候使用。

Serial/Serial old示意图:

JVM垃圾回收算法与垃圾收集器_第1张图片

ParNew收集器

parNew是Serial收集器的多线程版本,parnew除了采用多线程之外,其余的和serial没有太大的区别,他是运行在server模式下的首选新生代垃圾收集器,其中一个和性能无关的原因是目前除了parnew,只有serial能配合cms工作。

在单核CPU下,Serial的性能是超过parnew的,但是CPU数量大于等于二的情况下,parnew的效率必定超过serial。

ParNew收集器示意图:

JVM垃圾回收算法与垃圾收集器_第2张图片

parallel scavenge收集器

此收集器是一个新生代采用复制算法的并行多线程收集器。此收集器看上去和parnew收集器一样,但是他的关注点和其他收集器不同。

CMS等其他收集器关注点是尽可能的减少垃圾收集过程中用户线程的停顿时间,而此收集器的目标是达到一个可控制的吞吐量。吞吐量=用户线程执行的时间/(用户线程执行时间+垃圾收集时间)。

停顿时间短适合于用户进行交互的程序,使用好的响应速度提升用户的体验。而高的吞吐量可以高效的利用CPU,尽快的完成程序的计算,适合于后台运算而不需要太多的交互任务。

parallel scavenge通过参数最大垃圾收集停顿时间参数-XX:MaxGCPauseMillis和设置吞吐量参数-XX:GCTimeRatio进行控制吞吐量。

MaxGCPauseMillis参数是一个大于0的毫秒值,收集器保证垃圾收集时间不会超过这个时间值,但是不要以为将这个值设置的尽可能小就能够垃圾回收速度加快,因为停顿时间的缩短是通过牺牲吞吐量和新生代的空间换来的,缩短时间会导致垃圾收集的次数更频繁。

GCTimeRatio是一个大于0小于100的整数,就是垃圾收集时间占总是间的比率,相当于吞吐量的倒数。此数值设置的越大,表示吞吐量越大。

parallel old收集器

parallel old是parallel scavenge收集器的老年代版本,使用多线程和标记整理算法。此收集器是在jdk1.6中开始提供。在此之前,如果新生代使用parallel scavenge收集器,老年代除了使用serial old收集器别无选择,由于serial old在服务器端性能上的拖累使得parallel scavenge无法获得吞吐量最大化效果。

parallel old出现后吞吐量优先才有了名副其实的组合,所以在注重吞吐量和CPU资源敏感的场合中优先考虑parallel old和parallel scavenge的搭配使用。

parallel scavenge或parallel old示意图:

JVM垃圾回收算法与垃圾收集器_第3张图片

CMS收集器

以获得最短停顿时间为目标,基于标记清除算法,是server模式下提高用户请求响应速度,给用户带来良好体验的不错选择。
他的运作过程分为4个步骤:

1、初始标记:需要停止用户线程,初始标记只是标记一下GC Root能够直接关联到的对象,速度很快。

2、并发标记:进行遍历GC Root关联的对象,此过程可以和用户线程并行。

3、重新标记:修正并发比标记期间因用户线程继续执行而产生的变动,此过程需要停止用户的线程,并且此过程停顿的时间比初始标记停顿的时间稍长,但是小于并发标记所需要的时间。

4、并发清除:清除线程和用户线程并行

由于整个过程中耗时最长的并发标记和并发清除过程都可以和用户线程并行,所以总体上可以说垃圾收集过程和用户的线程是并行的。

CMS属于低停顿,但是并不是完美的,缺点如下:

1、 CMS收集器对CPU的资源非常的敏感。
在和用户线程并行执行的阶段,虽然不会停止用户的线程,但是或占用cpu的资源,导致用户线程的执行速度变慢,总体的吞吐量降低。CMS默认启动的回收线程的数量是(CPU数量 + 3)/4,占用CPU的资源=垃圾收集线程数量/CPU数量,不难计算出占CPU资源=(1/4 + 3/(4 × CPU数量))(此为估计值),所以回收线程占用CPU的资源至少为25%,随着CPU数量的增加而减少。并且当CPU的数量为2时,占用CPU的资源为50%,这时无法接受的。
为了解决这种问题,虚拟机提供了一种增量式并发收集器,这种收集方式采用的就是和单CPU执行抢占CPU资源的思想:在并发标记,清理的时候,让GC线程和用户线程交替执行,尽量减少GC线程占用CPU的时间,这样使得垃圾收集的总是间变得更长。但是实践证明,这种方式效果一般,在目前的版本中已经不提倡使用。

2、CMS收集器无法收集浮动垃圾,并且在收集过程中如果提供给用户线程使用的空间不够用将会导致FULL GC。
由于CMS进行并发清理的时候,用户线程同时也在运行,此时还会产生新的垃圾,这一部分垃圾发生在并发标记之后,CMS无法对其进行回收,需要留到下次GC时进行回收。也是由于垃圾收集阶段,用户线程需要运行,所以CMS收集器不能和其他垃圾收集器一样等到老年代被完全填满之后才进行收集,而是需要预留一部分空间给垃圾回收过程中的用户线程进行使用。在jdk1.5中,CMS收集器当老年代被使用超过68%时被激活,如果老年代中垃圾增长速度不是太快,可以适当将此值调高(-XX:CMSInitiatingOccupancyFraction),从而降低垃圾回收的次数来提高性能。在jdk1.6中,启动阀值提升至92%。

如果CMS运行期间预留的内存无法满足用户程序需要,会出现“Concurrent Mode Failure”失败,此时虚拟机将会启动后备方案:临时启用Serial Old收集器重新进行老年代垃圾的回收,这样使得GC停顿的时间变得很长,反而减低了性能。

3、CMS是基于标记清除算法实现,在垃圾收集的过程中会产生大量空间碎片。
空间碎片的过多将会导致老年代即时有很大的空间但是无法找到足够大的连续的空间来分配对象,从而导致FULL GC,为了解决这个问题,CMS收集器提供参数-XX:+UseCMSCompact-AtFullCollection开关参数(默认开启),用于当CMS收集器顶不住要进行FullGc时开启内存碎片的整合过程,整合过程是无法和用户线程进行并行的,内存碎片问题得到了解决,但是停顿时间不得不变长。另外提供参数-XX:CMSFullGCsBeforeCompaction,用于设置执行多少次不压缩的Full GC之后执行一次压缩,默认值为0。

CMS示意图:

JVM垃圾回收算法与垃圾收集器_第4张图片

G1收集器​

是一款面向服务端的垃圾收集器,与其他收集相比他主要有以下特点:
1、并发与并行:G1充分利用多CPU,多核环境下额硬件优势,使用多个CPU来缩短GC停顿时间,部分其他CPU原本需要停顿用户线程执行GC动作,G1收集器仍然可以和用户线程并行执行。

2、 分代收集:G1收集器仍保留了分代收集的概念,并且不与其他收集器配合的情况下也能够完成整个GC堆的收集和整理。但是G1收集器也可以采用不同的方式去处理新创建的对象,已经存活一段时间的对象,熬过多少次GC的对象以获取更好的收集效果。

3、空间整合:G1整体采用标记整理算法实现,局部来看(两个Region之间)基于复制算法实现,两个算法在垃圾收集过程中不会产生内存碎片,不会有频繁的Full GC出现。

4、可预测停顿:降低停顿时间是CMS和G1的共同关注点,但是G1追求低停顿同时还建立了停顿时间模型,能够让使用者明确指定在一个长度为M的时间片段,消耗在垃圾收集上的时间不能超过N秒。

在G1之前的所有收集器,都是针对整个新生代和老年代。而使用G1收集器时,java堆的内存布局与其他收集器明显不同,他整个java堆规划为多个大小相等的独立区域(Region),虽保留新生代和老年代的概念,但是新生代和老年代不再是物理隔离的,他们都是一部分不连续的Region的集合。

G1能够建立可预测的时间停顿模型的原因就是它能够有计划的避免在整个java堆进行垃圾收集。G1能够跟踪各个Region里面的垃圾堆积的价值大小(回收所需要的时间,回收所能够获得空间的大小),在后台维护一个优先列表,在每次垃圾收集时,根据允许的收集时间,优先回收价值最大的Region。这种方式保证了在有限的时间内获取尽可能高的收集效率。

G1把内存化整为零实践过程中的问题:Region不可能是孤立存在的。一个对象分配在某个Region中,他可以被整个java堆中的任意对象引用,所以在做可达性算法进行判断对象是否存活的时候,需要进行整个java堆扫面才能够保证垃圾回收的准确定。这个问题在其他收集器中也有,只是在G1中表现的更为突出。在之前的垃圾回收器中,新生代的规模一般比老年代小很多,新生代的收集也比老年代频繁很多,那时回收新生代的时候也面临着同样的问题——在回收新生代的时候不得不扫面老年代。那收集器是怎样对这种问题进行解决的呢。

在G1收集器中Region之间的对象引用问题以及其他收集器中新生代和老年代之间的对象引用问题,虚拟机都是使用Remembered Set来避免进行全堆进行扫描的。G1中的每个Region都有一个与之对应的Remembered Set,当虚拟机发现程序对Reference类型的数据进行写操作的时候,会产生一个Write Barrier暂时中断写操作,检查Reference是否处于不同的Region之中(在分代的收集器中就检查是否存在老年代引用了新生代的对象),如果是,就通过CardTable把相关的引用信息记录到被引用对象所属的Region的Remembered Set中。在进行垃圾回收的时候,在GC根节点的枚举范围中加入Remembered Set就可以保证不进行全堆进行扫面。

G1收集器垃圾回收步骤:

1、初始标记:仅仅是标记一下GC Root能够直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序在并发运行时,能在正确可用的Region中创建对象,此阶段需要停顿用户线程,但是停顿时间短。

2、并发标记:此阶段是通过GC Root引用判断对象是否存活,用时较长,但是和用户线程并发执行。

3、最终标记:标记并发标记阶段用户线程运行时候产生的对象变动,在并发标记阶段,虚拟机将对象的变动记录在线程Remembered Set Logs中,最终标记阶段将Remembered Set Logs中的数据合并到Remembered Set中,这阶段需要停止用户线程,但是标记线程可以并行执行。

4、 筛选回收:先对各个Region的回收价值进行排序,根据用户期望的停顿时间制定回收计划,这阶段可以做到和用户线程并行执行,但是由于只是收集部分区域,并且时间由用户可控,所以这阶段选择停止用户线程,让多个垃圾收集线程并行执行,提高垃圾回收的效率。

G1示意图:

JVM垃圾回收算法与垃圾收集器_第5张图片

你可能感兴趣的:(java)