JVM调优概述

1 调优层次

性能调优包含多个层次,比如:架构调优、代码调优、JVM调优、数据库调优、操作系统调优等。 架构调优和代码调优是JVM调优的基础,其中架构调优是对系统影响最大的。

2 调优指标

吞吐量:运行用户代码的时间占总运行时间的行例 (总运行时间=程序的运行时间+内存回收的时间);
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间;
内存占用:java堆区所占的内存大小;

这三者共同构成一个“不可能三角”。三者总体的表现会随着技术进步而越来越好。一款优秀的收集器通常最多同时满足其中的两项。

简单来说,主要抓住两点:
吞吐量 吞吐量优先,意味着在单位时间内,STW的时间最短
暂停时间 暂停时间优先,意味这尽可能让单次STW的时间最短

在设计(或使用)GC算法时,必须确定我们的目标:一个GC算法只可能针对两个目标之一(即只专注于较大吞吐量或最小暂停时间),或尝试找一个二者的折衷。

现在标准,在最大吞吐量优先的情况下,降低停顿时间。

3 JVM调优原则

3.1 优先原则


优先架构调优和代码调优,JVM优化是不得已的手段,大多数的Java应用不需要进行JVM优化

3.2 堆设置


参数-Xms和-Xmx,通常设置为相同的值,避免运行时要不断扩展JVM内存,建议扩大至3-4倍FullGC后的老年代空间占用。

3.3 年轻代设置


参数-Xmn,1-1.5倍FullGC之后的老年代空间占用。

避免新生代设置过小,当新生代设置过小时,会带来两个问题:一是minor GC次数频繁,二是可能导致 minor GC对象直接进老年代。当老年代内存不足时,会触发Full GC。 避免新生代设置过大,当新生代设置过大时,会带来两个问题:一是老年代变小,可能导致Full GC频繁执行;二是 minor GC 执行回收的时间大幅度增加。

3.4 老年代设置


注重低延迟的应用

老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数

如果堆设置偏小,可能会造成内存碎片、高回收频率以及应用暂停

如果堆设置偏大,则需要较长的收集时间

吞吐量优先的应用 一般吞吐量优先的应用都有一个较大的年轻代和一个较小的老年代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而老年代尽可能存放长期存活对象

3.5 方法区设置


基于jdk1.7版本,永久代:参数-XX:PermSize和-XX:MaxPermSize; 基于jdk1.8版本,元空间:参数 -XX:MetaspaceSize和-XX:MaxMetaspaceSize; 通常设置为相同的值,避免运行时要不断扩展,建议扩大至1.2-1.5倍FullGc后的永久带空间占用。

3.6 GC设置


3.6.1 GC发展阶段
SerialParallel(并行) CMS(并发) G1ZGC 截至jdk1.8 ,一共有7款不同垃圾收集器。每一款不同的垃圾收集器都有不同的特点,在具体使用的时候,需要根据具体的情况选择不同的垃圾回收器


3.6.2 G1的适用场景
面向服务端应用,针对具有大内存、多处理器的机器。(在普通大小的堆里表现并不惊喜)

最主要的应用是需要低GC延迟并具有大堆的应用程序提供解决方案(G1通过每次只清理一部分而不是全部Region的增量式清理来保证每次GC停顿时间不会过长)

在堆大小约6GB或更大时,可预测的暂停时间可以低于0.5秒

用来替换掉JDK1.5中的CMS收集器,以下情况,使用G1可能比CMS好

超过50% 的java堆被活动数据占用

对象分配频率或年代提升频率变化很大

GC停顿时间过长(大于0.5至1秒)

从经验上来说,整体而言:

小内存应用上,CMS大概率会优于 G1;

大内存应用上,G1则很可能更胜一筹。 这个临界点大概是在 6~8G 之间(经验值)

3.6.3 其他收集器适用场景
如果你想要最小化地使用内存和并行开销,请选择Serial Old(老年代) + Serial(年轻代)

如果你想要最大化应用程序的吞吐量,请选择Parallel Old(老年代) + Parallel(年轻代)

如果你想要最小化GC的中断或停顿时间,请选择CMS(老年代) + ParNew(年轻代)

4 JVM调优步骤

4.1 监控分析


分析GC日志及dump文件,判断是否需要优化,确定瓶颈问题点。

