Java虚拟机垃圾回收相关知识点全梳理(下)

1、前言

上一篇文章《Java虚拟机垃圾回收相关知识点全梳理(上)》我整理分享了JVM运行时数据区域的划分,垃圾断定算法以及垃圾回收算法,各类算法的适用场景。今天,我整理分享下JVM性能的度量指标,垃圾收集器的分类,最后分享一下JVM的调优建议。html

2、性能度量指标

  • 吞吐量:表示系统减去系统回收时间占总时间的比率,好比系统运行了100秒,垃圾回收占用了1秒,那么吞吐量量就是(100-1)/100=99%。java

  • 垃圾回收消耗:和吞吐量相反,垃圾回收器消耗指垃圾回收器耗时与系统运行总时间的比值。git

  • 停顿时间:指垃圾回收器运行时,系统停顿的时间。github

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

  • 反应时间:当一个内存对象被标记为垃圾对象后到这个对象被真正回收产生的时间。windows

根据这几个指标,咱们能够知道,垃圾回收性能好的表现是:吞吐量高,垃圾回收消耗低,停顿时间少,回收频率低,反应时间快。可是,并无这么完美的性能表现,这几个指标有些是互斥的,好比要下降回收频率,就要扩大空间,可是就会增长停顿时间;一样要想反应时间快,就必需要提升回收频率。因此,这些性能的追求就是一个博弈平衡的过程,咱们能够根据咱们追求的某一方面来进行调优,好比,对于客户端应用而言,应该尽量下降其停顿时间,给用户良好的使用体验,为此,能够牺牲垃圾回收的吞吐量;对服务端程序来讲,可能会更加关注吞吐量。bash

3、垃圾回收器

3.1 Serial 收集器

Serial 收集器是全部垃圾收集器中最古老的一种,也是JDK中最基本的垃圾收集器之一。Serial回收器主要有两个特色:第一:使用单线程进行垃圾回收;第二:独占式垃圾回收。网络

在串行收集器进行垃圾回收时,Java应用程序中的线程都须要暂停,等待垃圾回收完成。这种现象成为Stop-The-World。它将形成很是糟糕的用户体验,在实时性要求较高的应用场景中,这种现象每每是不能被接受的,可是它依然是在Client模式下默认的新生代收集器。在单核CPU环境下,因为没有线程间的切换,它甚至比并发收集器的性能都要好。(如下图片来源于网络)数据结构

图片来源于网络

3.2 ParNew 收集器

ParNew 收集器是Serial 收集器的多线程版本。它的回收策略、算法以及参数和串行回收器同样。它是许多Server模式下新生代首选的收集器,除了他的多线程回收功能外,还有一点的就是只有他能与CMS收集器配合工做。开启ParNew 收集器可使用如下参数:多线程

-XX:+UseParNewGC:新生代使用并行收集器,老年代使用串行回收器。

-XX:+UseConcMarkSweepGC:新生代使用并行回收器,老年代使用CMS。

并行收集器工做时的线程数量可使用 -XX:ParallelGCThreads 参数指定。通常最好与CPU数量至关,避免过多的线程数,影响垃圾收集性能。 在默认状况下,当CPU数量小于8个时,ParallelGCThreads 的值等于 CPU 数量;当 CPU 数量大于8个时,ParallelGCThreads 的值等于 3+[(5*CPU_Count)/8]

图片来源于网络

3.3 Parallel Scavenge 收集器

Parallel Scavenge 收集器是新生代收集器,它是使用复制算法的收集器,同时也是多线程收集器。它和其余并发收集器不一样的点是,Parallel Scavenge 收集器 关注吞吐量,其余的并行收集器关注的是下降停顿时间。 开启Parallel Scavenge 收集器可使用如下参数:

-XX:+UseParallelGC:新生代使用并行回收收集器,老年代使用串行回收器。

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

并行回收收集器提供了两个重要的参数用于控制系统的吞吐量:

-XX:+MaxGCPauseMills:设置最大垃圾收集停顿时间,它的值是一个大于 0 的整数。收集器在工做时会调整 Java 堆大小或者其余一些参数,尽量地把停顿时间控制在 MaxGCPauseMills 之内。这里须要注意的是若是但愿减小停顿时间,而把这个值设置得很是小,虚拟机为了达到预期的停顿时间,JVM 可能会使用一个较小的堆 (一个小堆比一个大堆回收快),而这将致使垃圾回收变得很频繁,从而增长了垃圾回收总时间,下降了吞吐量。

