接上篇JVM内存分配参数

JVM内存结构分配对Java应用程序的性能有较大的影响。本节主要介绍设置Java应用程序内存大小及内存结构的方法,如设置堆大小、设置新生代大小、设置持久带大小、设置线程栈大小等。

设置最大堆内存

Java应用程序可以使用的最大堆可以用-Xmx参数指定。最大堆指的是新生代和老年代的大小之和的最大值,它是Java应用程序的堆上限。

以下这段代码不停地在堆上分配空间,直到内存溢出。-Xmx参数的大小不同,将直接决定程序能够走过几个循环。

jvm常见的调优命令(字节大牛百万调优经验之作)(1)

使用java-Xmx5Mjavatuning.ch5.memory.TestXmx运行程序时,最大堆上限为5MB,此时程序输出结果如下,表明在完成4MB数据分配后,系统空闲的堆内存大小已经不足1MB了。

jvm常见的调优命令(字节大牛百万调优经验之作)(2)

当使用java-Xmx11Mjavatuning.ch5.memory.TestXmx运行程序时,最大堆上限为11MB,此时程序顺利结束,没有任何异常,表明在11MB的堆空间上成功分配了10MB的byte数组。

在运行程序时,可以使用Runtime.getRuntime().maxMemory()取得系统可用的最大堆内存。本例中最后一行的输出结果为:

jvm常见的调优命令(字节大牛百万调优经验之作)(3)

注意:通过-Xmx参数可以设置系统的最大堆。

设置最小堆内存

使用JVM参数-Xms可以设置系统的最小堆空间。也就是JVM启动时,所占据的操作系统内存大小。

Java应用程序在运行时,首先会被分配-Xms参数所指定的内存大小,并尽可能尝试在这个空间段内运行程序。当-Xms指定的内存大小确实无法满足应用程序时,JVM才会向操作系统申请更多的内存,直到内存大小达到-Xmx参数指定的最大内存为止。若超过-Xmx参数指定的值,则抛出OutOfMemoryError异常。

如果-Xms的数值较小,那么JVM为了保证系统尽可能地在指定内存范围内运行,就会更加频繁地进行GC操作,以释放失效的内存空间,从而会增加MinorGC和FullGC的执行次数,对系统性能产生一定的影响。

以下代码每次分配1MB空间,累计3MB时,清空所有内存。

jvm常见的调优命令(字节大牛百万调优经验之作)(4)

使用JVM参数-Xmx11M-Xms4M-verbose:gc运行以上代码,输出结果如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(5)

其中进行了10次MinorGC操作和2次FullGC操作。MinorGC操作共计耗时0.007382s,2次FullGC操作共计耗时0.017657,总计0.025039s。

为减少GC操作的次数,增大-Xms的值,使用JVM参数-Xmx11M-Xms11M-verbose:gc运行相同代码,输入如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(6)

增大-Xms后,只进行了4次MinorGC操作,合计耗时0.006899s,执行速度得到了优化。

注意:JVM会试图将系统内存尽可能地限制在-Xms中,当内存实际使用量触及-Xms指定的大小时,会触发FullGC。因此把-Xms值设置为-Xmx时,可以在系统运行初期减少GC操作的次数和耗时。

设置新生代

参数-Xmn可以用于设置新生代的大小。设置一个较大的新生代会减小老年代的大小,这个参数对系统性能及GC行为有很大的影响。新生代的大小一般设置为整个堆空间的1/4到1/3。

以上例中的代码为例,若使用JVM参数“-Xmx11M-Xms11M-Xmn2M-XX: PrintGCDetails”运行程序,将新生代的大小减小为2MB(默认情况下是3.5MB左右),那么MinorGC操作次数将从4次增加到9次。

在HotSpot虚拟机中,-XX:NewSize可以用于设置新生代的初始大小,-XX:Max-NewSize可用于设置新生代的最大值。但通常情况下,只设置-Xmn已经可以满足绝大部分应用的需要。设置-Xmn的效果等同于设置了相同的-XX:NewSzie和-XX:MaxNewSize。

若设置不同的-XX:NewSize和-XX:MaxNewSize,可能会导致内存振荡,从而产生不必要的系统开销。

设置持久代

持久代(方法区)不属于堆的一部分。在HotSpot虚拟机中,使用-XX:MaxPermSize可以设置持久代的最大值,使用-XX:PermSize可以设置持久代的初始大小。

持久代的大小直接决定了系统可以支持多少个类定义及支持多少常量。对于使用CGLIB或者Javassist等动态字节码生成工具的应用程序而言,设置合理的持久代大小有助于维持系统稳定。

以下代码显示了一个使用Javassist动态生成大量class类的应用,并在永久区溢出时,打印其生成的动态类总数。

jvm常见的调优命令(字节大牛百万调优经验之作)(7)

使用参数-XX:PermSize=4M-XX:MaxPermSize=4M运行该程序,输出结果如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(8)

将MaxPermSize加倍,使用-XX:PermSize=4M-XX:MaxPermSize=8M运行同样的代码,其输出结果如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(9)

可以看到,系统所支持的最大类数量与MaxPermSize成正比。一般来说,MaxPermSize设置为64MB就已经可以满足绝大部分应用程序的正常工作。如果依然出现永久区溢出,可以将MaxPermSize设置为128MB。这是两个很常用的永久区取值。如果128MB依然不能满足应用程序的需求,那么对于大部分应用程序来说,则应该考虑优化系统设计,减少动态类的产生,或者使用5.1.5小节“方法区”中提到的方法,利用GC回收部分驻扎在永久区中的无用类信息,以使系统健康运行。

