【转载】Jvm垃圾回收算法,回收策略,回收器

Java 语言的一大特点就是可以进行自动垃圾回收处理,而无需开发人员过于关注系统资源,例如内存资源的释放情况。自动垃圾收集虽然大大减轻了开发人员的后顾之忧,基本上不用担心垃圾回收问题了,注意是基本上喔,因为我们还有常见的OOM。

拥有垃圾收集器可以说是 Java 语言与 C++语言的一项显著区别。在 C++语言中,程序员必须小心谨慎地处理每一项内存分配,且内存使用完后必须手工释放曾经占用的内存空间。当内存释放不够完全时,即存在分配但永不释放的内存块,就会引起内存泄漏,严重时甚至导致程序瘫痪。

我们首先看看有哪些回收算法。

一、回收算法

引用计数法(Reference Counting)

引用计数器在微软的 COM 组件技术中、Adobe 的 ActionScript3 种都有使用。

引用计数器的实现很简单,对于一个对象 A,只要有任何一个对象引用了 A,则 A 的引用计数器就加 1,当引用失效时,引用计数器就减 1。只要对象 A 的引用计数器的值为 0,则对象 A 就不可能再被使用。引用计数器的实现也非常简单,只需要为每个对象配置一个整形的计数器即可。但是引用计数器有一个严重的问题,即无法处理循环引用的情况。

因此,在 Java 的垃圾回收器中没有使用这种算法。一个简单的循环引用问题描述如下:
有对象 A 和对象 B,对象 A 中含有对象 B 的引用,对象 B 中含有对象 A 的引用。此时,对象 A 和对象 B 的引用计数器都不为 0。但是在系统中却不存在任何第 3 个对象引用了 A 或 B。也就是说,A 和 B 是应该被回收的垃圾对象,但由于垃圾对象间相互引用,从而使垃圾回收器无法识别,引起内存泄漏。

标记-清除算法(Mark-Sweep)

这里写图片描述

标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
一种可行的实现是,在标记阶段首先通过根节点,标记所有从根节点开始的较大对象。因此,未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段,清除所有未被标记的对象。该算法最大的问题是存在大量的空间碎片,因为回收后的空间是不连续的。在对象的堆空间分配过程中,尤其是大对象的内存分配,不连续的内存空间的工作效率要低于连续的空间。

复制算法(Copying)

这里写图片描述

将现有的内存空间分为两快,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大。因此在真正需要垃圾回收的时刻,复制算法的效率是很高的。又由于对象在垃圾回收过程中统一被复制到新的内存空间中,因此,可确保回收后的内存空间是没有碎片的。该算法的缺点是将系统内存折半。

Java 的新生代串行垃圾回收器中使用了复制算法的思想。新生代分为 eden 空间、from 空间、to 空间 3 个部分。其中 from 空间和 to 空间可以视为用于复制的两块大小相同、地位相等,且可进行角色互换的空间块。from 和 to 空间也称为 survivor 空间,即幸存者空间,用于存放未被回收的对象。在垃圾回收时,eden 空间中的存活对象会被复制到未使用的 survivor 空间中 (假设是 to),正在使用的 survivor 空间 (假设是 from) 中的年轻对象也会被复制到 to 空间中 (大对象,或者老年对象会直接进入老年带,如果 to 空间已满,则对象也会直接进入老年代)。此时,eden 空间和 from 空间中的剩余对象就是垃圾对象,可以直接清空,to 空间则存放此次回收后的存活对象。这种改进的复制算法既保证了空间的连续性,又避免了大量的内存空间浪费。

标记-压缩算法(Mark-Compact)

这里写图片描述

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在年轻代经常发生,但是在老年代更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活的对象较多,复制的成本也将很高。

标记-压缩算法是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。也首先需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

增量算法(Incremental Collecting)

在垃圾回收过程中,应用软件将处于一种 CPU 消耗很高的状态。在这种 CPU 消耗很高的状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。

增量算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

分代(Generational Collecting)

根据垃圾回收对象的特性,不同阶段最优的方式是使用合适的算法用于本阶段的垃圾回收,分代算法即是基于这种思想,它将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的回收算法,以提高垃圾回收的效率。以 Hot Spot 虚拟机为例,它将所有的新建对象都放入称为年轻代的内存区域,年轻代的特点是对象会很快回收,因此,在年轻代就选择效率较高的复制算法。当一个对象经过几次回收后依然存活,对象就会被放入称为老生代的内存空间。在老生代中,几乎所有的对象都是经过几次垃圾回收后依然得以幸存的。因此,可以认为这些对象在一段时期内,甚至在应用程序的整个生命周期中,将是常驻内存的。如果依然使用复制算法回收老生代,将需要复制大量对象。再加上老生代的回收性价比也要低于新生代,因此这种做法也是不可取的。根据分代的思想,可以对老年代的回收使用与新生代不同的标记-压缩算法,以提高垃圾回收效率。

二、回收策略

