JVM系列(二)垃圾回收

JVM系列(二)垃圾回收

Hotspot垃圾收集器

JDK7/8之后,Hotpost虚拟机内所有的垃圾收集器及其组合(连线),下图:

img

img

他们所处区域,表明其所属新生代收集器还是老年代收集器:

  • 新生代垃圾收集器:Serial、ParNew、Parallel Scavenge
  • 老年代垃圾收集器:Serial Old、Parallel Old、CMS
  • 整堆收集器:G1

两个收集器之间有连线,表明他们可以搭配使用:

Serial/Serial OldSerial/CMSParNew/Serial OldParNew/CMSParallel Scavenge/Serial OldParallel Scavenge/Parallel OldG1

并发垃圾收集器和并行垃圾收集器的区别

并行与并发

  • 并行(Parallel)

多条垃圾收集线程并行工作,但此时用户线程处于等待状态

如ParNew、Pallel Scavenge、Parallel Old

  • 并发(Concurrent)

用户线程与垃圾收集线程同时执行(不一定是并行的,可能会是交替执行);

用户程序线程继续运行,而垃圾收集线程运行于另一个CPU上。如CMS

Minor GC 和 Full GC的区别

1. Minor GC(新生代GC)

又称新生代GC,指发生在新生代区域的垃圾收集动作;

因为Java对象大多是朝生夕灭的,所以Minor GC非常频繁,一般回收速度也比较快。

2. Major GC 、Full GC(年老代GC)

又称老年代GC ,指发生在老年代区域的垃圾收集动作;

出现Full GC(年老代GC)经常会伴随至少一次的Minor GC(不是绝对的,Parallel Sacvenge收集器可以设置策略)

Full GC 比 Minor GC 要慢10倍以上。

垃圾收集器介绍

Serial收集器

serial收集器是最基本、历史最悠久的收集器。它是一个单线程的收集器

它“单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作,更重要的是它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束

“Stop the World”这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,用户体验非常差。

我们看下Serial收集器的运行示意图:(书中原图)

img

img

到目前为止,Serial收集器依然是虚拟机运行在Client模式下的默认新生代收集器。

Serial收集器的优点:简单高效

-———————————————————————————————————————–

Serial收集器在JDK1.3之前是Hotspot新生代收集的唯一选择

特点

  • 针对新生代;
  • 采用复制算法;
  • 单线程收集;

它进行垃圾收集时,必须暂停所有工作线程,直到收集完成,即“Stop the World”

应用场景

是Hotspot虚拟机在Client模式下 新生代收集器;

对于限定单个CPU的环境来说,Serial收集器没有线程交互(切换)开销,可以获得最高的单线程收集效率;

设置参数

“-XX:+UseSerialGC” : 该参数用来显示的添加串行垃圾收集器。

运行示意图:

img

img

Stop the World 说明

JVM在后台自动发起和自动完成的,在用户不可见的情况下,把用户正常的工作线程全部停掉,即GC停顿;

GC停顿带来的用户体验非常差;

从Serial收集器–>Parallel收集器–>CMS–>G1收集器,用户线程停顿的时间在不断缩短,但仍然没法完全消除。

ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集之外,其余行为都与Serial收集器一样。

我们看下ParNew收集器工作过程示意图:

img

img

ParNew收集器除了多线程收集之外,其他与Serial收集器相比之下,并没有太多创新之处。不过在Server模式下,它是虚拟机首选的新生代收集器,因为它可以与CMS收集器配合使用。

ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于线程交互的开销,不能百分百保证可以超过Serial收集器。当然随着可以使用的CPU的数量增加,它对于GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数量与CPU的数量相同。

在CPU非常多的环境下,可以使用“-XX:ParallelGCThreads”参数来限制垃圾收集的线程数量。

-———————————————————————————————————————–

ParNew垃圾收集器是Serial收集器的多线程版本。

特点

除了多线程之外,其余的行为、特点与Serial收集器一样。

工作过程示意图:

img

img

应用场景

在Server模式下,ParNew收集器是一个非常重要的收集器,因为除Serial外,目前只有它能与CMS收集器配合工作。在单CPU环境中,不会比Serial收集器有更好的效果,因为存在线程交互开销。

参数设置

-XX:+UseConcMarkSweepGC:指定使用CMS后,会默认使用ParNew作为新生代收集器。

-XX:+UseParNewGC:强制指定使用ParNew收集器

-XX:ParallelGCThreads:指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相同。

Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

Parallel Scavenge收集器和ParNew的关注点不太一样,其他的收集器关注的都是尽可能的缩短垃圾收集时用户线程停顿的时间