从JDK8开始,Java虚拟机已经彻底抛弃了持久代,而改用元数据区存放类的元信息。元数据是一块直接内存。可以使用-XX:MaxMetaspaceSize指定可用的元数据区大小,如不指定,默认行为会试图耗尽所有可用的物理内存。因此,在JDK8中,如果想得到上述效果,需要使用虚拟机参数-XX:MaxMetaspaceSize=20M,读者可以自行尝试。PermSize和MaxPermSize参数在JDK8中已经废弃。

设置线程栈

线程栈是线程的一块私有空间。有关线程栈的详细描述可以参考5.1.2小节“Java虚拟机栈”。本节仅对线程栈做一些补充说明。

在JVM中,可以使用-Xss参数设置线程栈的大小。

在线程中进行局部变量分配和函数调用时,都需要在栈中开辟空间。如果栈的空间分配太小,那么线程在运行时,可能没有足够的空间分配局部变量或者达不到足够的函数调用深度而导致程序异常退出。如果栈空间过大,那么开设线程所需的内存成本就会上升,系统所能支持的线程总数就会下降。

由于Java堆也是向操作系统申请内存空间的,因此如果堆空间过大,就会导致操作系统可用于线程栈的内存减少,从而间接减少程序所能支持的线程数量。

以下代码尝试开设尽可能多的线程,并在线程数量饱和时打印已开设的线程数量。

jvm常见的调优命令(字节大牛百万调优经验之作)(10)

首先,简单地使用-Xss1M运行这段程序,即设置每个线程拥有1MB的栈空间。笔者的计算机上显示共可开设线程1803个。增大-Xss的值,使用参数-Xss20M为每个线程分配20MB的栈空间,结果在笔者的计算机上仅能支持81个线程。

如果指定系统的最大堆会发现,系统所能支持的线程数量还与堆的大小有关。结合最大堆参数和栈参数进行若干次实验,查看系统支持的最大线程数,结果如表5.1所示。

jvm常见的调优命令(字节大牛百万调优经验之作)(11)

表5.1栈大小与线程数的关系

Java堆的分配以200MB递增,当栈大小为1MB时,最大线程数量以200递减。说明每个线程确实占据了1MB空间,当系统的物理内存被堆占据时,就不可以被栈使用。同理,当栈空间为20MB时,最大线程数量以10递减,正好也是堆空间的递增值。

当系统由于内存空间不够而无法创建新的线程时,会抛出OOM(OutOfMemory)异常,如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(12)

根据以上讲解可知,这并不是由于堆内存不够而导致的OOM异常,而是因为操作系统内存减去堆内存后,剩余的系统内存不足而无法创建新的线程。在这种情况下,可以尝试减少堆内存以换取更多的系统空间来解决这个问题。

注意:这是一种非常特殊的OOM异常。它不是因为堆内存不够而溢出,而是因为栈空间不够所致。为了赢得更多的栈空间,可以适当减少(而不是增加)堆的大小,从而尽可能避免这种OOM异常。

综上所述,如果系统确实需要大量的线程并发执行,那么设置一个较小的堆和较小的栈,有助于提高系统所能承受的最大线程数。

堆的比例分配

在前几节中已经讲过如何设置最大堆、最小堆和新生代的大小。尤其对新生代大小的设置,介绍了-Xmn、-XX:NewSize和-XX:MaxNewSize三个参数,它们只可以设定一个固定大小的新生代空间。事实上,在实际生产环境中,更希望能够对堆空间进行比例分配。本节将介绍几个可用于将堆空间进行比例分配的JVM参数。

参数-XX:SurvivorRatio用来设置新生代中的eden空间和s0空间的比例关系。s0和s1空间又可称为from空间和to空间,它们的大小是相同的,功能也是一样的,在执行MinorGC后会互换角色。SurvivorRatio参数的含义如下:

-XX:SurvivorRatio=eden/s0=eden/s1

使用-XX: PrintGCDetails-Xmn10M-XX:SurvivorRatio=8运行一段简单的Java程序,便会有以下输出结果:

jvm常见的调优命令(字节大牛百万调优经验之作)(13)

修改参数,使用-XX: PrintGCDetails-Xmn10M-XX:SurvivorRatio=2运行程序,将比例设置为2,则程序输出结果如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(14)

上例中,指定新生代合计为10MB,并使eden区是s0的2倍大小,由于s1与s0相同,故有eden=10MB/(1 1 2)×2=5MB。

参数-XX:NewRatio可以用来设置新生代和老年代的比例,如下面的公式:

-XX:NewRatio=老年代/新生代

使用参数-XX: PrintGCDetails-XX:NewRatio=2-Xmx20M-Xms20M运行一段简单的代码,输出结果如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(15)

在本例中,因为堆大小为20MB,新生代和老年代的比例为1∶2,故新生代大小为20MB×1/3≈6MB左右,老年代为12MB左右。

注意:-XX:SurvivorRatio可以设置eden区与survivor区的比例,-XX:NewRatio可以设置老年代与新生代的比例。

这些JVM的XX参数在不同的JDK版本中的实现可能会略有不同。在具体使用时,可以使用-XX: PrintGCDetails参数打印出堆的实际大小,予以甄别。

堆分配参数总结

与Java应用程序堆内存相关的JVM参数如下:·-Xms:设置Java应用程序启动时的初始堆大小。·-Xmx:设置Java应用程序能获得的最大堆大小。·-Xss:设置线程栈的大小。

·-XX:MinHeapFreeRatio:设置堆空间最小空闲比例。当堆空间的空闲内存小于这个数值时,JVM便会扩展堆空间。

·-XX:MaxHeapFreeRatio:设置堆空间的最大空闲比例。当堆空间的空闲内存大于这个数值时,便会压缩堆空间,得到一个较小的堆。

