为了账号安全,请及时绑定邮箱和手机立即绑定

Java性能优化——GC垃圾收集调优基础

标签:
ZBrush

前言

Java性能优化永远绕不开的话题之一 ———— 垃圾收集调优,Java最诱人的特性之一是不需要显式地管理对象的生命周期:我们可以在需要时创建对象,对象不再被使用时,会由JVM在后台自动进行回收。

但是这个特性是把双刃剑,JVM为我们屏蔽了内存管理的冗杂操作,但是在上层的开发人员,在他根本不熟悉JVM的前提上,他对Java语言的认识还不够全面,导致在代码的书写过程中,可能会埋下内存溢出的隐患。

在我们熟知的很多开发规范开发守则中,对隐式的内存使用都会有对应的规范,比如对集合类的大小进行预先设置,对IO类的资源释放操作。这些规范都是让我们更好的使用内存,让代码具有更好的性能。

在C语言中,各种开放式的代码和编译奇葩结果让开发人员无法专心于核心业务上,Java语言和其周边生态的出现,唯一的目的就是让开发人员可以专注于核心业务,不用去纠结一些底层的管理和额外框架的交互。

正因为这样,我们要在代码上精益求精,在日常的服务日志查看中,看是否有存在性能瓶颈。及时进行修正,避免导致错误结果。所以对于开发人员来讲,JVM的垃圾收集机制和主流的垃圾收集器是必定需要了解的。

垃圾收集基本概念

垃圾收集简单的表述的话,基本操作有三个步骤:

查找不再使用的对象释放这些对象的内存空间对堆的内存布局进行压缩整理。

所有的垃圾收集器和GC算法都是围绕着以上三个基本操作去实现的。完成这些基本操作时,不同的收集器采用了不同的方法,这也是不同的垃圾收集器表现出不同性能的特征。

分代机制

在JVM的内存模型中,将堆划分成了不同的代(Generation),主要分为新生代和老年代,新生代还划分出来Eden区和Survivor区,采用分代机制的原因就是很多对象的生存时间非常短,大多数都是临时对象,垃圾收集线程扫描部分堆空间肯定是要比扫描整个堆要快得多,还有比如JVM开启了逃逸分析(-XX:+DoEscapeAnalysis(开启))的话,可能会进行的优化有一项是JVM编译器会直接在线程的寄存器中去操作临时对象,根本不会在堆上进行分配。对于新生代的划分,在垃圾收集线程中,会在Eden区和Survivor区之间移动对象,相当于一个变相的内存压缩整理的过程。

Minor GC

新生代分配方式,将对象分配于Eden区(大多数情况),垃圾收集时,将Eden区对象被移走或者回收,移动至Survivor空间,或者移动至老年代,这相当于对新生代空间进行了一次压缩整理。在新生代的空间被填满后,垃圾收集器会暂停所有的应用线程。回收新生代的空间,回收的基本步骤就如我刚才简略地描述:

查找不再使用对象进行回收将存活对象,放入Survivor区(有两部分S0和S1),增加存活年龄,如果到达晋升老年代的阈值时,会进行升代,剩余存活对象就继续放在Survivor区内。

新生代是堆一部分,垃圾收集器处理新生代的速度会比整个堆速度快,线程的停顿时间更短,但是停顿的次数也会比较频繁,所以这中间的取舍是一种权衡,要根据具体的应用场景去分析。

Full GC

随着应用程序的运行,对象会不断地晋升老年代,到最后老年代势必也会被对象填满,JVM便会找出老年代中不再使用的对象。简单的垃圾收集算法会直接停掉所有的应用线程,找出不再使用的对象,对其进行回收,然后对堆空间进行整理。Full GC通常会导致应用程序长时间的停顿(相比于Minor GC)

时空停顿(Stop the world)

垃圾收集器在回收对象时,或者在内存中移动对象时,必须确保应用程序线程不再继续使用这些对象,在操作过程中对象的内存地址会发生变化,因此这个过程中任何应用线程都不应再访问该对象。在现有的垃圾收集算法中在对新生代进行垃圾回收时都存在“时空停顿”现象(笔者目前认知JDK1.8版本),所以垃圾收集调优的一方面就是让时间停顿的时间越来越短,频率在可接受的范围内。

垃圾收集器

在JDK11版本之前,主流的垃圾收集器有4种。如下:

Serial垃圾收集器Parallel垃圾收集器CMS垃圾收集器G1垃圾收集器Serial垃圾收集器

Serial垃圾收集器主要是应用在Client型虚拟机(Windows平台上的32位JVM或者是运行在单处理器机器上的JVM)上,它进行Minor GC或者Full GC清理堆空间时,会将所有的应用线程暂停。进行Full GC时会有长时间的停顿。比较适用于单CPU的环境以及内存使用少于100MB的使用场景下。

Parallel垃圾收集器

Parallel垃圾收集器是Server级虚拟机(多CPU的Unix机器以及任何64位虚拟机)的默认收集器。它使用了多线程进行回收新生代,Minor GC的速度会比使用Serial垃圾收集器要快得多,但是它跟Serial垃圾收集器一样,在Minor GC和Full GC时都会暂停所有的线程,进行Full GC时会有长时间的停顿。

CMS垃圾收集器

