【JVM从小白学成大佬】5.垃圾收集器及内存分配策略

看完文章,若是以为还能够,也请各位小伙伴点个“赞👍”,谢谢🙏

前面介绍了垃圾回收算法,接下来咱们介绍垃圾收集器和内存分配的策略。有没有一种牛逼的收集器像银弹同样适配全部场景?很明显,不可能有,否则我也不必单独搞一篇文章来介绍垃圾收集器了。熟悉不一样收集器的优缺点,在实际的场景中灵活运用,才是王道。html

在开始介绍垃圾收集器前,咱们能够剧透几点:java

  • 根据不一样分代的特色,收集器可能不一样。有些收集器能够同时用于新生代和老年代,而有些时候,则须要分别为新生代或老年代选用合适的收集器。通常来讲,新生代收集器的收集频率较高,应选用性能高效的收集器;而老年代收集器收集次数相对较少,对空间较为敏感,应当避免选择基于复制算法的收集器。
  • 在垃圾收集执行的时刻,应用程序须要暂停运行
  • 能够串行收集,也能够并行收集。
  • 若是能作到并发收集(应用程序没必要暂停),那绝对是很妙的事情。
  • 若是收集行为可控,那也是很妙的事情。
  • 默认收集器算法

    • jdk1.7,1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
    • jdk1.9 默认垃圾收集器G1

但愿你们带着下面的问题进行阅读,有目标的阅读,可能收获更多。

  1. 为何没有一种牛逼的收集器像银弹同样适配全部场景?
  2. CMS和G1的对比,你知道他两的区别吗?
  3. 为何CMS只能用做老年代收集器,而不能应用在新生代的收集?
  4. 为何JVM的分代年龄是15?而不是16,20之类的呢?
  5. “动态对象年龄断定”里有个“天坑”哦,是啥坑呢?

1 垃圾收集器

GC线程与应用线程保持相对独立,当系统须要执行垃圾回收任务时,先中止工做线程,而后命令GC线程工做。以串行模式工做的收集器,称为串行收集器(即Serial Collector)。与之相对的是以并行模式工做的收集器,称为并行收集器(即Paraller Collector)segmentfault

1.1 串行收集器:Serial

串行收集器采用单线程方式进行收集,且在GC线程工做时,系统不容许应用线程打扰。此时,应用程序进入暂停状态,即Stop-the-world。数组

Stop-the-world暂停时间的长短,是度量一款收集器性能高低的重要指标。安全

是针对新生代的垃圾回收器,基于标记-复制算法多线程

1.2 并行收集器:ParNew

并行收集器充分利用了多处理器的优点,采用多个GC线程并行收集。可想而知,多条GC线程执行显然比只使用一条GC线程执行的效率更高。通常来讲,与串行收集器相比,在多处理器环境下工做的并行收集器可以极大地缩短Stop-the-world时间。并发

针对新生代的垃圾回收器,标记-复制算法,能够当作是Serial的多线程版本oracle

1.3 吞吐量优先收集器:Parallel Scavenge

针对新生代的垃圾回收器,标记-复制算法,和ParNew相似,但更注重吞吐率。在ParNew的基础上演化而来的Parallel Scanvenge收集器被誉为“吞吐量优先”收集器。吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。如虚拟机总运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。app

Parallel Scanvenge收集器在ParNew的基础上提供了一组参数,用于配置指望的收集时间或吞吐量,而后以此为目标进行收集。

经过VM选项能够控制吞吐量的大体范围:

  • -XX:MaxGCPauseMills:指望收集时间上限。用来控制收集对应用程序停顿的影响。
  • -XX:GCTimeRatio:指望的GC时间占总时间的比例,用来控制吞吐量。
  • -XX:UseAdaptiveSizePolicy:自动分代大小调节策略。

但要注意停顿时间与吞吐量这两个目标是相悖的,下降停顿时间的同时也会引发吞吐的下降。所以须要将目标控制在一个合理的范围中。

1.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,单线程收集器,使用标记-整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

1.5 Parallel Old收集器

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

1.6 CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器

CMS收集器仅做用于老年代的收集,是基于标记-清除算法的,它的运做过程分为4个步骤:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 从新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