·-XX:NewSize:设置新生代的大小。

·-XX:NewRatio:设置老年代与新生代的比例。它等于老年代大小除以新生代大小。

·-XX:SurviorRatio:设置新生代中eden区与survivior区的比例。·-XX:MaxPermSize:设置最大的持久区大小。·-XX:PermSize:设置永久区的初始值。

·-XX:TargetSurvivorRatio:设置survivior区的可使用率。当survivior区的空间使用率达到这个数值时,会将对象送入老年代。

如图5.6所示为主要的堆分配参数的含义。

jvm常见的调优命令(字节大牛百万调优经验之作)(16)

垃圾收集基础

Java语言的一大特点就是可以进行自动垃圾回收处理,开发人员无须过于关注系统资源(尤指内存资源)的释放情况。自动垃圾收集虽然大大减轻了开发人员的工作量,但同时也增加了软件系统的负担,一个不合适的垃圾回收方法和策略还会对系统性能造成不良影响。本节将主要介绍一些垃圾回收方法及HotSpot虚拟机支持的垃圾回收器,并对它们的使用和设置做简单的介绍。

垃圾收集的作用

拥有垃圾回收器可以说是Java语言与C 语言的一项显著区别。在C 语言中,程序员必须小心谨慎地处理每一项内存分配,但内存使用完后,必须手工释放曾经占用的内存空间。当内存释放不够完全时,即存在分配但永不释放的内存块,就会引起内存泄漏,严重时会导致程序瘫痪。

虽然,目前有许多自动化检测工具可以识别这些内存泄漏的代码点,但是这种纯手工管理内存的方法依然被不少人所诟病。为了解决这个问题,Java语言使用了垃圾回收器用来替代C 时代纯手工的内存管理方式,以减轻程序员的负担,减少出错的概率。

垃圾回收器要处理的基本问题如下:·

  1. 哪些对象需要回收?
  2. 何时回收这些对象?
  3. 如何回收这些对象?

垃圾回收算法与思想

垃圾回收器可以有多种不同的实现方式。本书不打算深入介绍各种垃圾回收器的具体实现算法,仅在本节中简单介绍主要的垃圾收集算法及其核心思想。

1.引用计数法

引用计数法(ReferenceCounting)是最经典也是最古老的一种垃圾收集方法,在微软的COM组件技术和Adobe的ActionScript3中,都可以找到引用计数器的身影。

引用计数器的实现很简单,对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1,当引用失效时,引用计数器就减1。只要对象A的应用计数器的值为0,则对象A就不可能再被使用。

引用计数器的实现也非常简单,只需要为每个对象配备一个整型计数器即可。但是,引用计数器有一个严重的问题,即无法处理循环引用的情况。因此,在Java的垃圾回收器中,没有使用这种算法。

例如,一个简单的循环引用问题:有对象A和对象B,对象A中含有对象B的引用,对象B中含有对象A的引用。此时,对象A和B的引用计数器都不为0。但是,在系统中却不存在任何第3个对象引用了A或B。也就是说,A和B是应该被回收的垃圾对象,但由于垃圾对象间的相互引用,从而使垃圾回收器无法识别,引起内存泄漏。

如图5.7所示,不可达的对象出现循环引用,它的引用计数器均不为0。

jvm常见的调优命令(字节大牛百万调优经验之作)(17)

因此,在Java语言中单纯地使用引用计数器算法实现垃圾回收是不可行的。

注意:由于无法处理循环引用的问题,引用计数法不适合用于JVM的垃圾回收。

标记-清除算法

标记-清除算法(Mark-Sweep)是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现方式是,在标记阶段,首先通过根节点标记所有从根节点开始的可达对象,因此未被标记的对象就是未被引用的垃圾对象。然后,在清除阶段清除所有未被标记的对象。标记-清除算法可能产生的最大问题就是空间碎片。

如图5.8所示,使用标记-清除算法对一块连续的内存空间进行回收。从根节点开始,所有的有引用关系的对象均被标记为存活对象(箭头表示引用),不可达的对象均为垃圾对象。在标记操作完成后,系统回收所有不可达的空间。

jvm常见的调优命令(字节大牛百万调优经验之作)(18)

由图5.8可以看到,回收后的空间是不连续的。在对象的堆空间分配过程中,尤其是大对象的内存分配,不连续的内存空间的工作效率要低于连续的空间,这也是标记-清除算法的最大缺点。

注意:标记-清除算法先通过根节点标记所有可达对象,然后清除所有不可达对象,从而完成垃圾回收。

3.复制算法

与标记-清除算法相比,复制算法(Copying)是一种相对高效的回收方法。它的核心思想是:将原有的内存空间分为两块,每次只使用其中的一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

如果系统中的垃圾对象很多,复制算法需要复制的存活对象数量并不会太大。因此,在真正需要垃圾回收的时刻,复制算法的效率是很高的。又因为对象是在垃圾回收过程中统一被复制到新的内存空间中的,因此可确保回收回的内存空间是没有碎片的。虽然有以上两大优点,但是复制算法的代价却是将系统内存折半,因此,单纯的复制算法也很难让人接受。

如图5.9所示,有A、B两块相同的内存空间,A在进行垃圾回收时将存活对象复制到B中,B中的空间在复制后保持连续。复制完成后,清空A,并将空间B设置为当前使用空间。

jvm常见的调优命令(字节大牛百万调优经验之作)(19)

在Java的新生代串行垃圾回收器中使用了复制算法的思想。新生代分为eden空间、from空间和to空间3个部分。其中,from和to空间可以视为用于复制的两块大小相同、地位相等且可进行角色互换的空间块。from和to空间也称为survivor空间,即幸存者空间,用于存放未被回收的对象。