-XX:+GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。好比 GCTimeRatio 等于 19,则系统用于垃圾收集的时间不超过 1/(1+19)=5%。默认状况下,它的取值是 99,即不超过 1%的时间用于垃圾收集。

除此以外,Parallel Scavenge 收集器与ParNew 收集器另外一个不一样之处在于,前者支持一种自适应的 GC 调节策略,使用-XX:+UseAdaptiveSizePolicy 能够打开自适应 GC 策略。在这种模式下,新生代的大小、eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。在手工调优比较困难的场合,能够直接使用这种自适应的方式,仅指定虚拟机的最大堆、目标的吞吐量 (GCTimeRatio) 和停顿时间 (MaxGCPauseMills),让虚拟机本身完成调优工做。

3.4 Serial Old 收集器

Serial Old 收集器是Serial收集器的老年代版本,从名字咱们就能够知道,它是一个单线程收集器,使用“标记-整理”算法。该虚拟机的主要使用场景是在Client模式下使用。它是CMS收集器的后备方案,当CMS收集器进行收集的时候,发生了Concurrent Mode Failure时,会触发使用Serial Old 收集器进行Full GC,此时会带来长时间的STW,进而影响系统响应,这也是CMS收集器的一个缺点。

图片来源于网络

3.5 Parallel Old 收集器

Parallel Old 收集器也是一种多线程并发的收集器。和Parallel Scavenge 收集器同样,它也是一种关注吞吐量的收集器。Parallel Old 收集器使用标记-压缩算法。

图片来源于网络

3.6 CMS(Concurrent Mark Sweep) 收集器

CMS 收集器是一个以获取最大回收停顿时间为目标的收集器,CMS垃圾回收的过程主要分为5步:初始标记、并发标记、从新标记、并发清除和并发重置。其中初始标记和从新标记是须要进行“Stop The World”,而并发标记、并发清除和并发重置是能够和用户线程一块儿执行的。所以,从总体上来讲,CMS 收集不是独占式的,它能够在应用程序运行过程当中进行垃圾回收 。CMS收集器也有三大缺点:

  • 对CPU资源比较敏感,在并发阶段,虽然不会致使用户线程停顿,可是仍是会占用部分CPU资源,从而致使程序变慢,吞吐量降低。
  • CMS没法处理浮动垃圾,由于CMS进行垃圾收集是和用户线程一块儿运行的,因此在收集的过程当中就会产生垃圾,这部分垃圾就被称为浮动垃圾,浮动垃圾只能等待下一次垃圾收集期间进行收集。由于垃圾收集过程与用户线程一块儿运行,因此收集过程当中仍是要预留空间给用户线程使用,若是空间不够,就会出现“Concurrent Mode Failure” 失败,接着就会出现备选方案的Serial Old收集器进行Full Gc,会进行长时间的停顿,进而影响性能。
  • CMS收集器是“标记-清除”算法的收集器,因此在垃圾收集事后会带来大量的内存碎片,CMS提供了一种内存压缩参数+XX:+UseCMSCompactAtFullCollection(默认是开启的)开启后CMS会在进行Full GC 的时候进行内存整理,+XX:CMSFullGCsBeforeCompaction能够设置执行多少次不压缩内存后再进行压缩的Full GC。

来源于网络

3.7 G1(Garbage-First) 收集器

G1收集器是一款面向服务端的垃圾收集器,在jdk1.7后能够正式使用,能够经过命令-XX:+UnlockExperimentalVMOptions –XX:+UseG1G来启用G1收集器。G1收集器采用的是“标记-整理”算法,它也是一个进行能够预测停顿时间的垃圾收集器。能够经过参数设置停顿时间:

-XX:MaxGCPauseMills = 20

-XX:GCPauseIntervalMills = 200。
复制代码

以上参数指定在200ms内,停顿时间不超过20ms。这两个参数是G1回收器的目标,G1回收器并不保证能执行它们。 G1收集器的区域分布以下图所示:

图片来源于网络

在G1中把java堆分红了多个大小相等的独立区域(Region),虽然保留了新生代和老年代的概念,可是他们都不是物理隔离的,只是逻辑上还有区分。