而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量

所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值:

吞吐量 = 运行用户代码时间 / (运行用户代码时间+垃圾收集时间)

虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。

停顿时间越短就越适合于用户交互的程序,良好的响应速度能提升用户体验

而高吞吐量则可以高效率的利用CPU时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。

-XX:MaxGCPauseMillis

控制最大垃圾收集停顿时间;

参数是大于0的毫秒数,收集器将尽可能保证内存回收不超过设定的值。

GC停顿时间缩短是以牺牲吞吐量量和新生代空间来换取的。

以前10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒;停顿时间的确在下降,但吞吐量也下降了。

-XX:GCTimeRatio

直接设置吞吐量大小;

参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。

如果把此参数设置为19,那允许的最大GC时间就占总时间的5%(即 1/(1+19)),默认值是99,就是允许最大1%(即 1/(1+99))的垃圾收集时间

由于与吞吐量关系密切,Parallel Scavenge收集器也被称为“吞吐量优先”收集器

-XX:+UseAdaptiveSizePolicy:这是一个参数开关,当这个参数打开之后,就不需要手工指定新生代大小等细节参数了,虚拟机会根据当前系统运行状态收集性能监控信息。

动态调整这些参数,这种调节方式被称为“GC自适应的调节策略”。自适应策略也是Parallel Scavenge收集器与ParNew收集器一个重要区别。

-———————————————————————————————————————–

Parallel Scavenge垃圾收集器因为与吞吐量关系密切,也称为吞吐量收集器

特点

新生代收集器、采用复制算法、多线程收集 —- 与ParNew收集器相同

主要特点:

它关注吞吐量,目标是达到一个可控制的吞吐量

应用场景

高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间。

当应用程序运行在多个CPU上,对暂停时间没有特别高的要求时,即程序主要在后台运行计算,而不需要与用户进行太多的交互

设置参数

  • -XX:MaxGCPauseMillis

​ 控制最大垃圾收集时间,大于0的毫秒数。

​ MaxGCPauseMillis设置的稍小,停顿时间可能会缩短,但也可能会使得吞吐量下降,因为可能导致垃圾收集发生的更频繁。

  • -XX:GCTimeRatio

设置垃圾收集时间占总时间的比率,0 < n < 100的整数。

GCTimeRatio相当于设置吞吐量大小。

垃圾收集执行时间占应用程序执行时间的比例计算方法:1 / (1+n)

例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5%,1 / (1+19)

它的默认值为1%,1 / (1+99),即n = 99;

垃圾收集所花费的时间是年轻代和年老代收集的总时间;

  • -XX:+UseAdaptiveSizePolicy

开启这个参数之后,就不用手工指定一些细节参数,JVM会动态调整,GC自适应的调节策略。

吞吐量与收集器关注点说明

  • 吞吐量

CPU用于运行用户代码的时间 与 CPU总消耗时间的比值,即:

吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)

​ 高吞吐量即减少垃圾收集时间,让用户代码获得更长时间的运行时间。

  • 垃圾收集器期望的目标

  • 停顿时间

    停顿时间越短就越适合于用户交互的程序,良好的响应速度能提升用户体验。

  • 吞吐量

    高吞吐量则可以高效率利用CPU时间,尽快完成运算任务。适合在后台计算而不需要太多交互的任务。

  • 覆盖区

    在达到前面两点的情况下,尽量减少堆的内存空间,可以获得更好的空间局部性。

Serial Old收集器

Serial Old是Serial收集器的年老代版本,同样是一个单线程收集器,使用“标记-整理”算法

这个收集器的主要意义也是在于给Client模式下的虚拟机使用的。

在Server模式下,它还有两大用途:

  • 在JDK1.5之前的版本与Parallel Scavenge收集器搭配使用。
  • 作为CMS收集器的后备方案。(后面再说)

执行过程如下图:(书中原图)

img

img

-———————————————————————————————————————–

img

img

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本;

特点

  • 针对年老代;
  • 采用“标记-整理”算法;
  • 多线程收集;

应用场景

JDK1.6之后才出现,用来代替年老代的Serial Old收集器。

在Server模式、多CPU、这种注重吞吐量以及CPU资源敏感的场景,就有了Parallel Scavenge + Parallel Old收集器这样给力的组合。

设置参数

-XX:+UseParallelOldGC:指定使用Parallel Old收集器。

运行过程如下图:

img

img

CMS收集器

CMS并发标记清理收集器也被称为并发低停顿收集器或低延迟垃圾收集器。

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。

特点

针对年老代;