在垃圾回收时,eden空间中的存活对象会被复制到未使用的survivor空间中(假设是to),正在使用的survivor空间(假设是from)中的年轻对象也会被复制到to空间中(大对象,或者老年对象会直接进入老年代,如果to空间已满,则对象也会直接进入老年代)。此时,eden空间和from空间中的剩余对象就是垃圾对象,可以直接清空,to空间则存放此次回收后的存活对象。

这种改进的复制算法既保证了空间的连续性,又避免了大量的内存空间浪费。如图5.10所示为串行垃圾回收器在新生代的回收过程。在完成所有复制操作到活动survivor区后,简单地清空eden区和无效的survivor区即可。

jvm常见的调优命令(字节大牛百万调优经验之作)(20)

注意:复制算法比较适用于新生代。因为在新生代中,垃圾对象通常会多于存活对象,复制算法的效果会比较好。

标记-压缩算法

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下,这种情况在年轻代经常发生。但是在老年代,更常见的情况是大部分对象都是存活对象,如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用新的算法。

标记-压缩算法(Mark-Compact)是一种老年代的回收算法,它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也需要首先从根节点开始对所有可达对象做一次标记。但之后,它并不是简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端,之后清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此其性价比较高。如图5-11所示为标记-压缩算法的工作示意图。

jvm常见的调优命令(字节大牛百万调优经验之作)(21)

增量算法

对于大部分垃圾回收算法而言,在垃圾回收的过程中,应用软件将处于一种StoptheWorld的状态。在StoptheWorld的状态下,应用程序的所有线程都会被挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收的时间很长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。

增量算法(IncrementalCollecting)的基本思想是,如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。垃圾收集线程每次只收集一小片区域的内存空间,接着切换到应用程序线程。如此反复,直到垃圾回收完成。使用这种方式,由于在垃圾回收的过程中间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

分代

前文中介绍了复制、标记-清除、标记-压缩等垃圾回收算法。在所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。因此,根据垃圾回收对象的特性,使用合适的算法回收,才是明智的选择。

分代(GenerationalCollecting)就是基于这种思想而设计的。它将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的回收算法,以提高垃圾回收的效率。

以HotSpot虚拟机为例,它将所有的新建对象都放入称为年轻代的内存区域,年轻代的特点是对象朝生夕灭,大约90%的新建对象会被很快回收,因此,在年轻代就要选择效率较高的复制算法。当一个对象经过几次回收后依然存活,对象就会被放入称为老年代的内存空间。在老年代中,几乎所有的对象都是经过几次垃圾回收后依然得以幸存的,因此可以认为这些对象在一段时期内,甚至在应用程序的整个生命周期中将是常驻内存的。

在极端情况下,老年代对象的存活率可以达到100%。如果依然使用复制算法回收老年代,将需要复制大量对象。再加上老年代的回收性价比也要低于新生代,因此这种做法是不可取的。根据分代的思想,可以对老年代的回收使用与新生代不同的标记-压缩算法,以提高垃圾回收效率。如图5.12所示为这种分代的思想。

注意:分代的思想被现有的HotSpot虚拟机广泛使用,几乎所有的垃圾回收器都区分年轻代和老年代。

jvm常见的调优命令(字节大牛百万调优经验之作)(22)

垃圾回收器的类型

从不同角度分析垃圾回收器,可以将垃圾回收器分为不同的类型。

按线程数分,可以分为串行垃圾回收器和并行垃圾回收器。串行垃圾回收器一次只使用一个线程进行垃圾回收,并行垃圾回收器一次将开启多个线程同时进行垃圾回收。在并行能力较强的CPU上,使用并行垃圾回收器可以缩短GC的停顿时间。

按照工作模式分,可以分为并发式垃圾回收器和独占式垃圾回收器。并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。独占式垃圾回收器(StoptheWorld)一旦运行,就停止应用程序中的其他所有线程,直到垃圾回收过程完全结束。

按碎片处理方式,可分为压缩式垃圾回收器和非压缩式垃圾回收器。压缩式垃圾回收器会在回收完成后对存活对象进行压缩整理,以消除回收后的碎片。非压缩式的垃圾回收器不进行这步操作。

按工作的内存区间,又可分为新生代垃圾回收器和老年代垃圾回收器。顾名思义,新生代垃圾回收器只在新生代工作,老年代垃圾回收器则工作在老年代。

垃圾回收器的分类如表5.2所示。

jvm常见的调优命令(字节大牛百万调优经验之作)(23)

评价GC策略的指标

评价一个垃圾回收器的好坏可以用以下指标进行衡量。

·吞吐量:指在应用程序的生命周期内,应用程序所花费的时间和系统总运行时间的比值。系统总运行时间=应用程序耗时 GC耗时。如果系统运行了100min,GC耗时1min,那么系统的吞吐量就是(100-1)/100=99%。

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

·停顿时间:指在垃圾回收器正在运行时,应用程序的暂停时间。对于独占式垃圾回收器而言,停顿时间可能会比较长。使用并发式垃圾回收器时,由于垃圾回收和应用程序交替运行,程序的停顿时间会变短,但是由于其效率很可能不如独占式垃圾回收器,故系统的吞吐量可能会较低。

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

·反应时间:指当一个对象称为垃圾后,多长时间内它所占据的内存空间会被释放。

·堆分配:不同的垃圾回收器对堆内存的分配方式可能不同。一个良好的垃圾回收器应该有一个合理的堆内存区间划分。

