深入理解JVM(五)----七种垃圾收集器和内存分配、回收策略

一:七种垃圾收集器

新生代收集器

  • Serial
  • ParNew
  • parallel

老年代收集器

  • Serial Old
  • CMS
  • parallel Old

新生代和老年代收集器

  • G1

几种垃圾收集器是相互配合使用,连线代表可以写作使用。
垃圾收集器

二、新生代垃圾收集器

Serial

概述:Serial是一类用于新生代的单线程收集器,采用复制算法进行垃圾收集。Serial进行垃圾收集时,不仅只用一条单线程执行垃圾收集工作,它还在收集的同时,所用的用户必须暂停。其执行过程如下图所示
在这里插入图片描述
从上图可知当应用程序进行到一个安全点的时候,所有的线程全都暂停(stop the world),等到GC完成后,应用程序线程继续执行。这就像是你一边扫地,旁边要是有人一边嗑瓜子,那你这要一直扫下去的节奏,只能先让他别吃了,然后你才能干活。stop the world虽然说是带来了不好的用户体验,但是在用户的桌面应用场景中,虚拟机内存一般不是很大,每次gc时间可以控制在一百多微妙内,只要不是频繁发生,是可以接受的行为。

  • 优势:简单高效(比起其他收集器的单线程),对单个cpu环境来说没有了上下文之间的的切换,效率比较高。
  • 缺点:会在用户不知道的情况下停止所有工作线程,用户体验感极差,令人难以接受。
  • 适用场景:Client 模式(桌面应用);单核服务器。
  • 参数: 可以使用命令如下开启Serial作为新生代收集器
-XX:+UserSerialGC #选择Serial作为新生代垃圾收集器

ParNew

概述:parNew收集器其实就是Serial的一个多线程版本,除了使用多线程进行垃圾收集以外,其他行为和其在单核cpu上的表现并不会比Serail收集器更好,在多核机器上,其默认开启的收集线程数与cpu数量相等。可以通过如下命令进行修改

-XX:ParallelGCThreads #设置JVM垃圾收集的线程数  
在cpu非常多的情况下使用,避免开启太多的垃圾收集线程

如下是ParNew收集器和Serial Old 收集器结合进行垃圾收集的示意图.
在这里插入图片描述
优点:充分利用多cpu的优势
缺点:和Serial一样
场景:运行在Server模式下的虚拟机中首选的新生代收集器。一个重要的原因就是除了Serial以外,目前只有ParNew可以和CMS收集器配合工作。在多核情况下,当然首选后两者结合。使用CMS后默认的新生代收集器是ParNew,也可以使用一下参数清醒指定。

-XX:UseParNewGC #新生代采用ParNew收集器  

Parrallel Scavenge收集器

概述:Parallel Scavenge也是一款用于新生代的多线程收集器,也是采用复制算法。那么它和ParNew的区别在哪里?

与ParNew的不同之处在于 Parallel Scavenge收集器的目的是达到一个可控制的吞吐量,而CMS等收集器关注点在于尽可能的缩短垃圾收集时用户线程的停顿时间。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值, 即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。

例如虚拟机一共运行了 100 分钟,其中垃圾收集花费了 1 分钟,那吞吐量就是 99% 。比如下面两个场景,垃圾收集器每 100 秒收集一次,每次停顿 10 秒,和垃圾收集器每 50 秒收集一次,每次停顿时间 7 秒,虽然后者每次停顿时间变短了,但是总体吞吐量变低了,CPU 总体利用率变低了。其与Parallel Old收集器运行示意图如下
在这里插入图片描述
停顿时间越短就越适合需要和用户交互的程序,响应速度快能提升用户体验;而高吞吐量则可以高效利用CPU时间,适用于后台运算量大而不需要太多交互的任务。
优点:追求高吞吐量,高效利用CPU,是吞吐量优先,且能进行精确控制。
适用场景:注重吞吐量高效利用CPU,需要高效运算,且不需要太多交互。
一些参数:

  • -XX:MaxGCPauseMilis。 控制最大垃圾收集停顿时间,参数值是一个大于0的毫秒数,收集器尽可能保证回收花费时间不超过设定值。但将这个值调小,并不一定会使系统垃圾回收速度更快,GC停顿时间是以牺牲吞吐量和新生代空间换来的。
  • -XX:GCTimeRadio。设置吞吐量大小,参数值是一个(0,100)两侧均为开区间的整数。也是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。若把参数设置为19,则允许的最大GC时间就占总时间的5%(1/(1+19))。默认值是99,即允许最大1%的垃圾收集时间。
  • -XX:+UserAdaptiveSizePolicy。这是一个开关函数,当打开这个函数,就不需要手动指定新生代的大小,Eden与Survivor区的比例(-XX:SurvivorRatio,默认是8:1:1),晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等参数。JVM会动态调整这些参数,以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略.