从不同角度分析垃圾收集器,可以将其分为不同的类型。

  1. 按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。串行垃圾回收器一次只使用一个线程进行垃圾回收;并行垃圾回收器一次将开启多个线程同时进行垃圾回收。在并行能力较强的 CPU 上,使用并行垃圾回收器可以缩短 GC 的停顿时间。

  2. 按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间;独占式垃圾回收器 (Stop the world) 一旦运行,就停止应用程序中的其他所有线程,直到垃圾回收过程完全结束。

  3. 按碎片处理方式可分为压缩式垃圾回收器和非压缩式垃圾回收器。压缩式垃圾回收器会在回收完成后,对存活对象进行压缩整理,消除回收后的碎片;非压缩式的垃圾回收器不进行这步操作。

  4. 按工作的内存区间,又可分为新生代垃圾回收器和老年代垃圾回收器。

我们可以看到垃圾回收其实有以下策略:

  1. 串行,并行
  2. 并发
  3. 压缩,非压缩,拷贝

可以用以下指标评价一个垃圾处理器的好坏:

吞吐量:指在应用程序的生命周期内,应用程序所花费的时间和系统总运行时间的比值。系统总运行时间=应用程序耗时+GC 耗时。如果系统运行了 100min,GC 耗时 1min,那么系统的吞吐量就是 (100-1)/100=99%。

垃圾回收器负载:和吞吐量相反,垃圾回收器负载指来记回收器耗时与系统运行总时间的比值。

停顿时间:指垃圾回收器正在运行时,应用程序的暂停时间。对于独占回收器而言,停顿时间可能会比较长。使用并发的回收器时,由于垃圾回收器和应用程序交替运行,程序的停顿时间会变短,但是,由于其效率很可能不如独占垃圾回收器,故系统的吞吐量可能会较低。

垃圾回收频率:指垃圾回收器多长时间会运行一次。一般来说,对于固定的应用而言,垃圾回收器的频率应该是越低越好。通常增大堆空间可以有效降低垃圾回收发生的频率,但是可能会增加回收产生的停顿时间。

反应时间:指当一个对象被称为垃圾后多长时间内,它所占据的内存空间会被释放。

堆分配:不同的垃圾回收器对堆内存的分配方式可能是不同的。一个良好的垃圾收集器应该有一个合理的堆内存区间划分。

有了一些回收算法和策略,加上一些评价的指标,我们可以看看垃圾回收器了。

三、垃圾回收器

这里写图片描述

Serial收集器

优点:简单高效(相对于其它收集器运行于单CPU环境下,没有线程分配的开销,可以获得最高的单线程垃圾收集效率)

新生代收集器,使用复制算法,单线程,虚拟机-client模式下新生代的默认回收器。

单线程进行垃圾收集,并且在其进行垃圾收集时,必须暂停其它所有工作线程,直到垃圾收集结束,这一过程(stop the world)由虚拟机在后台自动发起自动完成,会在用户不可见的条件下把用户所有正常工作线程全部停掉。当然如果停顿的时间很短是可以接受的,但是如果每次停顿5分钟,任何人都会崩溃。

对于某些客户端程序,新生代占用的内存空间往往很小,此时停顿时间完全可以控制在毫秒级别,因此对于运行在client模式下的虚拟机,Serial收集器是个不错的选择。

ParNew收集器

优点:优化多CPU使用

新生代收集器,使用复制算法,多线程。

Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为与Serial收集器完全一样,依然会stop the world。

很多虚拟机-server模式下的首选新生代收集器,主要原因是CMS收集器(老年代收集器)只能与Serial收集器或者ParNew收集器配合使用。

默认开启的回收线程数与CPU个数相同(现代服务器动辄32个逻辑CPU将会导致ParNew收集器开启32个收集线程,这种情况下最好限制下收集线程个数)。

常用配置:

-XX:ParallelGCThreads 多线程垃圾收集器内存回收开启的线程数量。

Parallel Scavenge收集器

优点:提高应用吞吐量

新生代收集器,使用复制算法,多线程。

该收集器的关注点与其它收集器的关注点不同,其它收集器的目标为尽可能缩短垃圾收集时用户线程的停顿时间,该收集器的目标为达到一个可控的吞吐量,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾回收时间) 。

停顿时间短则响应速度快,适合与用户交互比较多的应用;吞吐量大能够最高效的利用CPU时间,尽快完成运算任务,适合后台运算、交互不多的应用。

常用配置:

-XX:MaxGCPauseMillis 最大停顿时间,仅对Parallel Scavenge收集器生效。

-XX:GCTimeRatio 吞吐量大小,默认值为99,即1%的GC时间,仅对Parallel Scavenge收集器生效。

-XX:+UseAdaptiveSizePolicy 使用GC自适应调节策略,如果启用该策略,只需要设置好基本参数(-Xmx等),然后设置一个优化目标(最大停顿时间或吞吐量大小),虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整细节参数设置(例如:-XX:SurvivorRatio 新生代中Eden区与Survivor区的大小比率,默认为8,即Eden:survivor=8:1、-XX:PretenureSizeThreshold直接晋升老年代对象大小,超过这个大小的对象将直接在老年代分配,只有Serial和ParNew收集器认识这个参数、-XX:MaxTenuringThreshold 晋升老年代对象年龄,每个对象在坚持过一次Minor GC后对象年龄+1,超过设置数值对象移动至老年代)以提供最合适的停顿时间和最大吞吐量。