通常情况下,很难让一个应用程序在所有的指标上都达到最优,因此只能根据应用本身的特点,尽可能使垃圾回收器配合应用程序的工作。例如,对于客户端应用而言,应该尽可能降低其停顿时间,给用户良好的使用体验,为此可以牺牲垃圾回收的吞吐量。对后台服务程序来说,可能会更加关注吞吐量,所以可以适当延长系统的停顿时间。

新生代串行回收器

串行回收器是所有垃圾回收器中最古老的一种,也是JDK中最基本的垃圾回收器之一。串行回收器主要有两个特点:第一,它仅仅使用单线程进行垃圾回收;第二,它是独占式的垃圾回收。

在串行回收器进行垃圾回收时,Java应用程序中的线程都需要暂停,等待垃圾回收的完成,如图5.13所示,在串行回收器运行时,应用程序中的所有线程都停止工作,进行等待,这种现象称为StoptheWorld。它将造成非常糟糕的用户体验,在实时性要求较高的应用场景中,这种现象往往是不能接受的。

虽然如此,串行回收器却是一个成熟的、经过长时间生产环境考验的极为高效的回收器。新生代串行回收器使用复制算法,实现起来相对简单,逻辑处理特别高效,并且没有线程切换的开销。在诸如单CPU处理器或者较小的应用内存等硬件平台不是特别优越的场合,串行回收器的性能表现可以超过并行回收器和并发回收器。

在HotSpot虚拟机中,使用-XX: UseSerialGC参数可以指定使用新生代串行回收器和老年代串行回收器。当JVM在Client模式下运行时,它是默认的垃圾回收器。

jvm常见的调优命令(字节大牛百万调优经验之作)(24)

图5.13串行回收器示意图

一次新生代串行回收器的工作输出日志类似于如下信息(使用-XX: PrintGCDetails开关):

jvm常见的调优命令(字节大牛百万调优经验之作)(25)

它显示了一次垃圾回收前的新生代的内存占用量和垃圾回收后的新生代的内存占用量,以及垃圾回收所消耗的时间。

注意:串行垃圾回收器虽然古老,但是久经考验,在大多数情况下,其性能表现是相当不错的。

老年代串行回收器

老年代串行回收器使用的是标记-压缩算法。和新生代串行回收器一样,它也是一个串行的、独占式的垃圾回收器。由于老年代垃圾回收通常会使用比新生代回收更长的时间,因此在堆空间较大的应用程序中,一旦老年代串行回收器启动,应用程序很可能会因此停顿几秒甚至更长时间。

虽然如此,作为老牌的垃圾回收器,老年代串行回收器可以和多种新生代回收器配合使用,同时它也可以作为CMS回收器的备用回收器。

若要启用老年代串行回收器,可以尝试使用以下参数:

·-XX: UseSerialGC:新生代、老年代都使用串行回收器。

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

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

一次老年代串行回收器的工作输出日志类似于如下信息:

jvm常见的调优命令(字节大牛百万调优经验之作)(26)

以上信息显示了垃圾回收前老年代和永久区的内存占用量,以及垃圾回收后老年代和永久区的内存占用量。

并行回收器

并行回收器是一个工作在新生代的垃圾回收器。它只是简单地将串行回收器多线程化,它的回收策略、算法及参数和串行回收器一样。并行回收器的工作原理如图5.14所示。并行回收器也是独占式的回收器,在收集的过程中,应用程序会全部暂停。但由于并行回收器使用多线程进行垃圾回收,因此在并发能力比较强的CPU上,它产生的停顿时间要短于串行回收器,而在单CPU或者并发能力较弱的系统中,并行回收器的效果不会比串行回收器好,甚至由于多线程的压力,它的实际表现很可能比串行回收器差。

开启并行回收器可以使用以下参数:

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

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

并行回收器工作时的线程数量可以使用-XX:ParallelGCThreads参数指定。一般情况下,最好与CPU数量相当,避免过多的线程数影响垃圾收集的性能。在默认情况下,当CPU的数量小于8个时,ParallelGCThreads的值等于CPU的数量,当CPU的数量大于8个时,ParallelGC-Threads的值等于3 ((5×CPU_Count)/8)。

jvm常见的调优命令(字节大牛百万调优经验之作)(27)

一次并行回收器的日志输出信息如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(28)

可以看到,这个输出和新生代串行回收器几乎是一样的,只有回收器的标识符不同。

新生代并行回收器

新生代并行回收(ParallelScavenge)器也是使用复制算法的回收器。从表面上看,它和并行回收器一样,都是多线程、独占式的回收器。但是并行回收器有一个重要的特点:它非常关注系统的吞吐量。

新生代并行回收器可以使用以下参数启用:

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

·-XX: UseParallelOldGC:新生代和老年代都使用并行回收处理器。

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

·-XX:MaxGCPauseMillis:设置最大垃圾收集停顿时间。它的值是一个大于0的整数。回收器在工作时,会调整Java堆大小或者其他一些参数,尽可能把停顿时间控制在MaxGCPauseMillis以内。如果读者希望减少停顿时间而把这个值设得很小,为了达到预期的停顿时间,JVM可能会使用一个较小的堆(一个小堆比一个大堆回收快),这将导致垃圾回收变得很频繁,从而增加垃圾回收的总时间,降低吞吐量。

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

除此以外,新生代并行回收器与并行回收器的另一个不同之处在于,它还支持一种自适应的GC调节策略。使用-XX: UseAdaptiveSizePolicy可以打开自适应的GC调节策略。在这种模式下,新生代的大小、eden和survivior的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡。在手工调优比较困难的场合,可以直接使用这种自适应的方式,即仅指定虚拟机的最大堆、目标的吞吐量(GCTimeRatio)和停顿时间

