您的位置:首页 > 编程语言 > Java开发

JVM学习02-GC算法与种类

2016-01-03 23:31 435 查看

1. GC 简介

  GC(Garbage Collection) 是垃圾收集的简写,GC机制是java中一个比较重要的概念。java的内存管理提供了内存的分配和释放,内存处理是程序编写人员很容易出错的地方,忘记或错误的内存回收很容易导致系统的不稳定,甚至瘫痪。java的GC机制可以很好的检测对象是否超过作用域而可以达到回收的要求,从而实现自动回收垃圾对象的释放内存的目的。

   其实早在很久以前(1960)就已经有了GC的概念,只是java借用这个优秀的思想,在java内存模型中,GC的工作区域主要是在堆区域和方法区,大部分都是在堆区域中。

2. GC 算法

2.1 引用计数法

  首先呢,java并没有采用引用计数法作为GC算法,因为它有明显的缺陷。

  引用计数法实现比较简单:对于一个对象A来说,只要有任何一个对象或者引用指向了A,那么A的计数器就加1,当引用失效时,引用计数器就减1,最后只要对象A的引用计数器的值为0,那么A就不可能再被使用,就可被视为垃圾。



  引用计数法存在明显的缺陷,第一由于不断引用和去除引用伴随着加减法,影响性能。第二最大是问题就是很难解决循环引用的问题。



这样就会存在很多的对象无法被回收,所以JVM压根没有使用引用计数法作为GC算法。

2.2 标记-清除

  标记清除(Mark-Sweep)算法是现代垃圾回收算法的基础。顾名思义,标记清除算法将垃圾清除分为两个阶段:标记和清除

标记:通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用即不可达的对象。


清除:清除所有未被标记的对象。




2.3 标记-压缩

  标记压缩(Mark-Compact)算法适用于存活对象比较多的场合,比如老年代。它在标记-清除算法基础上做了一些优化,标记压缩算法和标记清除算法一样,首先通过根节点标记可达的对象,然后还将所有可达的对象(存活的对象)压缩到内存一端,然后清理边界外所有空间。

标记:通过根节点标记可达的对象。


压缩:将存活的对象整理到内存一端,清理边界外所有空间。




2.4 复制算法

与标记清除算法相比,复制(Copying)算法相对是一个比较高效的算法,由于涉及到存活对象的赋值,所以复制算法不适合存活对象比较多的场合(如不适合老年代)。复制算法的思想大致如下:将原有的内存分为两块空间,每次只使用其中一块,在垃圾回收的时候,将正在使用内存中存活的对象复制到未使用的内存块中,之后清除正在使用内存中的所有对象,然后交换两个内存的角色完成垃圾回收。



2.5 增量算法

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

2.6 总结

首先是引用计数法,这个缺陷比较明显,JVM也没有采用,不做讨论。

标记清除和标记压缩相比,前者产生大量不连续的内存碎片。后者虽然不产内存碎片,但是在前者的基础上还进行对象的压缩(整理),所以成本相对较高。且两者都会进行标记和清除,效率不是很高。

复制算法虽然比较简单高效,且不会产生内存碎片,但是明显浪费了大量的内存空间

3. 分代思想

  根据GC算法的总结可以知道,结合不同算法的有点才能规划出一套比较可行的方案,所谓没有最好只有更好。

  前面JVM内存模型中有说到将JAVA堆分为新生代和老年代,然后又将新生代细分为eden space,survivor space(分为from和to或者s0和s1),然后根据对象的存活周期将短命对象归为新生代,长命对象归为老年代。

  然后根据不同代的特点,在对象存活比较少的新生代采用复制算法,在老年代采用标记清除或标记压缩算法。



图中,在进行垃圾回收的时候,存活对象如何存放有很多种可能。

1. 左边绿色的箭头表示在垃圾回收的时候,第一种年龄比较大的对象会存放在老年代中;第二种情况就是一些比较大的对象无法放到 survivor 空间中,那么此时大对象也被存放到老年代中,所以有的是也称老年代是一个担保空空。

2. 左边红色和黄色的箭头就是进行正常的复制算法,垃圾回收结束就是右图的样子。

3. 有的时候查看GC日志的时候,新生代的垃圾回收一般称为minor gc。老年代由于区域比较大且存活对象很多,生命周期很长,所以gc时候会比较长,通常称为full gc。

4. 对象的可触及性