基于“标记-清除”算法;

以获取最短回收停顿时间为目标;

并发收集、低停顿;

需要更多的内存;

JDK1.5之后推出的第一款真正意义上的并发收集器;

第一次实现了让垃圾收集器与用户线程同时工作;

应用场景

与用户交互较多的场景;

希望系统停顿时间最短,注重服务的相应速度;提高用户体验。(常见web、b/s系统的服务上的应用)

设置参数

-XX:+UseConcMarkSweepGC: 指定使用CMS收集器

CMS收集器运作过程

CMS收集器的运行过程比较复杂,分文以下四步:

  • 初始标记

仅标记一下GC Roots能直接关联到的对象;

速度很快,但需要“Stop the World”

  • 并发标记

进行GC Roots Tracing的过程;

​ 应用程序也在运行;

​ 并不能保证可以标记出所有的存活对象;

  • 重新标记

为了修正并发标记期间因用户程序继续运行而导致标记变动的那一部分对象的标记记录。

​ 需要“Stop the World”,且停顿时间比初始标记稍长,但远比并发标记短。

​ 采用多线程并行执行来提升效率。

  • 并发清除

回收所有的垃圾对象。

​ 整个过程中耗时最长的并发标记和并发清除都可以与用户线程一起工作;

​ 所以总体上说,CMS收集器的内存回收过程与用户线程一起并发执行;

CMS收集器运行过程示意图

img

img

CMS收集器3个明显的缺点

  • 对CPU资源非常敏感

并发收集虽然不会暂停用户线程,但因为占用一部分CPU资源,还是会导致应用程序变慢,总吞吐量降低。

CMS默认的收集线程数量是 = (CPU数量 + 3)/4;

​ 当CPU数量多于4个,收集线程占用的CPU资源多于25%,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。

​ 注:针对这种情况,曾出现了“增量式并发收集器”。类似使用抢占式来模拟多任务机制的思想,让收集线程和用户线程交替运行,减少收集线程运行时间。不过效果不理想,JDK1.6之后不提倡使用。

  • 无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败
  1. 浮动垃圾

在并发清除时,用户线程新产生的垃圾,称为浮动垃圾; 这使得并发清除时需要预留一定的内存空间,不能像其他收集器一样,在年老代几乎填满再进行收集。 也可以认为CMS收集器所需要的空间比其他垃圾收集器大。

`-XX:CMSInitiatingOccupancyFraction`:设置CMS预留内存空间;

JDK1.5默认设置下,CMS收集器当年老代使用了68%的空间之后就会被激活。

JDK1.6大约 年老代使用了92%的空间之后,CMS收集器就会被激活。
  1. “Concurrent Mode Failure”失败

如果CMS预留内存空间无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败。这时JVM启动后备预案:临时启动Serial Old收集器,而导致另一次Full GC的产生。这样做的代价是很大的,所以CMSInitiatingOccupancyFraction不能设置的太大。

  • 产生大量内存碎片

由于CMS收集器基于“标记-清除”算法,清除后不能进行压缩操作。

​ 之前在讲“标记-清除”算法的时候,曾介绍过:产生大量不连续的内存碎片会导致分配大内存对象时,无法找到足够的连续内存,从而需要提前触发另一次Full GC动作。

解决方法

  1. -XX:+UseCMSCompactAtFullCollection

使得CMS出现上面这种情况时,不进行Full GC,而是开启内存碎片的合并并整理。但是合并整理过程无法并发,停顿时间会变长。默认开启(但不会进行,结合下面的CMSFullGCBeforeCompaction)

  1. -XX:+CMSFullGCBeforeCompaction

设置执行多少次不压缩的Full GC之后,来一次压缩整理。为了减少合并整理过程的停顿时间。默认为0,也就是说每次执行Full GC,不会进行压缩整理。由于空间不在连续,CMS需要使用可用“空闲列表”内存分配方式,这比简单实用的“碰撞指针”分配内存消耗大。

总的来看,与Parallel Old垃圾收集器相比,CMS减少了执行年老代垃圾收集时应用暂停的时间,但却增加了新生代垃圾收集时应用暂停的时间、降低了吞吐量,而且需要占用更大的堆空间。

G1收集器

G1是一款面向服务端应用的垃圾收集器。

特点

  • 并行与并发

能充分利用多CPU、多核环境下的硬件优势;

可以并行来缩短“Stop the World”停顿时间;

也可以并发让垃圾收集与用户程序同时进行;

  • 分代收集,收集范围包括新生代和年老代

能独立管理整个GC堆(新生代、年老代),而不需要与其他收集器搭配。

