JVM之GC调优原理(一)

前言

本文描述了GC(Garbage Collector)的相关概念、运行原理、GC算法以及调优参数,读者可根据应用系统的具体情况选择一个合适的GC算法以及配置对应的优化参数,以下内容仅供参数,以实际应用系统的运行环境信息为准。

1     GC(Garbage Collector)调优介绍 

众所周知,Java语言是非常流行的编程语言,据权威机构的调查显示,目前在全世界编程语言的排行榜中Java一直处于第一或者第二的位置,在web应用服务器的领域,Java HotSpot虚拟机被广泛用于各种不同应用的运行时支撑环境。Java HotSpot虚拟机为了支撑各种大小不同的应用部署需求,其内部机制提供多种garbage collector (GC),每一种GC满足不同应用需求的使用场景,基于其内部机制,Java se平台为运行的应用选择最适合的GC,然而,自动为应用选择GC的机制并不适合所有的应用需求,所以,为了满足其他需要严格的性能需求的用户,Java虚拟机提供配置机制,用户可以根据实际的应用需求指定配置一种GC,同时可以设定该GC的调优参数,使其满足当前应用的性能需求。本文的宗旨是为用户提供方法或信息,其中包括各种GC的核心介绍以及对应GC的各种调优参数,用户可根据应用的实际需求进行调整以满足其业务需求,以下将进行详细的描述。

首先,什么是garbage collector(GC)?

简而言之,GC能自动化地管理来自应用的动态内存申请的请求,所以GC通过以下的操作执行自动化的动态内存管理:

Ø 向操作系统申请或者释放内存空间

Ø 为应用分发需要的内存空间

Ø 确定应用正在使用的内存空间

Ø 为应用回收再利用不再使用的内存空间

此外,JVM设计并使用各种不同的技术去改善以上操作的执行效率:

Ø 使用基于分年龄代的技术去管理整个堆内存空间,一般情况下,内存空间包括很多可回收再利用的内存区域

Ø 使用多线程的技术去并行化操作,其中耗时较长的操作,可以与应用并发地执行,而不影响应用的正常运行

Ø 通过压缩技术对存活对象进行存储,可以恢复大量连续的可用的内存空间

由此可知,GC实际上是一个内存管理工具。

其次,重要的是什么时候该为你的应用选择一个收集器?实际上,JVM会通过一些计算为你的应用默认指定一个合适的收集器,而不用手动配置,但是默认机制并不适合大应用,例如那些需要处理大量数据、大量线程(高并发)、大量事务的使用场景,基于这些场景需要根据实际的业务场景手动指定一个合适的收集器。

Amdahl法则认为:对于给定的问题,并行加速比受制于问题本身的串行部分,因此,在实际的应用环境中,运行在JVM中的大部分工作负载不能很好地实现并行化,而且总有一部分工作负载是按照顺序执行的,而不适合并发地执行。所以,从JDK1.4版本开始,JVM为了支持计算机多处理器,不断优化GC的处理算法,以满足现在化应用系统中并行处理的场景。

现在,我们以下图1来对比分析,不同处理器数量在不同GC时间的比例下对应用系统吞吐率的影响:

图1

现在,从以下几个方面来分析上图1:

Ø 横坐标方向,表示的维度是应用系统的处理器数量,有效值是从1~30,刻度1表示单处理器系统,刻度30表示多处理器系统的处理器个数

Ø 纵坐标方向,表示的维度是应用系统的吞吐率比例,有效值是从0.1~1,刻度1表示系统的吞吐率不受到影响,刻度0.1表示系统的吞吐率已经损耗了1-0.1=0.9

Ø 各种不同颜色的线段,1%GC线表示的意义是,当GC所花费的时间占用系统总运行时间的比例是1%的时候,随着应用系统的处理器个数的增加,应用系统的吞吐率的比例(理论上,从系统资源消耗的角度考虑,总吞吐率=业务处理的线程数/( GC线程数+业务处理的线程数),而虚拟机的总线程数=GC线程数+业务处理的线程数,总量不变的情况下,GC花费的时间越多资源占用越多,则业务能占用的资源越少,所以吞吐率会下降),如图所示,单核系统吞吐率基本不受到影响,当处理器数量增加到30个,吞吐率损耗达到1-80%=20%