4.1.1 如何生成GC日志
常用参数部分会详细讲解如何生成GC日志

4.1.2 如何产生dump文件
4.1.2.1 JVM的配置文件中配置
JVM启动时增加两个参数:

# 出现OOME时生成堆dump:
-XX:+HeapDumpOnOutOfMemoryError
# 生成堆文件地址:
-XX:HeapDumpPath=/home/hadoop/dump/
4.1.2.2 jmap生成
发现程序异常前通过执行指令,直接生成当前JVM的dump文件

jmap -dump:file=文件名.dump [pid]
# 9257是指JVM的进程号
jmap -dump:format=b,file=testmap.dump 9257
第一种方式是一种事后方式,需要等待当前JVM出现问题后才能生成dump文件,实时性不高; 第二种方式在执行时,JVM是暂停服务的,所以对线上的运行会产生影响。

所以建议第一种方式。

4.1.2.3 第三方可视化工具生成

4.2 判断


如果各项参数设置合理,系统没有超时日志或异常信息出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化,如果GC时间超过1-3秒,或者频繁GC,则必须优化。 遇到以下情况,就需要考虑进行JVM调优:

系统吞吐量与响应性能不高或下降;

Heap内存(老年代)持续上涨达到设置的最大内存值;

Full GC 次数频繁;

GC 停顿时间过长(超过1秒);

应用出现OutOfMemory等内存异常;

应用中有使用本地缓存且占用大量内存空间;

4.3 确定目标

调优的最终目的都是为了应用程序使用最小的硬件消耗来承载更大的吞吐量或者低延迟。 jvm调优主要是针对垃圾收集器的收集性能优化,减少GC的频率和Full GC的次数,令运行在虚拟机上的应用能够使用更少的内存、高吞吐量、低延迟。

下面列举一些JVM调优的量化目标参考实例,注意:不同应用的JVM调优量化目标是不一样的。

堆内存使用率<=70%;

老年代内存使用率<=70%;

avgpause<=1秒;

Full GC次数0或avg pause interval>=24小时 ;

4.4 调整参数

调优一般是从满足程序的内存使用需求开始的,之后是时间延迟的要求,最后才是吞吐量的要求。 要基于这个步骤来不断优化,每一个步骤都是进行下一步的基础,不可逆行之。

4.5 对比调优前后指标差异

4.6 重复以上过程

4.7 应用

找到合适的参数,先在单台服务器上试运行,然后将这些参数应用到所有服务器,并进行后续跟踪。

5 JVM调优工具


5.1 jps
5.2 jstat
5.3 jinfo

5.4 jmap
5.5 jhat

5.6 jstack


5.7 hprof


5.8 jconsole

6 JVM参数


在JVM调整过程中,主要是对JVM参数做的调整,以下我们介绍主要参数。JVM参数有很多,其实我们直接使用默认的JVM参数,不去修改都可以满足大多数情况。但是如果你想在有限的硬件资源下,部署的系统达到最大的运行效率,那么进行相关的JVM参数设置是必不可少的。

JVM参数主要分为以下三种:标准参数、非标准参数、不稳定参数。

6.1 标准参数


标准参数,顾名思义,标准参数中包括功能以及输出的结果都是很稳定的,基本上不会随着JVM版本的变化而变化。

标准参数以-开头,如:java -version、java -jar等,通过java -help可以查询所有的标准参数,-help 也是个标准参数。

6.2 非标准参数


非标准参数以-X开头,是标准参数的扩展。对应前面讲的标准化参数,这是非标准化参数。表示在将来的JVM版本中可能会发生改变,但是这类以-X开始的参数变化的比较小。 我们可以通过 Java -X 命令来检索所有-X 参数。

我们可以通过设置非标准参数来配置堆的内存分配,常用的非标准参数有:

-Xmn新生代内存的大小,包括Eden区和两个Survivor区的总和,写法如:-Xmn1024,-Xmn1024k,-Xmn1024m,-Xmn1g 。

-Xms堆内存的最小值,默认值是总内存/64(且小于1G)。默认情况下,当堆中可用内存小于40%(这个值可以用-XX: MinHeapFreeRatio 调整,如-X:MinHeapFreeRatio=30)时,堆内存会开始增加,直增加到-Xmx的大小。