其中,初始标记、从新标记这两个步骤仍然须要Stop-the-world。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而从新标记阶段则是为了修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始阶段稍长一些,但远比并发标记的时间短。

CMS以流水线方式拆分了收集周期,将耗时长的操做单元保持与应用线程并发执行。只将那些必需STW才能执行的操做单元单独拎出来,控制这些单元在恰当的时机运行,并能保证仅需短暂的时间就能够完成。这样,在整个收集周期内,只有 两次短暂的暂停(初始标记和从新标记)达到了近似并发的目的

CMS收集器优势:并发收集、低停顿。

CMS收集器缺点

  • CMS收集器对CPU资源很是敏感。
  • CMS收集器没法处理浮动垃圾(Floating Garbage)。
  • CMS收集器是基于标记-清除算法,该算法的缺点都有。

CMS收集器之因此可以作到并发,根本缘由在于采用基于“标记-清除”的算法并对算法过程进行了细粒度的分解。前面篇章介绍过标记-清除算法将产生大量的内存碎片这对新生代来讲是难以接受的,所以新生代的收集器并未提供CMS版本。

1.7 G1收集器

G1从新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么作的目的是在进行收集时没必要在全堆范围内进行,这是它最显著的特色。区域划分的好处就是带来了停顿时间可预测的收集模型:用户能够指定收集操做在多长时间内完成。即G1提供了接近实时的收集特性。

G1与CMS的特征对好比下:

特征 G1 CMS
并发和分代
最大化释放堆内存
低延时
吞吐量
压实
可预测性
新生代和老年代的物理隔离

G1具有以下特色:

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优点,使用多个CPU来缩短Stop-the-world停顿的时间,部分其余收集器原来须要停顿Java线程执行的GC操做,G1收集器仍然能够经过并发的方式让Java程序继续运行。
  • 分代收集
  • 空间整合:与CMS的标记-清除算法不一样,G1从总体来看是基于标记-整理算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。但不管如何,这两种算法都意味着G1运做期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会由于没法找到连续内存空间而提早触发下一次GC
  • 可预测的停顿:这是G1相对于CMS的一个优点,下降停顿时间是G1和CMS共同的关注点。

在G1以前的其余收集器进行收集的范围都是整个新生代或者老年代,而G1再也不是这样。在堆的结构设计时,G1打破了以往将收集范围固定在新生代或老年代的模式,G1将堆分红许多相同大小的区域单元,每一个单元称为Region。Region是一块地址连续的内存空间,G1模块的组成以下图所示:

G1堆的Region布局.png

G1收集器将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,它们都是一部分Region(不须要连续)的集合。G1收集器之因此能创建可预测的停顿时间模型,是由于它能够有计划地避免在整个Java堆中进行全区域的垃圾收集。G1会经过一个合理的计算模型,计算出每一个Region的收集成本并量化,这样一来,收集器在给定了“停顿”时间限制的状况下,老是能选择一组恰当的Regions做为收集目标,让其收集开销知足这个限制条件,以此达到实时收集的目的。

对于打算从CMS或者ParallelOld收集器迁移过来的应用,按照官方 的建议,若是发现符合以下特征,能够考虑更换成G1收集器以追求更佳性能:

  • 实时数据占用了超过半数的堆空间;
  • 对象分配率或“晋升”的速度变化明显;
  • 指望消除耗时较长的GC或停顿(超过0.5——1秒)。

原文以下:
Applications running today with either the CMS or the ParallelOld garbage collector would benefit switching to G1 if the application has one or more of the following traits.

  • More than 50% of the Java heap is occupied with live data.
  • The rate of object allocation rate or promotion varies significantly.
  • Undesired long garbage collection or compaction pauses (longer than 0.5 to 1 second)

G1收集的运做过程大体以下:

  • 初始标记(Initial Marking):仅仅只是标记一下GC Roots能直接关联到的对象,而且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中建立新对象,这阶段须要停顿线程,但耗时很短
  • 并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。
  • 最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运做而致使标记产生变更的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段须要把Remembered Set Logs的数据合并到Remembered Set中,这阶段须要停顿线程,可是可并行执行
  • 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所指望的GC停顿时间来制定回收计划。这个阶段也能够作到与用户程序一块儿并发执行,可是由于只回收一部分Region,时间是用户可控制的,并且停顿用户线程将大幅提升收集效率。