对象的可触及性顾名思义就是从根节点可以标记的对象。大家一般情况会认为对象要么是可触及的,要么是不可触及的,其实中间还存在一个可复活性。

1. 可触及性

 从根节点可以触及的对象

2. 可复活性

一旦所有引用被释放,对象进入可复活状态,因为在 finalize() 方法后对象可能变得可触及

3. 不可触及性

finalize() 方法之后,对象可能进入不可触及状态。不可触及的对象不能复活,然后进入可回收的状态。

这里需要注意的一点是 finalize() 方法只会被执行一次,所以某个对象不能无限又可复活性到达可触及性。而且 finalize() 方法的执行优先级很低,何时出发GC并不确定,finalize()方法的调用也变得不确定。

更详细参照:《finalize 总结》

5. Stop-The-World (STW) 现象

  STW现象是java的一种全局暂停的现象,所有Java代码停止运行(Native代码可以执行,但不能与JVM交互)。产生这种现象的原因多半是由于 GC 引起的。GC 为什么产生 STW 现象?这个以开 Party 打个比方:我们在开party 的时候会产生很多的垃圾,那么此时如果有人来清理垃圾,我们他在清理的时候,我们又不断产生垃圾,那么房间永远打扫不干净,所以唯一的办法就是我们停止手中的事情,直到房间打扫干净后再进行活动。

  那么GC也是这个道理,当发生GC的时候,必然所有的工作线程会停止,那么此时就会产生java停顿的现象。

  当发生STW现象的时候,如果时间短还好,如果时间特别长甚至几十分钟,服务器就会长时间得不到响应,那么就会带来比较大的危害。解决方法可以使用主备机的切换吧,具体不是很清楚,也不展开了。

6. 垃圾回收器的种类

6.1 新生代串行收集器

串行收集器是一个古老而稳定,经过长时间考验的垃圾收集器。在诸如单 CPU 处理器或者较小的应用内存等硬件平台不是特别优越的场合,它的性能表现可以超过并行回收器和并发回收器。但是有的时候会停顿很长时间,且是线程独占的。

新生代串行收集器采用了复制算法,当 JVM 在 Client 模式下运行时,它是默认的垃圾收集器。

在 HotSpot 虚拟机中,使用 -XX:+UseSerialGC 参数可以指定使用新生代串行收集器和老年代串行收集器。此时老年代串行收集器采用的是标记-压缩算法



[GC 0.844: [DefNew: 17472K->2176K(19648K), 0.0188339 secs] 17472K->2375K(63360K), 0.0189186 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]


6.2 老年代串行收集器

老年代串行收集器和新生代串行收集器一样,老年代串行收集器采用的是标记-压缩算法,可以使用 -XX:+UseSerialGC 参数可以指定使用新生代串行收集器和老年代串行收集器。

图解和新生代串行收集器一样。

[Full GC 8.259: [Tenured: 43711K->40302K(43712K), 0.2960477 secs] 63350K->40302K(63360K), [Perm : 17836K->17836K(32768K)], 0.2961554 secs] [Times: user=0.28 sys=0.02, real=0.30 secs]


6.3 并行收集器

并行收集器工作在新生代,仅仅是将新生代串行收集器变成了多线程化了,它的回收策略、算法以及参数和串行回收器一样。

新生代依旧采用复制算法,老年代还是串行收集器(标记压缩算法)。GC依旧是线程独占的。

开启并行回收器可以使用参数 -XX:+UseParNewGC



多线程并不意味着GC一定会很快,且需要多核CPU的支持才会相对提高效率。

可以使用 -XX:ParallelGCThreads 限制线程数量

[GC 0.834: [ParNew: 13184K->1600K(14784K), 0.0092203 secs] 13184K->1921K(63936K), 0.0093401 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]


6.4 新生代并行回收收集器

与 ParNew 类似,新生代并行回收收集器也是使用复制算法的收集器。从表面上看,它和并行收集器一样都是多线程、独占式的收集器。但是,并行回收收集器有一个重要的特点:它非常关注系统的吞吐量

使用 -XX:+UseParallelGC 设置 新生代使用Parallel收集器+ 老年代使用串行收集器

使用 -XX:+UseParallelOldGC 设置 新生代和老年代都使用Parallel收集器



[Full GC [PSYoungGen: 2682K->0K(19136K)] [ParOldGen: 28035K->30437K(43712K)] 30717K->30437K(62848K) [PSPermGen: 10943K->10928K(32768K)], 0.2902791 secs] [Times: user=1.44 sys=0.03, real=0.30 secs]