-Xmx堆内存的最大值,默认值是总内存/4(且小于1G)。默认情况下,当堆中可用内存大于70%(这个值可以用-XX: MaxHeapFreeRatio调整,如-X:MaxHeapFreeRatio =80)时,堆内存会开始减少,一直减小到-Xms的大小。 *如果Xms和Xmx都不设置,则两者大小会相同*

-Xss每个线程的栈内存,默认1M,般来说是不需要改的。

-Xrs减少JVM对操作系统信号的使用。

-Xprof跟踪正运行的程序,并将跟踪数据在标准输出输出。适合于开发环境调试。

-Xnoclassgc关闭针对class的gc功能。因为其阻至内存回收,所以可能会导致OutOfMemoryError错误,慎用。

-Xincgc开启增量gc(默认为关闭)。这有助于减少长时间GC时应用程序出现的停顿,但由于可能和应用程序并发执行,所以会降低CPU对应用的处理能力。

-Xloggc:file与-verbose:gc功能类似,只是将每次GC事件的相关情况记录到一个文件中,文件的位置最好在本地,以避免网络的潜在问题。

6.3 不稳定参数


这是我们日常开发中接触到最多的参数类型。这也是非标准化参数,相对来说不稳定,随着JVM版本的变化可能会发生变化,主要用于JVM调优和debug。

不稳定参数以-XX 开头,此类参数的设置很容易引起JVM 性能上的差异,使JVM存在极大的不稳定性。如果此类参数设置合理将大大提高JVM的性能及稳定性。

不稳定参数分为三类:

性能参数:用于JVM的性能调优和内存分配控制,如内存大小的设置

行为参数:用于改变JVM的基础行为,如GC的方式和算法的选择

调试参数:用于监控、打印、输出jvm的信息

不稳定参数语法规则:

布尔类型参数值:

-XX:+

-XX:- 示例:-XX:+UseG1GC,表示启用G1垃圾收集器

数字类型参数值: -XX: 示例:-XX:MaxGCPauseMillis=500 ,表示设置GC的最大停顿时间是500ms

字符串类型参数值: -XX: 示例:-XX:HeapDumpPath=./dump.core

6.4 常参数


–Xms4g -Xmx4g –Xmn1200m –Xss512k
-XX:NewRatio=4 -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
-XX:PermSize=100m -XX:MaxPermSize=256m
-XX:MaxDirectMemorySize=1G -XX:+DisableExplicitGC
参数解析:

-Xms4g:初始化堆内存大小为4GB,ms是memory start的简称,等价于-XX:InitialHeapSize。

-Xmx4g:堆内存最大值为4GB,mx是memory max的简称,等价于-XX:MaxHeapSize。

-Xmn1200m:设置年轻代大小为1200MB。增大年轻代后,将会减小老年代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-Xss512k:设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1MB,以前每个线程堆栈大小为256K。应根据应用线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

-XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与老年代的比值(除去持久代)。设置为4,则年轻代与老年代所占比值为1:4,年轻代占整个堆栈的1/5

-XX:SurvivorRatio=8:设置年轻代中Eden区与Survivor区的大小比值。设置为8,则两个Survivor区与个Eden区的比值为2:8,个Survivor区占整个年轻代的1/10

-XX:PermSize=100m:初始化永久代大小为100MB。

-XX:MaxPermSize=256m:设置持久代大小为256MB。

-XX:MaxTenuringThreshold=15:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入老年代。对于老年代比较多的应用,可以提高效率。如果将此值设置为个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象在年轻代的存活时间,增加在年轻代即被回收的概率。

-XX:MaxDirectMemorySize=1G:直接内存。报java.lang.OutOfMemoryError: Direct buffermemory异常可以上调这个值。

-XX:+DisableExplicitGC:禁止运行期显式地调用System.gc()来触发fulll GC。 注意: Java RMI的定时GC触发机制可通过配置-Dsun.rmi.dgc.server.gcInterval=86400来控制触发的时间。

-XX:CMSInitiatingOccupancyFraction=60:老年代内存回收阈值,默认值为68。

-XX:ConcGCThreads=4:CMS垃圾回收器并行线程线,推荐值为CPU核心数。

-XX:ParallelGCThreads=8:新生代并行收集器的线程数。

-XX:CMSMaxAbortablePrecleanTime=500:当abortable-preclean预清理阶段执行达到这个时间时就会结束。