(MaxGCPauseMillis),让虚拟机自己完成调优工作。新生代并行回收器的工作日志如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(29)

日志中显示了回收器的工作成果,也就是回收前的内存大小和回收后的内存大小,以及花费的时间。

注意:新生代并行回收器关注系统的吞吐量。可以通过-XX:MaxGCPauseMillis和-XX:GCTimeRatio参数设置期望的停顿时间和吞吐量大小。

老年代并行回收器

老年代并行回收器也是一种多线程并发的回收器。和新生代并行回收器一样,它也是一种关注吞吐量的回收器。老年代并行回收器使用标记-压缩算法,它在JDK1.6中才可以使用。如图5.15所示为并行回收器的工作模式。

jvm常见的调优命令(字节大牛百万调优经验之作)(30)

图5.15老年代并行回收器工作示意图

使用-XX: UseParallelOldGC参数,可以在新生代和老年代都使用并行回收器。这是一对非常关注吞吐量的垃圾回收器组合,在对吞吐量敏感的系统中可以考虑使用。参数-XX:ParallelGCThreads也可以用于设置垃圾回收时的线程数量。

老年代并行回收器的工作日志如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(31)

日志中显示了新生代、老年代及永久区在回收前后的情况,以及FullGC操作所消耗的时间。

CMS回收器

与并行回收器不同,CMS回收器主要关注系统停顿时间。CMS是ConcurrentMarkSweep的缩写,意为并发标记清除,从名称上就可以得知,它使用的是标记-清除算法,同时它又是一个使用多线程并行回收的垃圾回收器。

CMS回收器的工作过程与其他垃圾回收器相比略显复杂。CMS工作时的主要步骤有初始标记、并发标记、重新标记、并发清除和并发重置。其中初始标记和重新标记是独占系统资源的,而并发标记、并发清除和并发重置是可以和用户线程一起执行的。因此,从整体上说,CSM回收器不是独占式的,它可以在应用程序运行过程中进行垃圾回收。CMS回收器的工作流程如图5.16所示。

jvm常见的调优命令(字节大牛百万调优经验之作)(32)

图5.16CMS回收器工作示意图

根据标记-清除算法,初始标记、并发标记和重新标记都是为了标记出需要回收的对象。并发清理则是在标记完成后正式回收垃圾对象。并发重置是指在垃圾回收完成后,重新初始化CMS数据结构和数据,为下一次垃圾回收做好准备。并发标记、并发清理和并发重置都是可以和应用程序线程一起执行的。

CMS回收器在其主要的工作阶段虽然没有暴力地彻底暂停应用程序线程,但是由于它和应用程序线程并发执行,相互抢占CPU,故在CMS执行期内对应用程序吞吐量将造成一定影响。CMS默认启动的线程数是(ParallelGCThreads 3)/4,其中,ParallelGCThreads是新生代并行回收器的线程数。也可以通过-XX:ParallelCMSThreads参数手工设定CMS的线程数量。当CPU资源比较紧张时,受到CMS回收器线程的影响,应用系统的性能在垃圾回收阶段可能会非常糟糕。

由于CMS回收器不是独占式的回收器,在CMS回收过程中,应用程序仍然在不停地工作。在应用程序的工作过程中,又会不断地产生垃圾。这些新生成的垃圾在当前CMS回收过程中是无法清除的。同时,因为应用程序没有中断,故在CMS回收过程中还应该确保应用程序有足够的内存可用。因此,CMS回收器不会等待堆内存饱和时才进行垃圾回收,而是当堆内存使用率达到某一阈值时便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。

这个回收阈值可以使用-XX:CMSInitiatingOccupancyFraction来指定,默认是68,即当老年代的空间使用率达到68%时会执行一次CMS回收。如果应用程序的内存使用率增长很快,在CMS的执行过程中已经出现了内存不足的情况,此时CMS回收就会失败,JVM将启动老年代串行回收器进行垃圾回收。如果这样,应用程序将完全中断,直到垃圾收集完成,这时应用程序的停顿时间可能会很长。

注意:通过-XX:CMSInitiatingOccupancyFraction参数可以指定当老年代空间使用率达到一定比例时进行一次CMS垃圾回收。

因此,根据应用程序的特点,可以对-XX:CMSInitiatingOccupancyFraction进行调优。如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代的回收次数,可以较为明显地改善应用程序的性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行回收器。

CMS是一个基于标记-清除算法的回收器。在之前的篇章中已经提到,标记-清除算法将会造成大量的内存碎片,离散的可用空间无法分配较大的对象。在这种情况下,即使堆内存仍然有较大的剩余空间,也可能会被迫进行一次垃圾回收,以换取一块可用的连续内存。这种现象对系统性能是相当不利的。为了解决这个问题,CMS回收器还提供了几个用于内存压缩整理的参数。

-XX: UseCMSCompactAtFullCollection开关可以使CMS在垃圾收集完成后进行一次内存碎片整理。内存碎片的整理不是并发进行的。-XX:CMSFullGCsBeforeCompaction参数可以用于设定进行多少次CMS回收后执行一次内存压缩。

CMS回收器工作时的日志输出如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(33)

以上信息是一次CMS收集的输出结果。可以看到,CMS回收器的工作过程包括初始化标记、并发标记、重新标记、并发清理和重发重置等几个重要阶段。在日志中,还可以看到CMS的耗时及堆内存信息。

除此之外,CMS回收器在运行时还可能会输出如下日志:

jvm常见的调优命令(字节大牛百万调优经验之作)(34)