Ø 对于10%GC的占用时间,当处理器数量增加到30个,吞吐率损耗达到1-25%=75%,这是业务系统不能容忍的程度

由以上分析可知,对于小的低并发的应用系统,没有出现性能瓶颈问题,但一旦扩展成为大的高并发的应用系统,则会出现明显的性能瓶颈问题,因此,解决与改善此类瓶颈问题,应用系统的性能将会大幅提升,所以,非常有必要为大应用系统选择一个合适的GC以及根据实际情况进行参数优化的设置。

2     工程学原理

JVM以及GC是通过启发式的分析方法为运行的应用系统提供默认的调整与优化,其目的是改善系统的性能或其他度量指标,例如,JVM内置的平台独立的功能模块能为应用系统设置默认GC、堆空间大小以及运行时编译器的类型,这些默认的设置能满足各种不同类型的应用系统的业务需求,而只需要很少的命令行式的输入参数,此外,JVM能基于系统的行为分析动态地优化调整堆空间的大小,以满足应用的特别行为。

2.1   默认设置

JVM提供重要的GC、堆空间大小、运行时编译器的默认配置,如下所述:

Ø 在server类型的JVM中首选是Garbage-First (G1)收集器,其次是串行收集器(Serial Collector )

Ø 最大的GC占用线程数是由堆空间大小以及CPU的可用资源决定的

Ø 初始的堆空间大小等于物理内存的1/64

Ø 最大的堆空间大小等于物理内存的1/4

Ø 层列式(Tiered compiler)编译器,包括C1和C2

2.2   基于行为分析的优化调整

JVM的行为分析的指标可以分为两个重要的可配置调整的维度:最大暂停时间、应用的吞吐率,JVM的调整顺序是先满足其中一个指标的最优化,再满足另外一个指标的最优化,但是实际上,由于受到堆空间总是被存活的对象数据占用或者其他操作的影响,总是不能同时满足两个维度的指标,因为它们之间总是相互制约的,所以JVM基于应用的行为分析算法需要做出适当的权衡,才能保证应用系统的业务逻辑的正常运行,以下分三点进行论述。

2.2.1 最大暂停时间目标

暂停时间是GC为了恢复释放不再使用的堆空间(非存活Java对象的占用堆空间,GC需要回收再利用)而停止应用运行(stop-the-world)的持续时间,而最大暂停时间指标的目的是限制应用暂时停止运行的持续时间的最大值,理论上,最大暂停时间越小越好,对应用的业务逻辑的执行影响最少。

JVM从启动运行那一刻开始,GC维护了两个分析类型的统计值:平均暂停时间与平均暂停时间的方差,统计平均暂停时间时增加了权重值,即最近统计的均值的权重值更大,在实际的运行环境中,当GC监测到以上两个统计量相加的值大于最大暂停时间,则GC认为应用系统未能满足最大暂停时间的目标。

JVM提供命令行设置调整参数,例如-XX:MaxGCPauseMillis=<nnn>表示设置最大暂停时间指标(不同GC收集器类型的默认最大暂停时间不同),一旦设置了该参数,则相当于对GC发出命令行指示,最大暂停时间只能小于或者等于该值nnn。那么,在实际的运行环境中,GC为了满足该指标,则以空间换时间的策略不断调整堆空间的大小以及和GC相关的调整参数,目的是为了保持暂停时间小于或者等于nnn,这样的调整可能会引起比较频繁的GC操作,进而会降低应用系统的总体吞吐率。然而,在某些应用场景中,所期待的暂停时间目标不能被满足,因为该指标由应用系统的很多因素共同决定。

2.2.2 吞吐率目标