Serial Old收集器

老年代收集器,使用标记-整理算法,单线程,Serial收集器的老年代版本。

作为CMS收集器的后备收集器:当CMS收集器产生Concurrent Mode Failure时,将临时启动Serial Old收集器重新进行老年代的垃圾收集。

Parallel Old收集器

老年代收集器,使用标记-整理算法,多线程,Parallel Scavenge收集器的老年代版本。

在注重吞吐量或CPU资源敏感的场合,可以优先考虑Parallel Scavenge收集器 + Parallel Old收集器。

CMS(Concurrent Mark Sweep)收集器

优点:回收快,停顿短

老年代收集器,使用标记-清除算法,多线程并发。

以达到最短的垃圾收集挺停顿时间为目标的收集器。

该收集器的收集过程分为4个步骤:

1,初始标记 CMS initial mark:该步会stop the world,但耗时非常短,标记GC Root直接关联的对象。

2,并发标记 CMS concurrent mark:耗时较长,用户线程可同时运行,标记至GC Root有可达路径的对象。

3,重新标记 CMS remark:该步会stop the world,但耗时非常短。由于步骤2中用户线程会同步运行,此时主要修正因步骤2中用户线程同步运行产生的对象标记变动。

4,并发清除 CMS concurrent sweep:耗时较长,用户线程可同时运行。

在耗时很长的并发标记阶段和并发清除阶段用户线程和收集线程都可同时工作,故而总体上来说,CMS收集器的内存回收是与用户线程一起并发执行的。

一般现在web应用大都默认使用ParNew+CMS回收器(jdk1.6/1.7)。

缺点:

1,对CPU资源敏感,CMS收集器默认开启的收集线程数为(CPU数量+3)/4,如果CPU数量较少,会占用不少CPU处理资源。

2,无法处理浮动垃圾,并且可能产生Concurrent Mode Failure从而导致另一次Full GC。

并发清除时(步骤4),用户线程是可以同时运行的,此时用户线程会产生新的垃圾,这部分垃圾在标记过程之后产生,本次GC已经不能进行标记后清除,只能留到下次GC时处理,被称为浮动垃圾。

由于CMS的收集线程执行时,用户线程也是会同时执行的,导致CMS收集器无法像其它老年代收集器那样在老年代内存几乎耗尽时再进行GC,必须为用户线程预留部分内存(低版本JDK默认值为68%,JDK6及以上版本默认值为92%将),如果预留内存无法满足用户线程的执行,将会出现Concurrent Mode Failure,此时虚拟机将会启动备用方案,调用Serial Old收集器执行一次Full GC,这将导致较长的收集停顿。

3,由于采用标记-清除算法实现,会产生内存碎片(Mark-Sweep算法的缺点)

配置:

-XX:CMSInitiatingOccupancyFraction 设置GC触发的百分比,太高会导致过多的Concurrent Mode Failure,太低则影响性能,仅对CMS收集器生效。
-XX:+UseCMSCompactAtFullCollection Full GC后提供内存整理,该过程是无法并发的,会导致性能下降,仅对CMS收集器生效。
-XX:CMSFullGCsBeforeCompaction 设置进行完几次不进行压缩的Full GC后,进行一次附带压缩的Full GC,仅对CMS收集器生效。

G1(Garbage First)收集器

优点:CMS优化

G1收集器并不采用新生代和老年代物理隔离的传统布局方式(仅在逻辑上划分新生代和老年代),而是将整个堆内存划分为2048个大小相同的独立Region块,每个Region块的大小根据堆的实际大小而定,整体被控制在1M-32M之间,G1收集器跟踪Region中的垃圾堆积情况并在后台维护一个优先级列表,每次根据设置的垃圾回收时间回收优先级最高的区域,这样可以避免整个新生代或整个老年代的垃圾回收,使得stop the world的时间更短、更可控,同时在有限的时间内可以获得最高的回收效率。

配置:

-XX:G1ReservePercent 设置空闲空间的预留百分比,以降低空间溢出风险,默认值为10,即10%

收集器使用配置:

-XX:+UseSerialGC 使用Serial+Serial Old的收集器组合

-XX:+UseParNewGC 使用ParNew+Serial Old的收集器组合

-XX:+UseConcMarkSweepGC 使用ParNew+CMS/Serial Old(Serial Old为备用)的收集器组合

-XX:+UseParallelGC 使用Parallel Scavenge+Serial Old的收集器组合

-XX:+UseParallelOldGC 使用Parallel Scavenge+Parallel Old的收集器组合

-XX:+UseG1GC 使用用G1收集器进行内存回收

一般来说,在没有明确目的调整方案的情况下,建议使用默认垃圾回收器。

原文地址:https://www.ibm.com/developerworks/cn/java/j-lo-JVMGarbageCollection/
http://www.360doc.com/content/13/0313/22/11098634_271354611.shtml
图文略有删改

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值