G1收集器进行垃圾收集分为4个阶段,初始标记,并发标记,最终标记,筛选回收。初始标记须要停顿用户线程,可是时间很短;并发标记是从GC Roots对堆中的对象进行可达性分析,这个阶段比较耗时,可是能够与用户线程并发执行;最终标记是修正在并发标记中产生的变更;筛选回收就是对标记好的垃圾对象进行价值和成本排序,根据用户设定的指望来进行回收(好比咱们上面设置的200ms停顿时间不超过20ms)。

3.8 ZGC(Z Garbage Collector) 收集器

ZGC 被称为“一个可伸缩低延迟的垃圾回收器”,这个垃圾回收器有什么神奇之处呢?它的主要特色就是能把回收时间控制在10ms之内,并且不受堆大小的影响,因此它能够支持TB级别的垃圾回收。

ZGC也是和G1收集器同样,并无进行分代,而是把整个内存分红了多个region,官方后续会尝试采用分代的设计,目前彻底由于是不分代这是最简单的设计。一次完整的 ZGC 回收周期分为如下几个阶段(Phase):

  • Pause Mark Start:标记根对象;

  • Concurrent Mark:并发标记阶段;

  • Concurrent Relocate:并发重定位;

    • 活动对象被移动到了一个新的 Heap Region B-region 中,以前旧对象所在的 Heap Region A-region 便可复用;若是 B-region 中对象之间的引用关系将会在这一阶段被更新;
    • 在重定位过程当中,新旧对象的映射关系(同一对象在不一样 Region 中的映射关系)被记录在了 Forwarding Tables 中。
  • Pause Mark Start:这个阶段实际上已经进入了新的 ZGC Cycle,一样也是标记根对象;

  • Concurrent Remap:并发重映射。 这个阶段除了标记根对象直接引用的对象外,还会根据上个 ZGC Cycle 中生成的 Forwarding Tables 更新跨 Heap Region 的引用;

ZGC仍是有停顿的,在Pause Mark Start 阶段进行根对象扫描(Root Scanning)时会出现短暂的暂停。 流程示意图以下(图片来源于网络)

4、一些JVM调优建议

4.1将新对象预留在年轻代

众所周知,因为 Full GC 的成本远远高于 Minor GC,所以某些状况下须要尽量将对象分配在年轻代,这在不少状况下是一个明智的选择。虽然在大部分状况下,JVM 会尝试在 Eden 区分配对象,可是因为空间紧张等问题,极可能不得不将部分年轻对象提早向年老代压缩。所以,在 JVM 参数调优时能够为应用程序分配一个合理的年轻代空间,以最大限度避免新对象直接进入年老代的状况发生。这里其实是为了不“朝生夕灭”的大对象发生,尽量的把设置合理新生代空间,把“朝生夕灭 ”对象留在新生代中。

4.2 将大对象直接分配再老年代

咱们分配对象通常都是分配在年轻代,分配大对象在年轻代,须要年轻代提供足够的空间,这个时候会致使原有的大量小对象进入老年代,占用老年代空间。基于以上缘由,能够将大对象直接分配到年老代,从而保留为年轻代保留了空间,保证了年轻代原有的目的,这样也能够提升 GC 的效率。若是一个大对象同时又是一个短命的对象,假设这种状况出现很频繁,那对于 GC 来讲会是一场灾难。本来应该用于存放永久对象的年老代,被短命的对象塞满,这也意味着对堆空间进行了洗牌,扰乱了分代内存回收的基本思路。所以,在软件开发过程当中,应该尽量避免使用“朝生夕灭”这样短命的大对象。可使用参数-XX:PetenureSizeThreshold 设置大对象直接进入年老代的阈值。当对象的大小超过这个值时,将直接在年老代分配。参数-XX:PetenureSizeThreshold 只对串行收集器和年轻代并行收集器有效,并行回收收集器不识别这个参数。

4.3 设置对象进入老年代的年龄