老年代垃圾收集器

Serial Old

概念:Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。下图是Serial收集器与Serial Old收集器的运行示意图。、
在这里插入图片描述
适用场景:Client模式下的虚拟机使用。
如果在Server模式下还有两大用途

  • JDK1.5之前于Parallel Scavenge配合使用
  • 作为CMS的后备预案,在并发收集发生concurrent mode failure时启用。

Parallel Old

概念:Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,可以充分利用多核CPU的计算能力。下图是两种收集器合作的运行示意图
在这里插入图片描述
适用场景:注重吞吐量与CPU资源敏感的场合,与Parallel Scavenge 收集器搭配使用,jdk7和jdk8默认使用该收集器作为老年代收集器。使用参数进行指定

-XX:+UserParallelOldGC

CMS

概念:CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。采用的算法是“标记-清除”,运作过程分为四个步骤

  • 初始标记,标记GC Roots 能够直接关联到达对象
  • 并发标记,进行GC Roots Tracing 的过程
  • 重新标记,修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录
  • 并发清除,用标记清除算法清除对象。

其中初始标记和重新标记这两个步骤仍然需要"stop the world"。耗时最长的并发标记与并发清除过程收集器线程都可以与用户线程一起工作,总体上来说CMS收集器的内存回收过程是与用户线程一起并发执行的。下图是CMS运行示意图。
在这里插入图片描述
优点:并发收集,低停顿
缺点:

  • CMS收集器对CPU资源非常敏感。CMS默认启动对回收线程数(CPU数量+3)/4,当CPU数量在4个以上时,并发回收时垃圾收集线程不少于25%,并随着CPU数量的增加而下降,但当CPU数量不足4个时,对用户影响较大。

  • CMS无法处理浮动垃圾,可能会出现“Concurrent Mode Failure”失败而导致一次FullGC的产生。由于CMS并发清理阶段用户线程还在运行,伴随程序运行自然还会有新的垃圾产生,这部分垃圾出现在标记过程之后,CMS无法在当次处理掉,只能等到下一次GC,这部分垃圾就是浮动垃圾。同时也由于在垃圾收集阶段用户线程还需要运行,那也就需要预留足够的内存空间给用户线程使用,因此CMS收集器不能像其他老年代几乎完全填满再进行收集。
    要是CMS预留的内存无法满足程序需要,那么就会导致一次“Concurrent Mode Failure”失败,此时虚拟机将启用后备预案,临时用SerialOld来重新进行老年代的垃圾收集。这样的话停顿时间就很长了。

可以通过参数-XX:CMSInitiatingOccupancyFraction修改CMS触发的百分比。
通过参数可以进行优化。设置太低会频繁发生CMS,设置过高则会导致频繁的Concurrent Mode Failure,效率反而低。

  • 因为CMS采用的是标记清除算法,因此垃圾回收后会产生空间碎片。使用参数进行优化
-XX:UserCMSCompactAtFullCollection #开启碎片整理(默认是开的)
-XX:CMSFullGCsBeforeCompaction #执行多少次不压缩的Full GC之后,跟着来一次压缩的Full GC

新生代和老年代收集器G1

概念: G1收集器是一款面向服务端应用的垃圾收集器,目前是JDK9的默认垃圾收集器。与其他收集器相比,G1具有如下特点。

  • 并行与并发。G1能充分利用多CPU,多核环境下的硬件优势。
  • 分代收集。能够采用不同的方式去处理新创建的对象和已经存活了一段时间的对象,不需要与其他收集器进行合作。
  • 空间整合。G1从整体上来看基于“标记-整理”算法实现的收集器,从局部上看是基于复制算法实现的,因此G1运行期间不会产生空间碎片。
  • 可预测的停顿。G1能建立可预测的时间停顿模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

G1收集器之前的收集器进行收集的范围是整个新生代或者老年代,而G1则不是这样。它将Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但两者之间不是物理隔离的。他们都是一部分Region(不需要连续)的集合。下图是Java堆的划分示意图。
在这里插入图片描述
每一个方块就是一个区域,每个区域可能是 Eden、Survivor、老年代,每种区域的数量也不一定。JVM 启动时会自动设置每个区域的大小(1M ~ 32M,必须是 2 的次幂),最多可以设置 2048 个区域(即支持的最大堆内存为 32M*2048 = 64G),假如设置 -Xmx8g -Xms8g,则每个区域大小为 8g/2048=4M。

