一、简介
讲到JVM调优,很多对JVM接触不深或者只是在应对面试前强行背一些指令的程序员,会觉得这个东西太深入,加上平时不太用得到,不愿意花时间系统学习。
但其实可以换一个角度去想,Java发展这么多年,加之现在被Oracle收购后,研发团队实力更加雄厚,那些顶级大佬们每天都在花时间研究怎么提升JVM性能,每天都在做各种调试和优化,追求极致,那么,我们这些平时只是使用Java语言做开发的程序员,为什么还要去调优呢,又该如何调优呢。
接下来将从这两个问题出发,讲解JVM调优,这里可以先给一个小结论:我们可以调优的东西真的不多,相对固定,所以,不需要太担心过于深入而看不懂。
二、为什么要调优
在上一章《浅谈Java虚拟机(三)—垃圾回收》中,我们学习了垃圾回收算法,也简单提到了GC中的STW现象(即Stop The World:GC线程在执行清理的过程中会暂停用户线程,完成GC后再恢复用户线程),事实上,不论哪个分代的GC,运用什么样的算法,也不论哪个厂商实现的垃圾收集器,都躲不过STW,只是时间长短问题而已,毕竟枚举GC Roots怎么也需要停顿的。
STW导致的停顿时间,会使程序运行效率降低,甚至直接影响用户体验,早年间如果玩过JAVA做的游戏,就会发现游戏玩着玩着会偶发突然莫名其妙的卡顿,那个可能就是程序正在GC导致的,虽然现在经过这么多年的改良,停顿时间已经大大缩短,甚至有的垃圾收集器已经可以做到停顿时间可控,但针对不同程序的应用场景,即使短暂的停顿时间或者停顿太频繁也是不可接受的,因此围绕停顿时间的优化不管JVM研发团队还是我们普通开发人员都是需要一直做的。
缩短STW的时间自然是由JVM研发团队,更确切的说,应该是垃圾收集器研发团队来做,而我们需要调优的第一个方向就是根据我们程序的应用场景选择合适的垃圾收集器。另外,虽然只要是GC都不可避免STW,但发生在年轻代的Minor GC效率很高,加上年轻代只有老年代的二分之一,相对来说内存区域也比较小,因此STW停顿时间很短,反之Full GC引起的停顿时间就会较长,因此,我们第二个调优方向就是尽可能减少GC发生的次数,尤其是Full GC(Full GC发生次数与我们写的代码息息相关,这是JVM研发团队无论如何也无法掌控的,所以,使用好的框架,保持良好的编码习惯,以及对项目的监控调优都只能是我们才能完成的。)
三、如何调优
3.1 垃圾收集器的选择
选择收集器的前提是我们得先认识不同的垃圾收集器以及它们的优缺点,JVM有自己的默认垃圾收集器,但也可以通过启动参数的配置来选择自己需要的垃圾收集器。
Serial收集器(-XX:+UserSerialGC)
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器。
顾名思义,这是一个单线程收集器,它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
采用复制算法,主要用于新生代的垃圾收集。
Serial收集器的优点在于它简单而高效(与其他收集器的单线程相比),由于没有线程交互的开销,自然可以获得很高的单线程收集效率,对于运行在Client模式(桌面应用)下的虚拟机来说是个不错的选择,或是在用户的桌面应用场景中,可用内存一般不大(几十M至一两百M),可以在较短时间内完成垃圾收集(几十MS至一百多MS),只要不频繁发生,这也是可以接受的。
Serial Old收集器(没有直接设置这个收集器的参数,还是用-XX:+UserSerialGC,可用其他新生代收集器的设置参数来替换掉Serial收集器)
Serial收集器的老年代版本,使用标记-整理算法,它同样是一个单线程收集器。
它主要有两大用途:一种用途是在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用,另一种用途是作为CMS收集器的后备方案。
ParNew收集器(XX:+UseParNewGC)
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器完全一样,ParNew 在单核 CPU 环境并不会比 Serial 收集器达到更好的效果,它默认开启的收集线程数和 CPU 数量一致,可以通过 -XX:ParallelGCThreads 来设置垃圾收集的线程数。
同样采用复制算法,用于新生代垃圾收集。
它是许多运行在Server模式下的虚拟机的首要选择。
Parallel Scavenge收集器(-XX:+UseParallelGC,JDK7、8默认使用)
Parallel Scavenge 收集器类似于ParNew 收集器,可以看作 ParNew收集器进一步的升级版。
Parallel Scavenge收集器关注点是吞吐量(高效率的利用CPU),所谓吞吐量就是CPU中用于运行用户代码的时间与CPU总消耗时间(运行用户代码时间+GC时间)的比值。并非收集时间越短,吞吐量就越高,比如一个收集器每60秒收集一次,一次花费5秒,另一收集器每30秒收集一次,每次花费3秒,后者单次收集时间更快,但总体吞吐量却更低了。
可以通过 -XX:MaxGCPauseMillis 来设置收集器尽可能在多长时间内完成内存回收,可以通过 -XX:GCTimeRatio 来较精确控制吞吐量。
使用多线程和复制算法,用于新生代垃圾收集。
Parallel Old收集器(-XX:+UseParallelOldGC,JDK7、8默认使用)
Parallel Scavenge收集器的老年代版本。
使用多线程和标记-整理算法,用于老年代垃圾收集。
CMS收集器(XX:+UserConMarkSweepGC)
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它而非常符合在注重用户体验的应用上使用。
CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
这里补充解释一下前面的并行处理器和这里的并发处理器概念上的区别:
并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行(可能是多CPU并行处理,也可能是单CPU快速切换时间片交替执行)。
从该收集器名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
1.初始标记(CMS initial mark): 暂停所有的其他线程,并记录下直接与root相连的对象,速度很快,单线程执行。
2.并发标记(CMS concurrent mark): 同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
3.重新标记(CMS remark): 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
4.并发清除(CMS concurrent sweep): 开启用户线程,同时GC线程开始对为标记的区域做清扫。
使用多线程和标记-清除算法,用于老年代垃圾收集。
CMS主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
对CPU资源敏感,随着 CPU 数量下降,占用 CPU 资源就越多,吞吐量也越小(换言之,CMS收集器更适配于多核处理器);
无法处理浮动垃圾(即并发过程中,由于用户线程未停止,会不断产生新对象,也可能使之前未标记的对象成为无引用的可回收对象,这部分称为浮动垃圾);
它使用的回收算法“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
G1收集器(-XX:+UseG1GC,JDK9默认使用)
G1 (Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多核处理器及大容量内存的机器,以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
前面的收集器都是针对于分代收集算法的新生代或老年代,而G1收集器直接作用于整个堆。
G1将整个Java堆划分为多个大小固定的独立区域(Region),但仍然保留了分代收集算法中的新生代和老年代划分,只不过每一个Region都可能是新生代和老年代,还有一个Humongous分区,专门用来放大对象,这些区域的划分也是为了G1方便统计和清理。
从整个回收过程而言,它同样分为4步,与CMS一样,不同在于最后一步的清理过程(G1将这一步命名为筛选回收),G1会跟踪所有Region的垃圾堆积面积,在后台维护一个优先级列表,每次根据用户设置的允许收集时间,优先回收垃圾最多的区域(这也是Garbage-First名字的由来),这样保证了G1收集器在有限的时间内可以获得最高的收集效率。
它与前面讲的 CMS 垃圾收集器相比,有两个显著的改进:
1.采用 标记-整理 的回收算法
对于每一个Region局部而言,是采用复制算法,但从整体而言,采用标记整理算法,这样不会产生空间碎片。
2.可以精确的控制停顿时间
使用参数-XX:MaxGCPauseMillis能让使用者明确指定消耗在垃圾回收上的时间不超过指定毫秒数。
ZGC是JDK11使用的最新的收集器,可以一次收集最大为T为单位的垃圾,十分强大,但由于笔者暂时对其并不了解,所以这里暂且不谈。
如何选择垃圾收集器
其实上面介绍各个收集器优缺点时或多或少提到过其应用场景,这里简单做一个总结:
如果内存较小的服务器(小于100m)或单核并且没有停顿时间的要求,可使用串行收集器(Serial);
如果不是很要求停顿时间,且比较注重吞吐量,高效利用 CPU,需要高效运算且不需要太多交互,选择并行( Parallel Scavenge);
如果响应时间最重要,并且不能超过1秒,使用并发收集器(CMS G1);
这里需要额外注意的一点是CMS作为老年代收集器,但却无法与JDK1.4已经存在的新生代收集器Parallel Scavenge配合工作, 因为Parallel Scavenge(以及G1)都没有使用传统的GC收集器代码框架,而是另外独立实现,其余几种收集器则共用了部分的框架代码。因此在搭配垃圾收集器时,要注意,能与CMS搭配的只有ParNew和Serial收集器。
综合来说,官方推荐G1,其性能各方面都脱颖而出,当然最终还是要根据服务器和应用情况来定。
3.2 GC调优
从上面各垃圾收集器的特点以及综合发展历程来说,也可以看出对于JVM调优,主要就是调整两个指标:停顿时间和吞吐量。我们减少GC的次数最终目的亦是如此。
我们要从GC日志中来分析GC发生的时间和原因,因此首先加上启动参数-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:./gc.log,四个参数分别是打印GC详细日志,打印GC时间戳(自JVM启动以后以秒计量),打印GC发生的系统时间,GC日志存放位置(我这里放在了项目根目录)。
笔者启动了一个基于spring boot的后台管理系统,并未做其他操作,直接到项目根路径找到gc日志并打开,内容如下(这里笔者没有指定收集器,使用的JDK8,默认收集器为Parallel Scavenge,因此以下是Parallel Scavenge的GC日志格式,如果是CMS或者G1,格式会跟下面内容不同,更重要的是学习调优的方法):
Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for windows-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:36:39 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8303556k(2147924k free), swap 19497824k(6824340k free)
CommandLine flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:InitialHeapSize=132856896 -XX:+ManagementServer -XX:MaxHeapSize=2125710336 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
2020-12-14T16:56:58.160+0800: 1.783: [GC (Allocation Failure) [PSYoungGen: 33280K->4161K(38400K)] 33280K->4169K(125952K), 0.1588282 secs] [Times: user=0.00 sys=0.00, real=0.16 secs]
2020-12-14T16:56:58.618+0800: 2.149: [GC (Allocation Failure) [PSYoungGen: 37424K->4640K(38400K)] 37432K->4656K(125952K), 0.0096581 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-14T16:56:58.996+0800: 2.527: [GC (Allocation Failure) [PSYoungGen: 37920K->4553K(38400K)] 37936K->4577K(125952K), 0.0141848 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2020-12-14T16:56:59.432+0800: 2.964: [GC (Allocation Failure) [PSYoungGen: 37833K->5113K(71680K)] 37857K->5518K(159232K), 0.0067484 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-14T16:56:59.863+0800: 3.395: [GC (Allocation Failure) [PSYoungGen: 71673K->5092K(71680K)] 72078K->7683K(159232K), 0.0136604 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-14T16:57:00.755+0800: 4.286: [GC (Metadata GC Threshold) [PSYoungGen: 68897K->7159K(138240K)] 71489K->11699K(225792K), 0.0123722 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-14T16:57:00.768+0800: 4.299: [Full GC (Metadata GC Threshold) [PSYoungGen: 7159K->0K(138240K)] [ParOldGen: 4540K->8861K(54272K)] 11699K->8861K(192512K), [Metaspace: 20620K->20618K(1067008K)], 0.0353734 secs] [Times: user=0.13 sys=0.00, real=0.04 secs]
2020-12-14T16:57:01.860+0800: 5.392: [GC (Allocation Failure) [PSYoungGen: 131072K->6138K(140288K)] 139933K->15007K(194560K), 0.0078854 secs] [Times: user=0.03 sys=0.02, real=0.01 secs]
2020-12-14T16:57:02.439+0800: 5.970: [GC (Allocation Failure) [PSYoungGen: 137210K->8156K(226816K)] 146079K->17097K(281088K), 0.0080187 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
2020-12-14T16:57:03.950+0800: 7.481: [GC (Allocation Failure) [PSYoungGen: 226268K->9716K(227840K)] 235209K->25743K(282112K), 0.0166100 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
2020-12-14T16:57:04.336+0800: 7.867: [GC (Metadata GC Threshold) [PSYoungGen: 55486K->8346K(315904K)] 71513K->29205K(370176K), 0.0193028 secs] [Times: user=0.05 sys=0.00, real=0.02 secs]
2020-12-14T16:57:04.356+0800: 7.887: [Full GC (Metadata GC Threshold) [PSYoungGen: 8346K->0K(315904K)] [ParOldGen: 20859K->18807K(86016K)] 29205K->18807K(401920K), [Metaspace: 33912K->33912K(1079296K)], 0.0916556 secs] [Times: user=0.27 sys=0.00, real=0.09 secs]
2020-12-14T16:57:08.468+0800: 12.000: [GC (Allocation Failure) [PSYoungGen: 302592K->13300K(315904K)] 321399K->34767K(401920K), 0.0135112 secs] [Times: user=0.05 sys=0.02, real=0.01 secs]
2020-12-14T16:57:10.831+0800: 14.363: [GC (Metadata GC Threshold) [PSYoungGen: 234414K->14848K(405504K)] 255881K->40860K(491520K), 0.0169806 secs] [Times: user=0.06 sys=0.01, real=0.02 secs]
2020-12-14T16:57:10.848+0800: 14.380: [Full GC (Metadata GC Threshold) [PSYoungGen: 14848K->0K(405504K)] [ParOldGen: 26011K->35887K(125952K)] 40860K->35887K(531456K), [Metaspace: 56473K->56466K(1099776K)], 0.2215918 secs] [Times: user=0.61 sys=0.02, real=0.22 secs]
2020-12-14T16:57:13.807+0800: 17.339: [GC (Allocation Failure) [PSYoungGen: 390144K->15517K(411648K)] 426031K->51413K(537600K), 0.0222413 secs] [Times: user=0.03 sys=0.02, real=0.02 secs]
在上一章中已经介绍过如何读GC日志,详细解释这里不再重复,只是简单回顾一下:
2020-12-14T16:56:58.160+0800: 1.783: [GC (Allocation Failure) [PSYoungGen: 33280K->4161K(38400K)] 33280K->4169K(125952K), 0.1588282 secs] [Times: user=0.00 sys=0.00, real=0.16 secs]
这是第一条真正关于GC的信息,前面的时间忽略,从时间后开始读,这里发生的是普通GC,发生原因为Allocation Failure,即内存分配失败,程序刚启动,大量创建对象,触发这个很正常,后面为发生这次Young GC前后年轻代的内存大小变化(如果发生的Full GC就还会有老年代、元空间内存大小变化)。
好了,基于这个简单回顾,接下来我们首先关注第一次发生Full GC的日志信息:
2020-12-14T16:57:00.768+0800: 4.299: [Full GC (Metadata GC Threshold) [PSYoungGen: 7159K->0K(138240K)] [ParOldGen: 4540K->8861K(54272K)] 11699K->8861K(192512K), [Metaspace: 20620K->20618K(1067008K)], 0.0353734 secs] [Times: user=0.13 sys=0.00, real=0.04 secs]
有了前面对GC日志的认识,这里就简单了:这次触发Full GC的原因是Metadata GC Threshold,即元空间达到阈值。并且从后面 [Metaspace: 20620K->20618K(1067008K)]可以看出GC前后,元空间的内存占用并无变化,其实原因也很容易想到,程序刚启动,大量的Class元数据信息被加载进元空间,这个时候的清理自然没什么效果,我们马上找到下一条Full GC信息:
2020-12-14T16:57:04.356+0800: 7.887: [Full GC (Metadata GC Threshold) [PSYoungGen: 8346K->0K(315904K)] [ParOldGen: 20859K->18807K(86016K)] 29205K->18807K(401920K), [Metaspace: 33912K->33912K(1079296K)], 0.0916556 secs] [Times: user=0.27 sys=0.00, real=0.09 secs]
仍然是元空间触发的Full GC,这条还可以看出另外一点,元空间的阈值比上一次Full GC时变大了( [Metaspace: 33912K->33912K(1079296K)]),也侧面印证了元空间是动态扩展的。
那么解决此次Full GC的方案就很明了了,使用-XX:MetaspaceSize=128M 增大初始元空间大小(元空间的默认初始大小是20.75MB,我这里将其设置为128M),再看更改设置后重新启动项目的GC日志:
Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for windows-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:36:39 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8303556k(2495208k free),swap 18265028k(10087868k free)
CommandLine flags: -XX:-BytecodeVerificationLocal -XX:-BytecodeVerificationRemote -XX:InitialHeapSize=132856896 -XX:+ManagementServer -XX:MaxHeapSize=2125710336 -XX:MetaspaceSize=134217728 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:TieredStopAtLevel=1 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
2020-12-15T14:45:45.254+0800: 1.277: [GC (GCLocker Initiated GC) [PSYoungGen: 33280K->4130K(38400K)] 33280K->4146K(125952K), 0.0054048 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-15T14:45:45.375+0800: 1.399: [GC (Allocation Failure) [PSYoungGen: 37398K->4463K(38400K)] 40738K->7811K(125952K), 0.0087424 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2020-12-15T14:45:45.687+0800: 1.711: [GC (Allocation Failure) [PSYoungGen: 37743K->4781K(38400K)] 41091K->8137K(125952K), 0.0060392 secs] [Times: user=0.03 sys=0.01, real=0.01 secs]
2020-12-15T14:45:45.930+0800: 1.954: [GC (Allocation Failure) [PSYoungGen: 38061K->5108K(71680K)] 41417K->8910K(159232K), 0.0074853 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-15T14:45:46.225+0800: 2.249: [GC (Allocation Failure) [PSYoungGen: 71668K->5107K(71680K)] 75470K->11170K(159232K), 0.0097313 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-15T14:45:46.619+0800: 2.643: [GC (Allocation Failure) [PSYoungGen: 71667K->7679K(138752K)] 77730K->15449K(226304K), 0.0112721 secs] [Times: user=0.03 sys=0.03, real=0.01 secs]
2020-12-15T14:45:47.333+0800: 3.358: [GC (Allocation Failure) [PSYoungGen: 138751K->8713K(140800K)] 146521K->19498K(228352K), 0.0116370 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-15T14:45:47.881+0800: 3.905: [GC (Allocation Failure) [PSYoungGen: 139785K->9793K(270848K)] 150570K->21575K(358400K), 0.0089494 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
2020-12-15T14:45:49.568+0800: 5.592: [GC (Allocation Failure) [PSYoungGen: 270401K->11771K(272384K)] 282183K->34080K(359936K), 0.0197679 secs] [Times: user=0.06 sys=0.00, real=0.02 secs]
2020-12-15T14:45:50.989+0800: 7.013: [GC (Allocation Failure) [PSYoungGen: 272379K->17405K(429568K)] 294688K->47770K(517120K), 0.0207186 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
2020-12-15T14:45:53.296+0800: 9.320: [GC (Allocation Failure) [PSYoungGen: 429565K->21845K(434688K)] 459930K->62361K(522240K), 0.0283819 secs] [Times: user=0.13 sys=0.00, real=0.03 secs]
可以明显看出,整个启动过程中,没有发生Full GC(修改前是三次,根据实际情况调整合适的元空间初始值,笔者这里为了效果一次性调得比较大),那么我们的此次调优目的就达到了,事实上,绝大多数调优的第一步都是增大元空间大小(项目比较大的时候,如果是十分微型的项目,可能不需要这步调优)。
基于对Full GC的调优,我们对新生代也可以如法炮制,增大新生代动态扩容增量(默认是20%),可以减少Young GC:-XX:YoungGenerationSizeIncrement=30,效果这里不再展示。
可能看GC日志比较枯燥,这里笔者再推荐一个很好用的可视化在线GC日志分析工具:gceasy.io(不需要翻墙),我们再用这个工具来展示上面调优前后的效果。
打开网站上传GC日志,点击分析按钮:
分析完成会跳转页面,然后找到这里:
这次上传的是调优前的日志,可以看到,标注的几个关键指标分别就是吞吐量:95.886%,平均GC停顿时间35毫秒,最大单次GC停顿时间为200毫秒,再往下翻可以看到GC次数统计:
可以看到,Minor GC共13次,Full GC共3次,当然,这些数据都可以从GC日志中自行计算,但有好的工具辅助我们调优,何乐不为呢,加上如果GC日志非常长,人工统计难免繁琐易错。
好了,废话不多说,重新上传调优后的日志再看数据:
可以看到,调优后,吞吐量达到了98.21%,平均GC停顿时间15.5毫秒,最大单次GC停顿时间40毫秒,Minor GC11次,Full GC0次,这里再给大家做一个表格,更直观看出调优前后数据对比:
这两步调优能解决大多数项目启动慢的问题,至于项目运行中的情况,可能会更复杂,也因项目而异,但调优的方法都差不多:分析GC产生的原因,针对原因设置JVM启动参数,其实上面给出的这个工具中也有GC原因统计:
调优的指令也有很多,这里不可能全部模拟出来并使用,只能列举出来,读者在平时调优过程中可能会用到:
堆栈设置
-Xss:每个线程的栈大小
-Xms:初始堆大小,默认物理内存的1/64
-Xmx:最大堆大小,默认物理内存的1/4
-Xmn:新生代大小
-XX:NewSize:设置新生代初始大小
-XX:NewRatio:默认2,表示新生代占年老代的1/2,占整个堆内存的1/3。
-XX:SurvivorRatio:默认8,表示一个survivor区占用1/8的Eden内存,即1/10的新生代内存。
-XX:YoungGenerationSizeIncrement: 设置新生代动态扩容增量
-XX:MetaspaceSize:设置元空间初始大小
-XX:MaxMetaspaceSize:设置元空间最大允许大小,默认不受限制,JVM Metaspace会进行动态扩展。
收集器设置
-XX:+UseSerialGC:设置串行收集器
-XX:+UseParallelGC:设置并行收集器
-XX:+UseParallelOldGC:老年代使用并行回收收集器
-XX:+UseParNewGC:在新生代使用并行收集器
-XX:+UseParalledlOldGC:设置并行老年代收集器
-XX:+UseConcMarkSweepGC:设置CMS并发收集器
-XX:+UseG1GC:设置G1收集器
-XX:ParallelGCThreads:设置用于垃圾回收的线程数
并行收集器设置
-XX:ParallelGCThreads:设置并行收集器收集时使用的CPU数。并行收集线程数。
-XX:MaxGCPauseMillis:设置并行收集最大暂停时间
-XX:GCTimeRatio:设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)
CMS收集器设置
-XX:+UseConcMarkSweepGC:设置CMS并发收集器
-XX:+CMSIncrementalMode:设置为增量模式。适用于单CPU情况。
-XX:ParallelGCThreads:设置并发收集器新生代收集方式为并行收集时,使用的CPU数。并行收集线程数。
-XX:CMSFullGCsBeforeCompaction:设定进行多少次CMS垃圾回收后,进行一次内存压缩
-XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收
-XX:UseCMSInitiatingOccupancyOnly:表示只在到达阀值的时候,才进行CMS回收
-XX:ParallelCMSThreads:设定CMS的线程数量
-XX:CMSInitiatingOccupancyFraction:设置CMS收集器在老年代空间被使用多少后触发
-XX:+UseCMSCompactAtFullCollection:设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片的整理
G1收集器设置
-XX:+UseG1GC:使用G1收集器
-XX:ParallelGCThreads:指定GC工作的线程数量
-XX:G1HeapRegionSize:指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
-XX:GCTimeRatio:吞吐量大小,0-100的整数(默认9),值为n则系统将花费不超过1/(1+n)的时间用于垃圾收集
-XX:MaxGCPauseMillis:最大停顿时间(默认200ms)
-XX:G1NewSizePercent:新生代内存初始空间(默认整堆5%)
-XX:G1MaxNewSizePercent:新生代内存最大空间
-XX:TargetSurvivorRatio:Survivor填充容量(默认50%)
-XX:MaxTenuringThreshold:最大任期阈值(默认15)
-XX:InitiatingHeapOccupancyPercen:老年代占用空间超过整堆比IHOP阈值(默认45%),超过则执行混合收集
-XX:G1HeapWastePercent:堆废物百分比(默认5%)
-XX:G1MixedGCCountTarget:参数混合周期的最大总次数(默认8)
四、总结
本章讲解了JVM调优,如何选择垃圾收集器,如何减少GC,调优的步骤和方法其实很简单,也很单调,但JVM调优讲究的更多是经验累积,读者可以下来后用自己的项目进行调优实战,熟能生巧。
本章并未讲解JMap、JStat这种JVM监控命令,还有JConsole、JVisualvm这种可视化监控工具,主要是使用这些的目的更多是监控项目运行情况和定位如内存溢出这种问题,个人理解其并不在调优范畴,感兴趣的读者可以搜索其他作者关于JVM监控和分析专题的文章。
JVM的内容也远不止这些,笔者“浅谈Java虚拟机系列”主要一则是分享自己的学习历程,二者是引领对JVM不了解的Java学习者们从认识虚拟机到进行很多同行都比较“害怕”的JVM调优,每一篇文章都层层递进,从了解到上手自此打开JVM这扇大门,后续的学习笔者与读者共勉。
那么本系列就到此结束,后续会更新其他Java系列的文章,学无止境,一起加油。
本系列文章参考文档:《深入理解Java虚拟机:JVM高级特性与最佳实践》-- 周志明