吞吐率目标是由两个时间维度进行测量,分别是GC消耗的时间以及非GC也即应用执行业务逻辑所消耗的时间。该目标由-XX:GCTimeRatio=nnn命令行参数设置,这是比率值的设置,也即GC消耗的时间与应用消耗的时间比例,通过1/ (1+nnn)计算所得各占多少的时间比,例如-XX:GCTimeRatio=19,则1/1+19=1/20=0.05=5%,表示GC消耗的时间占总JVM运行时间的5%,则应用消耗的时间占总JVM运行时间的95%,一般情况下,5%的GC时间占用率不适合需要高并发的性能要求的业务场景。

因此,消耗在GC的总时间等于每次应用暂停时的持续时间的总和,在实际应用运行环境中,假如吞吐率的目标不能满足,GC可能采取的行动是增加堆空间大小,从而减少GC的次数,从而减少GC的总时间,从而增加应用运行的总时间。

2.2.3 内存覆盖区(堆区)

假如吞吐率目标与最大暂停时间目标都已经满足,则GC将减少堆空间的大小,直到其中一个目标不能满足为止,一般情况下,吞吐率最先接近设定的临界值的目标。JVM提供命令行参数设定堆空间的大小,例如-Xms=<nnn> 与-Xmx=<mmm>,前者表示设定最小的堆空间大小,后者表示设定最大的堆空间大小,JVM将根据实际情况在此范围内增大或者减少堆空间的大小。

2.3   调优策略

由以上分析可知,GC通过增长或者缩减堆空间的大小来满足对应的吞吐率目标,所以,GC的调优策略最基本需要掌握的是选择一个恰当的最大的堆空间大小,以及一个最大的GC暂停时间目标。

如果没有具体的测量以及调试,不要轻易设定一个最大的堆空间大小,即保持JVM的默认设置的最大值是最佳的选择,此外,设定一个恰当的能让应用获得最佳效率的吞吐率目标,这些目标都需要在实际的业务环境中实行具体的测量,才能准确地被定义。

一个应用的行为的变化能引起堆空间的大小的增长或者缩减,例如,应用开始以一个较高的效率申请内存空间,则为了维持吞吐率不变,堆空间的大小会以同样的效率增长。

假如堆空间的大小已经增长到最大值,但吞吐率目标还不能被满足,则只能说明堆空间太小而不能满足其吞吐率的目标,同理,如果将最大的堆空间大小设置为接近物理内存的大小,应用还能正常运行,但依然不能满足设定的吞吐率目标,则只能说明最大的吞吐率目标设置得太大,应用系统目前的硬件配置无法支撑该容量。

假如应用系统的吞吐率目标能满足,但暂停时间太长,则可以设置一个最大的暂停时间,然而,设置一个最大的暂停时间则意味着限制了吞吐率的目标,由此可知,吞吐率与暂停时间这两个维度是两个相互制约的指标,因此,需要根据实际运行环境的具体情况做出权衡,通过精确的测量才能获得最佳的设置参数。

因为JVM使用基于启发式的行为分析方法,所以堆空间的大小会不停地在最小值最大值之间的范围内增长与缩减,从而满足吞吐率目标与暂停时间的目标,即使应用系统已经达到了一个稳定的状态,这种堆空间大小不停调整变化的情况也依然存在。总而言之,应用系统中来自吞吐率的压力总是与最大暂停时间和堆空间大小相互竞争与制约,从而谋求达到一个最稳定的状态,这种状态既能满足各种指标,又能节省应用系统的资源。

3     GC的实现原理

众所周知,将开发者从繁琐复杂的内存申请、内存释放以及GC管理等相关工作中剥离出来,从而让开发者更加专注、更加高效的对业务逻辑进行处理,实现这些目标一直是我们Java SE平台努力的方向之一。

然而,在实际运行的应用环境中,当遇上原则性的性能瓶颈的时候,有必要去掌握这些GC的实现原理,从而更容易地找到解决这些瓶颈问题的有效方法,从以下几点进行论述。

3.1   分代收集器

在Java程序运行环境中,当一个对象不可达,也即不再被其他任何一个存活的对象所引用,那么JVM认为该对象是一个垃圾的对象,并且其占用的内存空间可以被回收再利用。