能够采用不同方式处理不同时期的对象。

虽然保留分代概念,但Java堆的内存布局有很大差别。

将整个堆划分为多个大小相等的独立区域(Region);

新生代和年老代不再是物理隔离,它们都是一部分Region(不需要连续)的集合。

以下是书中原文,关于Region的解释:

在G1之前的其他收集器进行收集的范围都是整个新生代或者年老代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和年老代的概念,但新生代和年老代不再是物理隔离得到了,它们都是一部分Region(不需要连续)的集合。

  • 结合多种垃圾收集算法、空间整合,不产生碎片

从整体来看,是基于标记-整理算法;

从局部(两个Region)来看,是基于复制算法;

两种算法都不会产生内存碎片,收集后能提供规整的可用内存,有利于长时间运行。

  • 可预测的停顿:低停顿的同时实现高吞吐量

  • G1除了追求低停顿外,还能建立可预测的停顿时间模型;

可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒

应用场景

面向服务端应用,针对具有大内存、多处理器的机器

最主要是为 需要 低GC延迟,并具有大堆的应用程序提供解决方案;

如:在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒。

用来替换掉JDK1.5中的CMS收集器

在下面的情况中,使用G1可能比CMS好:

  • 超过50%的Java堆被活动数据占用
  • 对象分配频率或年老代提升频率变化很大
  • GC停顿时间过长(长于0.5 至 1秒)

那么是否应要采用G1呢?也不一定:如果现在采用的收集器没有出现问题,不用着急去选择G1,如果应用程序追求低停顿,可以尝试选择G1;

是否代替CMS需要实际场景测试才知道

设置参数

-XX:UseG1GC:指定使用G1收集器。

-XX:InitiatingHeapOccupancyPercent:当整个Java堆的占用率达到参数值时,开始并发标记阶段,默认为45。

-XX:MaxGCPauseMillis:为G1收集器设置暂停时间目标,默认值为200毫秒

-XX:G1HeapRegionSize:设置每个Region大小,范围1MB到32MB;在最小Java堆时可以拥有大约2048个Region

为什么G1垃圾收集器可以实现“可预测”的停顿

G1垃圾收集器可以建立可预测得到停顿时间模式,是因为:

  • 可以有计划的避免 在Java堆进行全区域的垃圾收集。
  • G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表。
  • 每次根据允许的收集时间,优先回收价值最大的Region。

这样子,就保证了在有限的时间内可以获取尽可能高的收集效率。

G1收集器运作过程

G1垃圾收集器的运作过程与CMS比较相似。

  1. 初始标记

​ 仅标记一下GC Roots能直接关联到的对象。

​ 且修改TAMS(Next Top at Mark Sart),让下一阶段并发运行时,用户程序能在正确可用的Region中创建新对象。

​ 需要“Stop The World”,但速度很快。

  1. 并发标记

​ 进行GC Roots Tracing的过程。

​ 刚才产生的集合中标记出存活对象。

​ 耗时较长,但应用程序也在运行。

​ 并不能保证可以标记出所有的存活对象。

  1. 最终标记

为了修正并发标记期间,因用户程序继续运作,而导致标记变动的那一部分对象的标记记录。需要“Stop The World”,且停顿时间比初始标记稍长,但远比并发标记短。

​ 采用多线程并行执行来提高效率。

  1. 筛选回收
  • 首先排序各个Region的回收价值和成本;
  • 然后根据用户期望的GC停顿时间来制定回收计划;
  • 按计划回收一些价值高的Region中的垃圾对象;

回收时采用“复制”算法,从一个或多个Region复制存活对象到堆上的另一个空的Region,并且在此过程中压缩和释放内存;

可以并发运行,降低停顿时间,并增加吞吐量。

G1收集器运行过程如下图:

img

img

垃圾回收的触发条件

young gc

对于 young gc,触发条件似乎要简单很多,当 eden 区的内存不够时,就会触发young gc,我们看看在 eden 区给对象分配一块内存是怎样一个过程,画了一个简单的流程图,我一直觉得一个好的示意图可以让一个枯燥的过程变得更有意思。

img

img

在 eden 区分配空间内存不足时有两种情况,为对象分配内存、为TLAB分配内存,总之就是内存不够,需要进行一次 young gc 为eden区腾出空间为后续的内存申请做准备,然后由一个用户线程通知VM Thread,接下去要执行一次 young gc。

full gc

  • old gen 空间不足