*新生代、老年代、永久代的参数,如果不进性指定,虚拟机会子动选择合适的值,同时也会基于系统的开销自动调整*。

6.4.1 -XX:+PrintFlagsInitial、-XX:+PrintFlagsFinal
Java 6(update 21oder 21之后)版本, HotSpot JVM 提供给了两个新的参数,在JVM启动后,在命令行中可以输出所有XX参数和值。 -XX:+PrintFlagsInitial:查看初始值 -XX:+PrintFlagsFinal:查看最终值(初始值可能被修改掉) 让我们现在就了解一下新参数的输出。以 -client 作为参数的 -XX:+ PrintFlagsFinal 的结果是一个按字母排序的590个参数表格(注意,每个release版本参数的数量会不一样)


表格的每一行包括五列,来表示一个XX参数。第一列表示参数的数据类型,第二列是名称,第四列为值,第五列是参数的类别。第三列”=”表示第四列是参数的默认值,而”:=” 表明了参数被用户或者JVM赋值了。

如果我们只想看下所有XX参数的默认值,能够用一个相关的参数,-XX:+PrintFlagsInitial 。 用 -XX:+PrintFlagsInitial , 只是展示了第三列为“=”的数据(也包括那些被设置其他值的参数)。

然而,注意当与-XX:+PrintFlagsFinal 对比的时候,一些参数会丢失,大概因为这些参数是动态创建的。

6.4.2 -XX:+PrintCommandLineFlags
让我们看下这个参数,事实上这个参数非常有用: -XX:+PrintCommandLineFlags 。这个参数让JVM打印出那些已经被用户或者JVM设置过的详细的XX参数的名称和值。

换句话说,它列举出 -XX:+PrintFlagsFinal的结果中第三列有":="的参数。以这种方式, 我们可以用-XX:+PrintCommandLineFlags作为快捷方式来查看修改过的参数。看下面的例子。


现在如果我们每次启动java 程序的时候设置 -XX:+PrintCommandLineFlags 并且输出到日志文件上,这样会记录下我们设置的JVM 参数对应用程序性能的影响。

6.4.3 GC日志相关
设置JVM GC格式日志的主要参数包括如下8个:

-XX:+PrintGC 输出简要GC日志

-XX:+PrintGCDetails 输出详细GC日志

-Xloggc:gc.log 输出GC日志到文件

-XX:+PrintGCTimeStamps 输出GC的时间戳(以JVM启动到当期的总时长的时间戳形式)

-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2020-04-26T21:53:59.234+0800)

-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息

-verbose:gc : 在JDK 8中,-verbose:gc是-XX:+PrintGC一个别称,日志格式等价于:-XX:+PrintGC。不过在JDK 9中 -XX:+PrintGC被标记为deprecated。 -verbose:gc是一个标准的选项,-XX:+PrintGC是一个实验的选项,建议使用-verbose:gc 替代-XX:+PrintGC

-XX:+PrintReferenceGC 打印年轻代各个引用的数量以及时长

开启GC日志 多种方法都能开启GC的日志功能,其中包括:使用-verbose:gc或-XX:+PrintGC这两个标志中的任意一个能创建基本的GC日志(这两个日志标志实际上互为别名,默认情况下的GC日志功能是关闭的)使用-XX:+PrintGCDetails标志会创建更详细的GC日志。 推荐使用-XX:+PrintGCDetails标志(这个标志默认情况下也是关闭的);通常情况下使用基本的GC日志很难诊断垃圾回收时发生的问题。

开启GC时间提示 除了使用详细的GC日志,我们还推荐使用-XX:+PrintGCTimeStamps或者-XX:+PrintGCDateStamps,便于我们更精确地判断几次GC操作之间的时间。这两个参数之间的差别在于时间戳是相对于0(依据JVM启动的时间)的值,而日期戳(date stamp)是实际的日期字符串。由于日期戳需要进性格式化,所以它的效率可能会受轻微的影响,不过这种操作并不频繁,它造成的影响也很难被我们感知。

指定GC日志路径 默认情况下GC日志直接输出到标准输出,不过使用-Xloggc:filename标志也能修改输出到某个文件。除了显式地使用-PrintGCDetails标志,否则使用-Xloggc会自动地开启基本日志模式。

使用日志循环(Log rotation)标志可以限制保存在GC日志中的数据量;对于需要长时间运行的服务器而言,这是一个非常有用的标志,否则累积几个月的数据很可能会耗尽服务器的磁盘。