那么GC如何找到那些垃圾对象?理论上,最简单直接的算法是每次执行GC的时候只对所有可达的存活对象迭代遍历,那么剩下的不可达的未被迭代遍历的对象则被认为是垃圾对象,虽然该算法简单,但是消耗的时间太多,与存活的对象的个数成正比例,也即存活的对象越多,则GC的时间越多(GC暂停时间),所以该算法不适用于包含有很多存活对象的大应用。

JVM设计一些不同的GC算法,这些算法除了ZGC外都使用了同一种被称之为基于代的技术,虽然GC每次收集的时候都简单地迭代遍历检测每个存活的对象,但是基于代的GC利用一些来自应用自身的可观察的基于经验的或者启发式的分析方法,来最小化释放垃圾对象的工作负载,这些属性之中最重要的属性是弱代假说,即JVM利用弱代假说声明大部分的对象只能在短时间内存活。

图2

如上图2所示,该图是典型的Java对象生命的存活时间分布图,X轴表示的对象存活时间,因为对象是以占用的堆内存空间的字节大小来测量的,所以当朝X轴方向增大时,表示Java对象(占用的字节大小)生命存活的持续时间越长,Y轴表示堆内存空间中所有存活对象的总占用字节大小,图中的某一个坐标(x,y)的意义是,从JVM启动的时刻算起,在某个时刻所有存活对象的总占用字节的大小,当对象生命存活的时间结束时,GC可以回收再利用,如图所示,GC在持续地回收非存活对象所占用的字节空间,Minor Collections表示GC在回收存活时间较短的对象,而Major Collections表示GC在回收存活时间较长的对象。在图示的最左边Y轴位于高处聚集很多存活时间短的对象,这些对象一旦被用完就被GC回收了,其对应的使用场景是在迭代或者循环列表中被局部声明的对象,当迭代或者循环结束即时被GC回收,因此其存活时间较短。

一些对象的存活时间较长,其趋向于分布在图示的右侧,例如,一些对象从JVM初始化时就已经存在,直到JVM结束运行,对应的使用场景如Java设计模式中的单例对象,在一些数据分析型的应用中,有很多对象是参与中间值的计算,有些对象是用于保存计算结果或者状态,所以其持续时间较长。

总而言之,JVM中大部分的Java对象的生命存活时间较短,其占用的空间被GC迅速地回收再利用。

3.2   代的定义

JVM为了优化上面提到的问题,使用基于分代的概念(即内存池中包含各种不同年龄的对象)来管理Java对象占用的内存空间,当每代的堆内存空间被填满,则执行GC操作。

几乎所有新创建的对象都在年轻代的内存池中申请堆空间,由上图可知,几乎大部分对象的生命存活时间都在年轻代内存池中结束(占用空间被GC回收再利用),当年轻代的内存池被Java对象填满,则触发一次minor collection的操作(专用于收集存活时间短的垃圾对象,即年轻代内存池的专用操作),而在非年轻代的内存池不用执行此操作,根据前一章节所述,该操作所消耗的时间与存活对象的个数成正比,即年轻代内存池中的垃圾对象越多,则其执行速度越快。一般情况下,在每次minor collection操作的时候,都有一部分来自年轻代内存池中的存活对象会被移动到老年代的内存池中,最终,老年代的内存池也会被填满,这会触发一次major collection的操作,该操作是针对所有堆空间(包括年轻代与老年代),major collection操作的持续时间通常比minor collection操作的持续时间长很多,因为涉及到几乎所有的Java对象。

图3

如上图3所示,该图表示串行收集器(Serial Collector)中基于分代收集器的内存分布。该图中Young的区域表示年轻代的内存池区域,Old的区域表示老年代的内存池区域。

JVM在启动的时候,会向系统的地址空间预定全部的Java堆空间,但并不申请任何物理内存空间,除非实际需要。覆盖堆空间的地址空间逻辑上被划分为年轻代以及老年代,同理,为Java对象的内存预定的地址空间也可以被划分为年轻代以及老年代。