堆中的每个对象都有本身的年龄。通常状况下,年轻对象存放在年轻代,老年对象存放在老年代。为了作到这点,虚拟机为每一个对象都维护一个年龄。若是对象在 Eden 区,通过一次 GC 后依然存活,则被移动到 Survivor 区中,对象年龄加 1。之后,若是对象每通过一次 GC 依然存活,则年龄再加 1。当对象年龄达到阈值时,就移入老年代,成为老年对象。那么设置一个合适的老年代的年龄就有利于提高系统性能,能够经过-XX:MaxTenuringThreshold 来设置,默认值是 15。虽然-XX:MaxTenuringThreshold 的值多是 15 或者更大,但这不意味着新对象非要达到这个年龄才能进入老年代。若是在Survivor空间中相同年龄全部对象的大小总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代。

4.4 稳定的堆与震荡的堆

通常来讲,稳定的堆大小对垃圾回收是有利的。得到一个稳定的堆大小的方法是使-Xms 和-Xmx 的大小一致,即最大堆和最小堆 (初始堆) 同样。若是这样设置,系统在运行时堆大小理论上是恒定的,稳定的堆空间能够减小 GC 的次数。所以,不少服务端应用都会将最大堆和最小堆设置为相同的数值。稳定的堆大小虽然能够减小 GC 次数,但同时也增长了每次 GC 的时间。让堆大小在一个区间中震荡,在系统不须要使用大内存时,压缩堆空间,使 GC 应对一个较小的堆,能够加快单次 GC 的速度。基于这样的考虑,JVM 还提供了两个参数用于压缩和扩展堆空间。

XX:MinHeapFreeRatio: 设置堆的最小空闲比例,默认是40,当堆空间的空闲空间小于这个数值时,jvm会自动扩展空间。

-XX:MaxHeapFreeRatio: 设置堆的最大空闲比例,默认是70,当堆空间的空闲空间大于这个数值时,jvm会自动压缩空间。

当-Xmx 和-Xms 相等时,-XX:MinHeapFreeRatio 和-XX:MaxHeapFreeRatio 两个参数无效。
复制代码

4.5 尝试使用大的内存分页

CPU 是经过寻址来访问内存的。32 位 CPU 的寻址宽度是 0~0xFFFFFFFF ,计算后获得的大小是 4G,也就是说可支持的物理内存最大是 4G。但在实践过程当中,碰到了这样的问题,程序须要使用 4G 内存,而可用物理内存小于 4G,致使程序不得不下降内存占用。为了解决此类问题,现代 CPU 引入了 MMU(Memory Management Unit 内存管理单元)。MMU 的核心思想是利用虚拟地址替代物理地址,即 CPU 寻址时使用虚址,由 MMU 负责将虚址映射为物理地址。MMU 的引入,解决了对物理内存的限制,对程序来讲,就像本身在使用 4G 内存同样。内存分页 (Paging) 是在使用 MMU 的基础上,提出的一种内存管理机制。它将虚拟地址和物理地址按固定大小(4K)分割成页 (page) 和页帧 (page frame),并保证页与页帧的大小相同。这种机制,从数据结构上,保证了访问内存的高效,并使 OS 能支持非连续性的内存分配。在程序内存不够用时,还能够将不经常使用的物理内存页转移到其余存储设备上,好比磁盘,在windows下,这部分空间叫作虚拟内存,Linux下叫作SWAP分区。

在 Solaris 系统中,JVM 能够支持 Large Page Size 的使用。使用大的内存分页能够加强 CPU 的内存寻址能力,从而提高系统的性能。

java –Xmx2506m –Xms2506m –Xmn1536m –Xss128k –XX:++UseParallelGC
 –XX:ParallelGCThreads=20 –XX:+UseParallelOldGC –XX:+LargePageSizeInBytes=256m
–XX:+LargePageSizeInBytes:设置大页的大小。
复制代码

过大的内存分页会致使 JVM 在计算 Heap 内部分区(perm, new, old)内存占用比例时,会出现超出正常值的划分,最坏状况下某个区会多占用一个页的大小

4.6 根据场景选择合适的收集器

对于对响应时间不敏感的场景,能够选择吞吐量优先的收集器来提高性能,好比Parallel Old 收集器。若是是对响应时间要求高的场景,就须要选择低停顿的垃圾回收器,好比CMS,G1,ZGC(虽然目前还不是很是成熟)。

5、总结

这篇文章内容比较,主要分享了虚拟机的性能度量指标,垃圾回收器的分类,一些调优建议。最后放一张本文的脑图进行总结:

6、参考

相关文章
相关标签/搜索