开启日志滚动输出 通过-XX:+UseGCLogfileRotation -XX:NumberOfGCLogfiles=N -XX:GCLogfileSize=N标志可以控制日志文件的循环。 默认情况下,UseGCLogfileRotation标志是关闭的。它负责打开或关闭GC日志滚动记录功能的。要求必须设置 -Xloggc参数开启UseGCLogfileRotation标志后,默认的文件数目是0(意味着不作任何限制),默认的日志文件大小是0(同样也是不作任何限制)。因此,为了让日志循环功能真正生效,我们必须为所有这些标志设定值。

需要注意的是:

设置滚动日志文件的大小,必须大于8k。当前写日志文件大小超过该参数值时,日志将写入下一个文件

设置滚动日志文件的个数,必须大于等于1

必须设置 -Xloggc 参数

开启语句

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/home/hadoop/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=10 -XX:GCLogFileSize=512k
其他有用参数 -XX:+PrintGCApplicationStoppedTime 打印GC造成应用暂停的时间 -XX:+PrintTenuringDistribution 在每次新生代 young GC时,输出幸存区中对象的年龄分布

6.4.4 -XX:CMSFullGCsBeforeCompaction
CMSFullGCsBeforeCompaction 说的是,在上一次CMS并发GC执行过后,到底还要再执行多少次full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入full GC的时候都会做压缩。 如果把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的full GC才做一次压缩(而不是每10次CMS并发GC就做一次压缩,目前VM里没有这样的参数)。这会使full GC更少做压缩,也就更容易使CMS的old gen受碎片化问题的困扰。 本来这个参数就是用来配置降低full GC压缩的频率,以期减少某些full GC的暂停时间。CMS回退到full GC时用的算法是mark-sweep-compact,但compaction是可选的,不做的话碎片化会严重些但这次full GC的暂停时间会短些。这是个取舍。

-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=10
两个参数必须同时使用才能生效。

6.4.5 -XX:HeapDumpPath
堆内存出现OOM的概率是所有内存耗尽异常中最高的,出错时的堆内信息对解决问题非常有帮助,所以给JVM设置这个参数(-XX:+HeapDumpOnOutOfMemoryError),让JVM遇到OOM异常时能输出堆内信息,并通过(-XX:+HeapDumpPath)参数设置堆内存溢出快照输出的文件地址,这对于特别是对相隔数月才出现的OOM异常来说尤为重要。 这两个参数通常配套使用:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./
6.4.6 -XX:OnOutOfMemoryError
-XX:OnOutOfMemoryError=
"/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/binjconsole"
表示发生OOM后,运行jconsole程序。这里可以不用加“”,因为jconsole.exe路径Program Files含有空格。

利用这个参数,我们可以在系统OOM后,自定义一个脚本,可以用来发送邮件告警信息,可以用来重启系统等等。

6.4.7 XX:InitialCodeCacheSize
JVM一个有趣的,但往往被忽视的内存区域是“代码缓存”,它是用来存储已编译方法生成的本地代码。代码缓存确实很少引起性能问题,但是一旦发生其影响可能是毁灭性的。如果代码缓存被占满,JVM会打印出一条警告消息,并切换到interpreted-only 模式:JIT编译器被停用,字节码将不再会被编译成机器码。因此,应用程序将继续运行,但运行速度会降低一个数量级,直到有人注意到这个问题。

就像其他内存区域一样,我们可以自定义代码缓存的大小。相关的参数是-XX:InitialCodeCacheSize 和- XX:ReservedCodeCacheSize,它们的参数和上面介绍的参数一样,都是字节值。

6.4.8 -XX:+UseCodeCacheFlushing
如果代码缓存不断增长,例如,因为热部署引起的内存泄漏,那么提高代码的缓存大小只会延缓其发生溢出。

为了避免这种情况的发生,我们可以尝试一个有趣的新参数:当代码缓存被填满时让JVM放弃一些编译代码。通过使用-XX:+UseCodeCacheFlushing 这个参数,我们至少可以避免当代码缓存被填满的时候JVM切换到interpreted-only 模式。

不过,我仍建议尽快解决代码缓存问题发生的根本原因,如找出内存泄漏并修复它。

你可能感兴趣的:(Java,jvm)