以上日志信息说明CMS回收器并发收集失败,这很可能是由于应用程序在运行过程中老年代空间不够所导致。如果在CMS工作过程中出现非常频繁的并发模式失败,就应该考虑进行调整,尽可能预留一个较大的老年代空间;或者可以设置一个较小的-XX:CMSInitiatingOccupancyFraction参数,以降低CMS触发的阈值,使CMS在执行过程中仍然有较大的老年代空闲空间供应用程序使用。

注意:CMS回收器是一个关注停顿的垃圾回收器。同时,CMS回收器在部分工作流程中可以与用户程序同时运行,从而降低应用程序的停顿时间。

G1回收器

G1回收器是目前较新的垃圾回收器。它在JDK1.6update14中提供了早期预览版,并伴随JDK7update4发布。从长期目标来看,它是为了取代CMS回收器而设计的。G1回收器拥有独特的垃圾回收策略,这和之前提到的回收器截然不同。从分代上看,G1依然属于分代垃圾回收器,它会区分年轻代和老年代,依然有eden区和survivor区,但从堆的结构上看,它并不要求整个eden区、年轻代或者老年代都连续。作为CMS的长期替代方案,G1同时使用了全新的分区算法,其特点如下:

·并行性:G1在回收期间可以由多个GC线程同时工作,从而有效利用多核计算能力。

·并发性:G1拥有与应用程序交替执行的能力,部分工作可以和应用程序同时执行,因此一般来说,不会在整个回收期间完全阻塞应用程序。

·分代GC:G1依然是一个分代回收器,但是和之前的回收器不同,它同时兼顾年轻代和老年代。除了G1外,其他垃圾回收器或者工作在年轻代,或者工作在老年代,这是一个很大的不同。

·空间整理:G1在回收过程中会进行适当的对象移动,而不像CMS只是简单地标记清理对象,在若干次GC操作后,CMS必须进行一次碎片整理。而G1不同,它每次回收都会有效地复制对象,从而减少空间碎片。

·可预见性:由于分区的原因,G1可以只选取部分区域进行内存回收,这样就缩小了回收的范围,因此对于全局停顿也能得到较好的控制。

G1回收器最大的特点就是将大块内存划分成大小相等的多个小块,并分别管理。这些小块叫作区(region),其结构如图5.17所示。

jvm常见的调优命令(字节大牛百万调优经验之作)(35)

图5.17G1的内存结构

每次回收时,G1会避免回收整个Java堆,而是根据算法,选择垃圾比例最高的几个区进行回收,因此大大降低了执行FullGC可能带来的长时间停顿。对比CMS回收器,虽然也在努力尝试减少一次回收产生的停顿时间,但随着回收次数不断增加,CMS回收器所管理的堆内存会逐渐地碎片化,因此不可避免地会进行一次长时间的FullGC操作。当这种FullGC操作发生在一个很大的堆上时(如32GB或64GB),系统可能因此而停顿几分钟。这对于延迟要求比较高的系统显然是不可接受的。而G1的这种分块回收思想则从理论上打破了虚拟机必须进行FullGC操作的怪圈。只要程序的内存使用是合理并且的GC操作是友好的,在不断地进行区间回收过程中,系统总是能保证在不进行FullGC操作的前提下还有空闲的内存可以使用。

可以使用-XX: UseG1GC标记打开G1回收器开关。对G1回收器进行设置时,最重要的一个参数就是-XX:MaxGCPauseMillis,它用于指定目标的最大停顿时间。如果任何一次停顿超过这个设置值时,G1就会尝试通过调整新生代和老年代的比例、堆大小和晋升年龄等手段,试图达到预设目标。对于性能调优来说,总是鱼和熊掌不可兼得的,如果停顿时间缩短,对于新生代来说,这意味着很可能要增加新生代GC操作的次数,GC操作反而会变得更加频繁。对于老年代区域来说,为了获得更短的停顿时间,那么在进行混合GC收集时,一次收集的区域数量也会变少,这样无疑增加了进行FullGC操作的可能性。

另外一个重要的参数是-XX:ParallelGCThreads,它用于设置并行回收时进行GC操作的工作线程数量。

此外,-XX:InitiatingHeapOccupancyPercent参数可以指定当整个堆使用率达到一定比例时,触发并发标记周期的执行,默认值是45,即当整个堆占用率达到45%时,执行并发标记周期。InitiatingHeapOccupancyPercent参数一旦设置,始终不会再被G1回收器修改,这意味着G1回收器不会试图改变这个值来满足MaxGCPauseMillis的目标。如果InitiatingHeapOccupancyPercent值设置偏大,会导致并发周期迟迟得不到启动,那么引起FullGC操作的可能性也大大增加;反之,一个过小的InitiatingHeapOccupancyPercent值会使得并发周期非常频繁,大量的GC线程抢占CPU,会导致应用程序的性能有所下降。

最后一个值得了解的参数是-XX:G1HeapRegionSize=n,它设置了G1的每个区(region)的大小。默认情况下,虚拟机会根据整个堆的大小计算一个合理的区大小,每个区的大小总是介于1MB~32MB之间。如果用户需要自己指定区大小,则可以使用这个参数,以避免虚拟机自行计算。

StoptheWorld案例

前文中曾经讲到,垃圾回收时,应用系统会产生一定的停顿,尤其在独占式的垃圾回收器中,整个应用程序会被停止,直到垃圾回收完成,这种现象可以称为StoptheWorld。

本节将以一个简单的例子说明垃圾回收对应用程序产生的影响,希望能给读者留下深刻的印象。这样,当生产环境遇到类似的问题时,系统莫名其妙地停止工作,读者便可以检查是否是由于垃圾回收引起的,进而排查问题。

以下是这个案例的代码:

jvm常见的调优命令(字节大牛百万调优经验之作)(36)