6.5 老年代并行回收收集器

老年代的并行收集器和新生代并行收集器一样,所线程且关注系统吞吐量,采用标记压缩的算法。

使用 -XX:+UseParallelOldGC 设置 新生代和老年代都使用Parallel收集器

图解和清单和新生代并行收集器一样。

这里新生代和老年代的并行收集器和使用以下参数启动:

-XX:MaxGCPauseMills : 设置最大的停顿时间,单位是毫秒。GC会尽力保证回收的时间不超过吞吐量。

-XX:GCTimeRatio : 设置吞吐量的大小n,GC时间比[ 1/(1+n) ]。默认值为99,即最大允许1%的时间用于垃圾回收。

这两个参数本来就是矛盾的,如果将最大停顿时间设置越小,那么GC就会越频繁,从而降低整个系统的吞吐量。如果吞吐量设置越大,GC导致的停顿时间也会越长,所以有所矛盾。所以在实际中也可以采用自适应的GC调节策略,使用 -XX:+UseAdaptiveSizePolicy 可以打开自适应 GC 策略。在这种模式下,新生代的大小、eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,可以直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和停顿时间 (MaxGCPauseMills),让虚拟机自己完成调优工作。

6.6 CMS收集器

  CMS (Concurrent Mark Sweep)是并发标记清除的收集器,这里的并发表示可以和用户线程一起工作,且采用标记清除算法

  CMS 收集器针是老年代收集器,新生代采用 ParNew 并行收集器。

  可以使用 -XX:+UseConcMarkSweepGC 启动 CMS 收集器。

  CMS 工作大致可分为如下几个过程:

1. 初始标记:GC 线程独占,对根可以直接关联到的对象进行标记,速度比较快。

2. 并发标记:GC 线程和用户线程一起执行,对所有的对象进行标记。

3. 重新标记:由于上一步并发过程,用户线程可能还会生产出垃圾,所以GC 线程独占,在正式清理前重新做一次标记。

4. 并发清除:GC 线程和用户线程一起执行,GC 回收垃圾对象。