咱们能够看下官方文档对G1的展望(这段英文描述比较简单,我就不翻译了):

Future:
G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS). Comparing G1 with CMS, there are differences that make G1 a better solution. One difference is that G1 is a compacting collector. G1 compacts sufficiently to completely avoid the use of fine-grained free lists for allocation, and instead relies on regions. This considerably simplifies parts of the collector, and mostly eliminates potential fragmentation issues. Also, G1 offers more predictable garbage collection pauses than the CMS collector, and allows users to specify desired pause targets.

2 内存分配策略

对象的内存分配,往大方向上讲,就是在上分配(但也可能通过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,若是启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数状况下可能会直接分配在老年代中。

2.1 对象优先在Eden分配

大多数状况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(前面篇章中有介绍过Minor GC)。但也有一种状况,在内存担保机制下,没法安置的对象会直接进到老年代。

2.2 大对象直接进入老年代

大对象时指须要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。目的就是避免在Eden区及两个Survivor区之间发生大量的内存复制。

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

虚拟机给每一个对象定义了一个对象年龄(Age)计数器。若是对象在Eden出生并通过第一次Minor GC后仍然存活,而且能被Survivor容纳的话,将被移动到Survivor空间中,而且对象年龄设为1 。对象在Survivor区中没通过一次Minor GC,年龄就加1岁,当年龄达到15岁(默认值),就会被晋升到老年代中。

对象晋升老年代的年龄阈值,能够经过参数-XX: MaxTenuringThreshold设置。

接下来咱们来回答为何JVM的分代年龄为何是15?而不是16,20之类的呢?

真的不是为何不能是其它数(除了15),着实是臣妾作不到啊!

事情是这样的,HotSpot虚拟机的对象头其中一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark word”。

例如,在32位的HotSpot虚拟机中,若是对象处于未被锁定的状态下,那么Mark Word的32bit空间中25bit用于存储对象哈希码,4bit用于存储对象分代年龄,2bit用于存储锁标志位,1bit固定为0 。

明白是什么缘由了吗?对象的分代年龄占4位,也就是0000,最大值为1111也就是最大为15,而不可能为16,20之类的了。

2.4 动态对象年龄断定

为了能更好的适应不一样程序的内存情况,虚拟机并非永远地要求兑现过的年龄必须达到了MaxTenuringThreshold才能晋升老年代。

知足以下条件之一,对象能晋升老年代:

  • 1.对象的年龄达到了MaxTenuringThreshold(默认15)能晋升老年代。
  • 2.若是在Survivor空间中相同年龄全部对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

不少文章都只是注意到了上面描述的状况(包括阿里中间件公众号发的一篇文章里也只是这么简单的介绍,当时给它们后台留过言说明状况),但若是只是这么认识的话,会发如今实际的内存回收中有悖于此条规定。

举个小栗子,如对象年龄5的占30%,年龄6的占36%,年龄7的占34%,按那两个标准,对象是不能进入老年代的,但Survivor都已经100%了啊

你们能够关注这个参数TargetSurvivorRatio,目标存活率,默认为50%。大体意思就是说年龄从小到大累加,如加入某个年龄段(如栗子中的年龄6)后,总占用超过Survivor空间*TargetSurvivorRatio的时候,从该年龄段开始及大于的年龄对象就要进入老年代(即栗子中的年龄6对象,就是年龄6和年龄7晋升到老年代)。动态对象年龄判断,主要是被TargetSurvivorRatio这个参数来控制。并且算的是年龄从小到大的累加和,而不是某个年龄段对象的大小。

2.5 空间分配担保

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

上面说的风险是什么呢?咱们知道,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来做为轮换备份,所以当出现大量对象在Minor GC后仍然存活的状况(最极端的状况就是内存回收后新生代中全部对象都存活),就须要老年代进行分配担保,把Survivor没法容纳的对象直接进入老年代。

3 总结脑图

内存分配策略.png

脑图太大,如需高清完整大图,请留言告知。
相关文章
相关标签/搜索