如上图3所示,年轻代可以划分为一个Eden区域以及两个Survivor区域,大部分的刚创建的初始化对象是在Eden区域申请堆空间,下面来详细说明Survivor区域的作用,在任何时候都有其中一个Survivor区域是空的,该空Survivor区域成为Eden区域以及另一个Survivor区域复制存活对象的目的区域,即每次发生GC时候,GC都把Eden以及其中一个非空Survivor区域中的存活对象复制到另一个空的Survivor区域中,复制完毕后,Eden区域以及源Survivor区域都变成空的区域,在下一次GC时候,GC又执行相同的操作,即把Eden区域和非空的Survivor区域复制到空的Survivor区域中,这样的机制保证其中的一个Survivor区域在任何时候都是空的,在两个Survivor区域之间的存活对象被来回复制多次之后,对象的年龄会持续增大,因为每复制一次,则年龄增加1,当目的Survivor区域空间被填满,则Survivor区域中的全部存活对象会被复制到老年代区域。

如上图3所示,年轻代以及老年代都有一个virtual区域,这是已经被预定的但没有分配物理空间的虚拟空间,即还没有被使用的地址空间,根据上一章节的论述,JVM会根据实际情况增长或者缩放堆空间的大小。

3.3   关于性能的思考

GC提供两个主要的测量维度:吞吐率以及延迟水平。

Ø 理论上,从运行耗时的角度考虑,吞吐率等于非GC总时间/(GC总时间+非GC总时间),基于应用运行了一段长时间后做出的统计,即JVM总运行时间=GC总时间+非GC总时间

Ø 理论上,延迟水平是指应用的响应能力(请求响应),GC的暂停时间会影响应用的响应能力

不同的应用场景有不同的GC需求,例如,在web服务器应用场景中,吞吐量率是正确的度量指标,因为GC引起的延迟可以容忍,可以简单的看作是网络延迟,然而,在交互式的图形界面的应用场景中,即使很短的GC暂停也会对用户体验产生消极的影响。

应用场景不同,则考虑的影响指标不同,例如,内存覆盖区是一个进程的工作集,其度量的指标是内存页与行缓存,因此,在一个物理内存受限以及许多进程的系统中,内存覆盖区会影响系统规模化的处理能力。此外,敏捷(迅速响应请求)是一个分布式系统(RMI,Remote Method Invocation,远程方法调用)重点考虑的指标,该指标的意义是一个存活对象成为垃圾对象到其占用的堆空间被回收释放所经历的这段时间,这段时间越短,则越敏捷。

总而言之,如何为年轻代以及老年代的内存池分配合理的空间大小或者合适的比例,这是一个值得权衡以及思考的问题,例如,为了最大化系统的吞吐率,可以为年轻代分配非常大的内存池,这意味着需要以其他指标变差为代价,即内存覆盖区占用增大,GC暂停时间也增大并导致系统请求响应的敏捷性变弱,所以需要权衡多种影响因素和指标,让系统满足业务需求。反之亦然,即可以为年轻代分配较小的内存池,这意味着GC暂停时间减小,但需要以吞吐量变差为代价。年轻代与老年代的内存池的分配大小或者比例,并没有相互影响GC发生的频率以及GC的暂停时间,它们是相互独立的实现机制。

由此可知,并没有一个固定不变的正确方法去分配年轻代与老年代的内存池的大小和比例,最佳的方法是具体问题具体分析,以实际的运行环境为准,找出符合业务需求目标的分配大小与比例。因此,JVM的默认机制提供的也许不是最佳的设置,你也可以通过JVM命令行的方式重新设置。

3.4   测量吞吐率与内存覆盖区

JVM提供命令行的参数设置监测系统的吞吐量以及堆内存的分布情况,-verbose:gc,该命令行参数可以输出GC的行为信息到日志文件中,从而作为业务分析的依据,如下所示:

[15,651s][info ][gc] GC(36) Pause Young (G1 Evacuation Pause) 239M->57M(307M) (15,646s, 15,651s) 5,048ms