1.662: [GC [1 CMS-initial-mark: 28122K(49152K)] 29959K(63936K), 0.0046877 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.666: [CMS-concurrent-mark-start]
1.699: [CMS-concurrent-mark: 0.033/0.033 secs] [Times: user=0.25 sys=0.00, real=0.03 secs]
1.699: [CMS-concurrent-preclean-start]
1.700: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.700: [GC[YG occupancy: 1837 K (14784 K)]1.700: [Rescan (parallel) , 0.0009330 secs]1.701: [weak refs processing, 0.0000180 secs] [1 CMS-remark: 28122K(49152K)] 29959K(63936K), 0.0010248 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
1.702: [CMS-concurrent-sweep-start]
1.739: [CMS-concurrent-sweep: 0.035/0.037 secs] [Times: user=0.11 sys=0.02, real=0.05 secs]
1.739: [CMS-concurrent-reset-start]
1.741: [CMS-concurrent-reset: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]


CMS虽然是并发收集器,但是其实也会存在线程独占的情况导致全局停顿,只是降低了停顿的时间罢了。由于用户线程的运行过程中,GC的时候还需要分CPU去做垃圾回收,这样就会大大降低整体系统的反应速速。在清理阶段,由于和用户线程并发执行,还会产生新的垃圾,导致清理不彻底。甚至还可能会存在产生的垃圾使得CMS来不及清理,让可使用内存的容量迅速减小,直到内存预留不够,引起 concurrent mode failure 错误。

解决方案:

第一可以通过 -XX:CMSInitiatingOccupancyFraction 的值来设置触发CMS收集器的阀值。默认为68,即当老年代空间使用率达到68%的时候触发CMS回收。


第二当引起concurrent mode failure 错误的时候,JVM就会启动备用回收器 **老年代串行回收器** 作为GC回收器。


CMS收集器的一些参数:

1. -XX:+UseCMSCompactAtFullCollection 参数将在进行一个Full GC之后进行一次内存压缩(整理),由于CMS采用的是标记清除算法。由于整理过程是线程独占的,所以可能引起的停顿时间较长。

2. -XX:+CMSFullGCsBeforeCompaction 参数设置多少次Full GC后进行一次内存整理。

3. -XX:ParallelCMSThreads 参数设置CMS线程的数量。

6.7 G1收集器

G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征. 在Oracle JDK 7 update 4 及以上版本中得到完全支持, 专为以下应用程序设计:

1. 可以像CMS收集器一样,GC操作与应用的线程一起并发执行

2. 紧凑的空闲内存区间且没有很长的GC停顿时间.

3. 需要可预测的GC暂停耗时.

4. 不想牺牲太多吞吐量性能.

5. 启动后不需要请求更大的Java堆.

G1的长期目标是取代CMS(Concurrent Mark-Sweep Collector, 并发标记-清除). 因为特性的不同使G1成为比CMS更好的解决方案. 一个区别是,G1是一款压缩型的收集器,G1通过有效的压缩完全避免了对细微空闲内存空间的分配,不用依赖于regions,这不仅大大简化了收集器,而且还消除了潜在的内存碎片问题。除压缩以外,G1的垃圾收集停顿也比CMS容易估计,也允许用户自定义所希望的停顿参数(pause targets)。

详细参考博文:《G1垃圾收集器入门》

7. GC 相关的参数

1. 与串行回收器相关的参数

-XX:+UseSerialGC : 在新生代和老年代使用串行回收器。


-XX:+SuivivorRatio : 设置 eden 区大小和 survivor 区大小的比例。8表示 两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10。


-XX:NewRatio : 新生代和老年代(不包含永久区)的比。4 表示 新生代:老年代=1:4,即年轻代占堆的1/5。


-XX:+PretenureSizeThreshold : 设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配。


-XX:MaxTenuringThreshold : 设置对象进入老年代的年龄的最大值。每一次 Minor GC 后,对象年龄就加 1。任何大于这个年龄的对象,一定会进入老年代。


2. 与并行 GC 相关的参数

-XX:+UseParNewGC : 在新生代使用并行收集器。


-XX:+UseParallelOldGC : 老年代使用并行回收收集器。


-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。


-XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。


-XX:GCTimeRatio : 设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。


-XX:+UseAdaptiveSizePolicy : 打开自适应 GC 策略。在这种模式下,新生代的大小,eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。


3. 与 CMS 回收器相关的参数

-XX:+UseConcMarkSweepGC : 新生代使用并行收集器,老年代使用 CMS + 串行收集器(备用收集器)。


-XX:+ParallelCMSThreads : 设定 CMS 的线程数量。


-XX:+CMSInitiatingOccupancyFraction : 设置 CMS 收集器在老年代空间被使用多少后触发,默认为 68%。


-XX:+UseFullGCsBeforeCompaction : 设定进行多少次 CMS 垃圾回收后,进行一次内存压缩。


-XX:+CMSClassUnloadingEnabled : 允许对类元数据进行回收。


-XX:+CMSParallelRemarkEndable : 启用并行重标记。


-XX:CMSInitatingPermOccupancyFraction : 当永久区占用率达到这一百分比后,启动 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。


-XX:UseCMSInitatingOccupancyOnly : 表示只在到达阈值的时候,才进行 CMS 回收。


-XX:+CMSIncrementalMode : 使用增量模式,比较适合单 CPU。


4. 与 G1 回收器相关的参数

-XX:+UseG1GC:使用 G1 回收器。


-XX:+UnlockExperimentalVMOptions : 允许使用实验性参数。


-XX:+MaxGCPauseMills : 设置最大垃圾收集停顿时间。


-XX:+GCPauseIntervalMills : 设置停顿间隔时间。


5. 其他参数

-XX:+PrintGCDetails : 打开显示GC日志的开关。


-XX:+DisableExplicitGC : 禁用显示 GC。


-Xloggc:Xxx.log : 设置GC的log位置和名称。


-XX:+HeapDumpOnOutOfMemoryError : 当堆内存移除出错的时候显示最后的GC日志。


-Xmx 和 –Xms : 设置堆内存的最大允许值和最小值,如-Xmx32M -Xms32M


-XX:PermSize 和 -XX:MaxPermSize : 设置永久代的最小初始值和最大允许值 如-XX:PermSize=64MB 和 -XX:MaxPermSize=256M。


-XX:MaxPermSize缺省值和client/server选项相关,-server选项下默认MaxPermSize为64m,-client选项下默认MaxPermSize为32m。


-Xmn : 设置新生代大小 , 如 -Xmn16M
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  java JVM 垃圾回收 GC