上面的代码定义了2个线程——MyThread和PrintThread。MyThread不停地申请系统内存,这将迫使进行垃圾回收;PrintThread则每隔0.1s打印出系统的启动时间。在正常情况下,系统应用每秒钟有10个输出。

使用参数-Xmx512M-Xms512M-XX: UseSerialGC-Xloggc:gc.log-XX: PrintGC-Details运行这段代码,部分输出结果如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(37)

以上省略部分都是看似正常的执行结果,即每秒钟有10条记录输出。但加粗部分显示程序似乎在这两个地方停顿了三四秒的时间。理论上,在这段时间内,应该有30~40条记录输出。PrintThread在这两个时间点上停顿了,并且时间超过了3秒。进一步查看GC操作的日志,截取部分输出信息如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(38)

从时间上看,以上两个GC操作发生的时间正好位于15s和20s左右,并且持续时间长达3s~5s,从发生时间和影响时间上看,这两个GC操作严重干扰了PrintThread的正常工作。这就是StoptheWorld产生的后果。

在笔者的开发经验中,垃圾回收器甚至还使系统产生过100s以上的停顿,从而造成了严重的业务中断。因此,慎重选择回收器并对其进行调优是相当重要的。

注意:垃圾回收产生的停顿可能会对应用程序的执行效果产生严重的不良影响。

垃圾回收器对系统性能的影响

本节将简单展示不同垃圾回收器对应用软件性能的影响,但目的并不是通过测试筛选出最优秀的垃圾回收器。事实上,在众多的垃圾回收器中并没有最好的,只有最适合应用的回收器。根据应用软件的特性及硬件平台的特点,选择不同的垃圾回收器才能有效地提高系统性能。

通过本节的样例测试,读者将看到不同的垃圾回收器对应用程序性能产生的直接影响。希望读者通过对本节的阅读,能够在实际工作中重视对垃圾回收器的选择。

测试代码如下:

jvm常见的调优命令(字节大牛百万调优经验之作)(39)

上面这段代码进行了1万次循环,每次分配512×100个字节空间,最后输出程序运行所消耗的时间。笔者在Dell1950服务器上(8核CPU)测试以上代码,得到的垃圾回收器和程序耗时的关系如表5.3所示。

jvm常见的调优命令(字节大牛百万调优经验之作)(40)

表5.3垃圾回收器的测试耗时

不同的回收器,其代码的执行时间有着较为明显的差别。在本例中,使用CMS回收器比并行回收器快了将近30%。并行回收器甚至比串行回收器更慢。当然,这并不能说明CMS回收器比并行回收器更好,只能说明CMS回收器更适合笔者的测试硬件环境和样例代码。

注意:根据应用程序的不同特点,可以选择不同的垃圾回收器,以提高应用程序的性能。

GC操作相关参数总结

1.与串行回收器相关的参数

下面是与串行回收器相关的参数。·-XX: UseSerialGC:在新生代和老年代使用串行回收器。·-XX:SurvivorRatio:设置eden区和survivior区的大小比例。

·-XX:PretenureSizeThreshold:设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配。

·-XX:MaxTenuringThreshold:设置对象进入老年代年龄的最大值。每一次执行MinorGC操作后,对象年龄就加1。任何大于这个年龄的对象,一定会进入老年代。

2.与并行回收器相关的参数

下面是与并行回收器相关的参数。·-XX: UseParNewGC:在新生代使用并行回收器。·-XX: UseParallelOldGC:在老年代使用并行回收器。

·-XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和CPU数量相等。但在CPU数量比较多的情况下,设置相对较小的数值也是合理的。

·-XX:MaxGCPauseMillis:设置最大垃圾收集的停顿时间,它的值是一个大于0的整数。回收器在工作时会调整Java堆大小或者其他参数,尽可能地把停顿时间控制在MaxGCPauseMillis以内。

·-XX:GCTimeRatio:设置吞吐量大小,它的值是一个0~100之间的整数。假设GCTimeRatio的值为n,那么系统将花费不超过1/(1 n)的时间用于垃圾收集。

·-XX: UseAdaptiveSizePolicy:打开自适应GC调节策略。在这种模式下,新生代的大小、eden和survivior的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡。

3.与CMS回收器相关的参数

下面是与CMS回收器相关的参数。

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

·-XX:ParallelCMSThreads:设定CMS的线程数量。

·-XX:CMSInitiatingOccupancyFraction:设置CMS回收器在老年代空间被使用多少后触发,默认为68%。

·-XX: UseCMSCompactAtFullCollection:设置CMS回收器在完成垃圾收集后是否要进行一次内存碎片的整理。

·-XX:CMSFullGCsBeforeCompaction:设置进行多少次CMS垃圾回收后,进行一次内存压缩。

·-XX: CMSClassUnloadingEnabled:允许对类元数据进行回收。

·-XX: CMSParallelRemarkEnabled:启用并行重标记。

·-XX:CMSInitiatingPermOccupancyFraction:当永久区占用率达到这一比例时,启动CMS回收(前提是-XX: CMSClassUnloadingEnabled被激活了)。

·-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阈值的时候才进行CMS回收。

·-XX: CMSIncrementalMode:使用增量模式,比较适合单CPU。

4.与G1回收器相关的参数

下面是与G1回收器相关的参数。

·-XX: UseG1GC:使用G1回收器。·-XX:MaxGCPauseMillis:设置最大垃圾收集的停顿时间。·-XX:GCPauseIntervalMillis:设置停顿间隔时间。·-XX:G1HeapRegionSize=n:手工设置每个区的大小。

5.其他参数

另外一个常用的参数是-XX: DisableExplicitGC,表示禁用显式的GC操作。

,