G1收集器可以有计划地避免在整个Java堆全区域的垃圾收集。G1可以跟踪各个Region里面垃圾堆积的价值大小(回收所获得的空间大小及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,收集加载最大的region,这种方式保证了有限时间内可以获取尽可能多高的收集效率。

为了在 GC Roots Tracing 的时候避免扫描全堆,在每个 Region 中,都有一个 Remembered Set 来实时记录该区域内的引用类型数据与其他区域数据的引用关系(在前面的几款分代收集中,新生代、老年代中也有一个 Remembered Set 来实时记录与其他区域的引用关系),在标记时直接参考这些引用关系就可以知道这些对象是否应该被清除,而不用扫描全堆的数据。

下图是G收集器运行示意图。从图中可知G1收集器收集器收集过程有初始标记、并发标记、最终标记、筛选回收(筛选回收价值最大的),和 CMS 收集器前几步的收集过程很相似:
在这里插入图片描述

  • 初始标记。标记出GC Roots直接关联的对象,这个阶段速度较快,需要停止用户线程,单线程执行。
  • 并发标记。从 GC Root 开始对堆中的对象进行可达新分析,找出存活对象,这个阶段耗时较长,但可以和用户线程并发执行。
  • 最终标记。修正在并发标记阶段引用户程序执行而产生变动的标记记录。
  • 筛选回收。选回收阶段会对各个 Region 的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来指定回收计划(用最少的时间来回收包含垃圾最多的区域,这就是 Garbage First ,第一时间清理垃圾最多的区块),这里为了提高回收效率,并没有采用和用户线程并发执行的方式,而是停顿用户线程。

适用场景:要求尽可能可控 GC 停顿时间;内存占用较大的应用。可以用
-XX:+UseG1GC 使用 G1 收集器,jdk9 默认使用 G1 收集器

并发、并行

上面说到的很多垃圾收集器是并发和并行的。那么在垃圾收集器工作过程中并发和并行指的是什么?
并发(Parallel):多条垃圾收集线程并行工作,但此时用户线程是等待状态
并发(Concurrent):用户线程和垃圾收集线程同时执行(但不一定并行,可能是交叉执行),用户程序继续运行,而垃圾收集程序运行在另一个CPU上。

内存分配和回收策略

java的自动内存管理解决了两个问题:内存的分配和回收内存。
内存的分配,在大方向上讲,就是在堆上分配(也可能通过JIT编译后被拆散为标量类型并间接在栈上分配<标量替换> ),主要分配在Eden区,如果开启了本地线程分配缓冲,则按线程优分配在TLAB上。少数情况会直接分配在老年代,分配情况取决于多个因素。

对象优先在Eden区分配

如果Eden区空间不够,则发起一次MinorGC。

大对象直接进入老年代

大对象是指,需要大量连续内存空间的Java对象(比如很长的字符床,数组)。大对象对虚拟机来说是一个坏消息(尤其是短命的大对象),会导致虚拟机还有很多内存,但是不得不提前触发GC以获取足够的内存去安置它们。

长期存活的对象进入老年代

对象头有个分代年龄还记得么?当对象经过一次MinorGC后仍存活且能被Survior容纳,这个Age就+1,当到一定年龄时(默认15),会晋升到老年代。这个年龄可以通过参数设置-XX:MaxTenuringThreshold

动态对象年龄判定

当然为了更好的适应不同程序的内存情况,虚拟机并不是必须到了最大年龄才进入老年代,如果Survivor空间中相同年龄所有对象的大小总和大于Survivor空间的一半,那么年龄大于或等于该年龄的对象就可以直接进入老年代。

内存分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlerPromotionFailure设置是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。如果大于,将尝试着进行一次Minor GC,尽管这次GC是有风险的。如果小于,或者HandlerPromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC了。

冒险是冒了什么样的风险?
前面提到过,新生代使用复制收集算法,但是为了内存利用率。只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况(最极端的情况是内存回收之后,新生代中所有的对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象存活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败。如果出现HandlerPromotionFailure失败,那就只好在失败后重新发起一次FULL GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是将HandlerPromotionFailure开关打开,避免Full GC过于频繁。

参考文章:
《深入理解Java虚拟机》(周志明 著)
文中很多内容都是来自这本书然后+一些个人理解吧
垃圾收集器和内存策略
垃圾收集器
垃圾回收机制

java的逃逸分析请看这里,感觉写的很棒啊
java逃逸分析

猜你喜欢

转载自blog.csdn.net/machine_Heaven/article/details/104617461