CMS垃圾收集器是一个Concurrent垃圾收集器,主要是为了消除Parallel垃圾收集器和Serial垃圾收集器中进行Full GC是长时间的停顿。它不再使用Parallel的收集算法(-XX:+UseParallelGC),改用新的算法(-XX:+UseParNewGC)来收集新生代的对象,在Minor GC的时候会暂停所有的应用线程,并以多线程的方式进行垃圾回收。富贵论坛在应用线程运行的过程的同时会扫描堆的使用情况,所以在垃圾收集的时候,带来的停顿时间极短。因为花费额外的线程去扫描堆的情况,所以带来的影响就是需要更高的CPU使用率,同时后台线程不再进行压缩工作,堆变得碎片化。当堆过度碎片化或者无法获得足够的CPU资源,退化为Serial收集器行为:暂停所有应用线程,使用单线程回收、整理老年代空间。这之后又恢复到并发运行,并且随着堆的增大,扫描的时间变长,CMS的工作也越多,导致并发模式失效,退化为Serial收集器行为,性能损失严重。所以CMS的主要适用场景就是CPU资源充足,并且堆的大小小于4GB。

G1垃圾收集器

因为CMS存在的问题,所以它的下一代G1收集器主要就是为了解决垃圾收集器在处理超大堆(大于4GB)时产生的停顿。首先G1垃圾收集器将堆划分为若干个区域,这些区域同样沿用分代机制,一部分区域属于新生代(Eden区和Survivor区),一部分区域属于老年代。G1收集器属于Concurrent收集器,它在收集新生代的时候会采用暂停所有应用线程的方式,采用多线程的方式将存活对象移动至老年代于Survivor区。老年代的垃圾收集工作由后台线程完成,大多数的时候需要暂停应用线程,G1的垃圾收集工作主要将对象从一个区域复制到另一个区域中,这意味着在垃圾收集的过程中,G1实现了对堆的压缩整理(至少是部分堆整理)。

选择合适的垃圾收集器

现在我们已经熟悉了主流的四种垃圾收集器了,那么要如何进行选择呢?结合它们的适用场景以及以下有几点进行参考:

考虑因素单个请求会受停顿时间的影响——不过其受Full GC长时间停顿的影响更大。如果目标是要尽可能地缩短响应时间,那么选择使用Concurrent收集器更合适。如果平均响应时间比最大响应时间更重要(譬如90%的响应时间),采用Parallel收集器通常就能满足要求。使用Concurrent收集器来避免长的停顿时间也有其代价,这会消耗额外的CPU。原则如果CPU足够强劲,使用Concurrent收集器避免发生Full GC停顿可以让任务运行得更快。如果CPU有限,那么Concurrent收集器额外的CPU消耗会让批量任务消耗更多的时间。GC调优

垃圾收集器方面的选择完毕后,那么就是要开始对应用程序的GC调优了,GC调优主要有以下几个方面:

堆大小的设置

堆的大小的设置需要根据应用情况进行选择,它是一种权衡性的配置。如果分配地堆过于小,程序的大部分时间可能都消耗在GC上,没有足够的时间去运行应用程序的逻辑。相对的增大堆的空间,停顿的持续时间也会变长。停顿的频率会变得更少,但是它们持续的时间会让程序的整体性能变慢。

主要可以参考以下几点:

永远不要将堆的容量设置得比机器的物理内存还大,如果多个JVM实例,这个原则适用于所有堆的总和JVM自身以及机器上其他的应用程序预留一部分的内存空间,通常情况下,对于普通的操作系统,应该预留至少1 G的内存空间。因为操作系统使用虚拟内存机制管理机器的物理内存,JVM无法得知内存交换的细节,导致磁盘内存交换的昂贵操作,停顿时间也会过长。堆的自适应调节(设置最大值与最小值),让JVM能够根据实际的负荷情况更灵活地调整JVM的行为-Xmx标志设置经验法则是完成Full GC后,应该释放出70%的空间(30%的空间仍然占用),根据GC测试。代空间调整

如果新生代分配得比较大,垃圾收集发生的频率就比较低,从新生代晋升到老年代的对象也更少。任何事物都有两面性,采用这种分配方法,老年代就相对比较小,比较容易被填满,会更频繁地触发Full GC。这里找到一个恰当的平衡点是解决问题的关键。所有用于调整代空间的命令行标志调整的都是新生代空间。

永久代和元空间调整

JVM载入类的时候,它需要记录这些类的元数据。这部分数据被保存在一个单独的堆空间中。在Java 7里,这部分空间被称为永久代(Permgen或Permanent Generation),在Java 8中,它们被称为元空间(Metaspace)。保存在其中的类像其他的对象一样会经历垃圾回收。由于元空间默认的大小是没有作限制的,因此Java 8(尤其是32位系统)的应用可能由于元空间被填满而耗尽内存。开发中的应用服务器(或者任何需要频繁重新载入类的环境)上经常能碰到由于永久代或元空间耗尽触发的Full GC,这时老的元数据会被丢弃回收。

控制并发

除Serial收集器之外几乎所有的垃圾收集器使用的算法都基于多线程,控制并发限制垃圾线程的使用,减少竞争冲突,在拥有更多CPU、运行了多个JVM的机器上,通常出现的问题是有太多的垃圾回收线程在同时并发运行。

自适应调整

让JVM进行堆的自适应调整是一种尽力而为(Best-Effort)的方案,它进行性能调优的依据是以往的性能历史:这其中隐含了一个假设,即将来GC周期的状况跟最近历史GC周期的状况可能很类似。意味着小型应用程序不需要再为指定了过大的堆而担心,平台默认的配置确保不会使用大量的内存。应用程序根本不需要担心它们堆的大小。JVM会自动调整堆和代的大小,依据垃圾回收算法的性能目标,使用优化的内存量。

GC调优参数脑图

以下是常用的Java GC标志调优

Java性能优化——GC垃圾收集调优基础

总结

使用JFR观察应用程序情况。根据分析情况进行对应的优化调优!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消