[16,162s][info ][gc] GC(37) Pause Young (G1 Evacuation Pause) 238M->57M(307M) (16,146s, 16,162s) 16,565ms

[16,367s][info ][gc] GC(38) Pause Full (System.gc()) 69M->31M(104M) (16,202s, 16,367s) 164,581ms

下面来分析以上日志的具体含义,第一、第二行表示年轻代发生GC,第三行表示完全GC(Full GC,JVM对所有的堆区进行GC操作,理论上耗时比较长),Full GC是由于程序代码中调用了System.gc()后JVM做出的响应。

现在对第一行的信息进行详细说明,如下表所示:

信息项

意义

GC操作类型

[15,651s]

表示应用已启动的时间,也相当于JVM已经运行的时间,单位:秒

年轻代GC

[info ]

日志级别

[gc]

表示日志记录的GC类型的操作

GC(36)

表示系统分配的标识该次GC操作的序列号

Pause Young

表示GC操作的类型

(G1 Evacuation Pause)

表示引起GC的原因,G1是收集器的类型,是JVM系统机制自动触发

239M

表示执行GC前已经使用的堆空间大小,单位:M

57M

表示执行GC后已经使用的堆空间大小,单位:M

(307M)

表示的堆空间总大小,单位:M

(15,646s, 15,651s)

(GC的开始执行时间点,GC结束执行时间点),是从应用的启动时刻开始计算,单位:秒

5,048ms

GC消耗的总时间,单位:毫秒

现在对第三行的信息进行详细说明,如下表所示:

信息项

意义

GC操作类型

[16,367s]

表示应用已启动的时间,也相当于JVM已经运行的时间,单位:秒

Full GC

[info ]

日志级别

[gc]

表示日志记录的GC类型的操作

GC(38)

表示系统分配的标识该次GC操作的序列号

Pause Full

表示GC操作的类型

(System.gc())

表示引起GC的原因,是程序代码中调用,是用户主动执行GC操作

69M

表示执行GC前已经使用的堆空间大小,单位:M

31M(104M)

表示执行GC后已经使用的堆空间大小,单位:M

(104M)

表示的堆空间总大小,由307M缩减到104M,根据前面一章节的论述,JVM使用启发式的分析方法,自动增长或者缩放总的堆空间大小,单位:M

(16,202s, 16,367s)

(GC的开始执行时间点,GC结束执行时间点),是从应用的启动时刻开始计算,单位:秒

164,581ms

GC消耗的总时间,单位:毫秒

此外,命令行参数-Xlog:gc是-verbose:gc的别名,该参数是通用的JVM日志配置选项,例如通过-Xlog:gc*形式的设置,可以打印如下格式的日志:

[10.178s][info][gc,start ] GC(36) Pause Young (G1 Evacuation Pause)

[10.178s][info][gc,task ] GC(36) Using 28 workers of 28 for evacuation

[10.191s][info][gc,phases ] GC(36) Pre Evacuate Collection Set: 0.0ms

[10.191s][info][gc,phases ] GC(36) Evacuate Collection Set: 6.9ms

[10.191s][info][gc,phases ] GC(36) Post Evacuate Collection Set: 5.9ms

[10.191s][info][gc,phases ] GC(36) Other: 0.2ms

[10.191s][info][gc,heap ] GC(36) Eden regions: 286->0(276)

[10.191s][info][gc,heap ] GC(36) Survivor regions: 15->26(38)

[10.191s][info][gc,heap ] GC(36) Old regions: 88->88

[10.191s][info][gc,heap ] GC(36) Humongous regions: 3->1

[10.191s][info][gc,metaspace ] GC(36) Metaspace: 8152K->8152K(1056768K)

[10.191s][info][gc ] GC(36) Pause Young (G1 Evacuation Pause) 391M->114M(508M) 13.075ms

[10.191s][info][gc,cpu ] GC(36) User=0.20s Sys=0.00s Real=0.01s

(未完待续)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wangys2006

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值