当创建一个大对象、大数组时,eden 区不足以分配这么大的空间,会尝试在old gen 中分配,如果这时 old gen 空间也不足时,会触发 full gc,为了避免上述导致的 full gc,调优时应尽量让对象在 young gc 时就能够被回收,还有不要创建过大的对象和数组。统计得到的 young gc 晋升到 old gen的对象平均总大小大于old gen 的剩余空间

  • 当准备触发一次 young gc时,会判断这次 young gc 是否安全,这里所谓的安全是当前老年代的剩余空间可以容纳之前 young gc 晋升对象的平均大小,或者可以容纳 young gen 的全部对象,如果结果是不安全的,就不会执行这次 young gc,转而执行一次 full gc
  • perm gen 空间不足

如果有perm gen的话,当系统中要加载的类、反射的类和调用的方法较多,而且perm gen没有足够空间时,也会触发一次 full gc

  • ygc出现 promotion failure

promotion failure 发生在 young gc 阶段,即 cms 的 ParNewGC,当对象的gc年龄达到阈值时,或者 eden 的 to 区放不下时,会把该对象复制到 old gen,如果 old gen 空间不足时,会发生 promotion failure,并接下去触发full gc

在GC日志中,有时会看到 concurrent mode failure 关键字,这是因为什么原因导致的问题呢? 对这一块的理解,很多文章都是说因为 concurrent mode failure 导致触发full gc,其实应该反过来,是full gc 导致的 concurrent mode failure,在cms gc的算法实现中,通常说的cms是由一个后台线程定时触发的,默认每2秒检查一次old gen的内存使用率,当 old gen 的内存使用率达到-XX:CMSInitiatingOccupancyFraction设置的值时,会触发一次 cms gc,对 old gen 进行并发收集,而真正的 full gc 是通过 vm thread线程触发的,而且在判断当前ygc会失败的情况下触发full gc,如上一次ygc出现了promotion failure,如果执行 full gc 时,发现后台线程正在执行 cms gc,就会导致 concurrent mode failure

对于以上这些情况,CMSInitiatingOccupancyFraction参数的设置就显得尤为重要,设置的太大的话,发生CMS时的剩余空间太小,在ygc的时候容易发生promotion failure,导致 concurrent mode failure 发生的概率就增大,如果设置太小的话,会导致 cms gc 的频率会增加,所以需要根据应用的需求对该参数进行调优。

  • 执行 System.gc()jmap -histo:live <pid>jmap -dump

可达性检测

  • 引用计数:一种在jdk1.2之前被使用的垃圾收集算法,我们需要了解其思想。其主要思想就是维护一个counter,当counter为0的时候认为对象没有引用,可以被回收。缺点是无法处理循环引用。目前iOS开发中的一个常见技术ARC(Automatic Reference Counting)也是采用类似的思路。在当前的JVM中应该是没有被使用的。
  • 根搜算法:思想是从gc root根据引用关系来遍历整个堆并作标记,称之为mark,等会在具体收集器中介绍并行标记和单线程标记。之后回收掉未被mark的对象,好处是解决了循环依赖这种『孤岛效应』。这里的gc root主要指:
  • 虚拟机栈(栈桢中的本地变量表)中的引用的对象
  • 方法区中的类静态属性引用的对象
  • 方法区中的常量引用的对象
  • 本地方法栈中JNI的引用的对象

整理策略

  • 复制:主要用在新生代的回收上,通过from区和to区的来回拷贝。需要特定的结构(也就是Young区现在的结构)来支持,对于新生成的对象来说,频繁的去复制可以最快的找到那些不用的对象并回收掉空间。所以说在JVM里YGC一定承担了最大量的垃圾清除任务。
  • 标记清除/标记整理:主要用在老生代回收上,通过根搜的标记然后清除或者整理掉不需要的对象。

垃圾收集的意义

  • 垃圾收集的出现解放了C++中手工对内存进行管理的大量繁杂工作,手工malloc,free不仅增加程序复杂度,还增加了bug数量。
  • 分代收集。即在新生代和老生代使用不同的收集方式。在垃圾收集上,目标主要有:加大系统吞吐量(减少总垃圾收集的资源消耗);减少最大STW(Stop-The-World)时间;减少总STW时间。不同的系统需要不同的达成目标。而分代这一里程碑式的进步首先极大减少了STW,然后可以自由组合来达到预定目标。

参考资料

http://blog.csdn.net/Simba_cheng/article/details/78218541?locationNum=5&fps=1

http://www.jianshu.com/p/c9ac99b87d56

https://mp.weixin.qq.com/s/HS1VT9ww7XOWqKciUOhwUw

坚持原创技术分享,您的支持将鼓励我继续创作!