翻译文章:JVM Tuning: How to Prepare Your Environment for Performance Tuning
当涉及到Java应用程序时,要确保它们以最高性能运行,至关重要的是缩小代码与运行它的虚拟机之间的资源差距(如果有的话)。做到这一点的方法是深入并微调Java虚拟机
尽管很关键,但是调整JVM不足以确保最佳性能。例如,如果应用程序的架构设计不佳或代码编写不佳,那么仅通过调整JVM就无法期望性能飞速增长
做得很好的调优将研究整个系统以及可能影响性能的所有层,包括数据库和操作系统。就是说,当您处于执行JVM调整的阶段时,请假定项目的体系结构和代码是最佳的或已调整。但是,在深入研究它之前,您必须设置性能优化目标并确定当前的性能问题。这些目标将作为基准,以便在优化后将其与应用进行比较,并确定是否需要进一步的干预
JVM参数是特定于Java的值,这些值会更改Java虚拟机的行为
就JVM性能而言,一般都需要初始化堆内存
# 用于指定最小和最大堆大小的参数,unit 是初始化内存的单位, 可以是g (GB), m(MB), or k(KB)
-Xms<heap size>[unit]
-Xmx<heap size>[unit]
在设置JVM内存的最小和最大堆大小时,您可能需要考虑将它们设置为相同的值。这样,您就不必调整堆大小,从而节省了宝贵的CPU周期。如果您正在使用较大的堆,则可能还需要预触摸所有页面方法是将-XX:+AlwaysPreTouch
设置为启动项
从Java 8开始,Metaspace已取代了旧的PermGen内存空间。不再有java.lang.OutOfMemoryError:PermGen
错误,现在我们可以开始监视应用程序日志中的java.lang.OutOfMemoryError: Metadata space
此内存区域中的大量垃圾回收工作可能表明类或类加载器中的内存泄漏
默认情况下,元数据分配受可用本机内存量的限制,但JVM公开的以下属性使我们可以控制元空间:
# 设置可分配给元空间的最大本机内存量, 默认情况下为无限制
-XX:MaxMetaspaceSize
# 垃圾搜集器内部是根据变量_capacity_until_GC来判断metaspace区域是否达到阈值的
# 该参数用于设置首次使用不够而触发FGC的阈值, GC收集器会对metaspace的回收, 同时计算新的_capacity_until_GC值
# 以后发生FGC就跟MetaspaceSize没有关系了
-XX:MetaspaceSize
# 垃圾回收后需要可用的元空间内存区域的最小百分比。 如果剩余的内存量低于阈值,则将调整元空间区域的大小
-XX:MinMetaspaceFreeRatio
# 垃圾回收后需要可用的元空间内存区域的最大百分比。 如果剩余的内存量大于阈值,则将调整元空间区域的大小
-XX:MaxMetaspaceFreeRatio
我们将在JDK中使用默认的parallel
垃圾收集器(或throughput
收集器), 可以通过XX:+UseParallelGC
开启。 这个标志启用了新生代和老年代收集器的并行版本。也可以通过XX:+UseSerialGC
来进行串行垃圾收集器。串行收集器是单线程收集器,而并行收集器是多线程收集器
通过使用-XX:+PrintCommandLineFlags -version
检查Java版本的默认值
深入研究,重要的是要注意内存分配参数
# 设置新生代空间的初始大小
-XX:NewSize
# 设置新生代空间的最大大小
-XXMaxNewSize
# 指定整个年轻代空间的大小,即eden和两个survivor空间
-Xmn
您将使用以下参数来计算有关老年代的空间大小:根据新生代空间的大小自动设置老年代的大小
# 初始的老年代空间等于
(-Xmx) - (-XX:NewSize)
# 老年代的最小大小为
(-Xmx) - (-XXMaxNewSize)
OutOfMemoryError
可能是每个开发者的噩梦。 可能面临着难以复制和诊断的应用崩溃。 不幸的是,在大型应用程序中这种情况很常见。 幸运的是,JVM具有将堆内存写入文件的参数,您可以使用该参数进行故障排除
# 在抛出java.lang.OutOfMemoryError时命令JVM将堆转储到物理文件中
-XX:+HeapDumpOnOutOfMemoryError
# 指定目录或文件名的路径
-XX:HeapDumpPath=./java_pid<pid>.hprof
# 第一次发生OutOfMemoryError时,用于运行紧急用户定义的命令
-XX:OnOutOfMemoryError=";"
# 用于限制在抛出OutOfMemoryError之前,VM在GC中花费的时间比例
-XX:+UseGCOverheadLimit
JVM具有四个垃圾收集器实现:
-XX:+UseSerialGC # 串行垃圾收集器
-XX:+UseParallelGC # 并行垃圾收集器
-XX:+UseConcMarkSweepGC # CMS垃圾收集器
-XX:+UseG1GC # G1垃圾收集器
垃圾收集性能与JVM和应用程序性能密切相关。 当垃圾收集器无法清除内存时,它会越来越多地工作,最终导致stop-the-world
事件, 甚至出现内存不足的情况。 我们希望尽可能避免这种情况。 为了做到这一点,我们需要能够观察JVM垃圾收集器在做什么。 监视GC性能的最佳方法之一是查看GC日志。 您可以使用以下命令记录GC活动:
-XX:+UseGCLogFileRotation # 指定日志文件轮换策略
-XX:NumberOfGCLogFiles=<number of log files> # 说明轮换日志时要使用的最大日志文件数
-XX:GCLogFileSize=<file size>[unit] # 指日志文件的最大大小
-Xloggc:/path/to/gc.log # 指定文件路径
对于GC日志记录,还有其他重要的JVM参数。 例如:
-XX:+PrintGC # 输出GC日志
-XX:+PrintGCDetails # 输出GC的详细日志
-XX:+PrintGCTimeStamps # 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps # 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC # 在进行GC的前后打印出堆的信息
-XX:+PrintTenuringDistribution # 在日志中添加有关对象年龄信息
-XX:+PrintGCApplicationStoppedTime # 包含有关应用程序在安全点停止的时间的信息,通常是由于stop the world垃圾收集导致的
示例: 使用以下命令行打开完整GC日志
-XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:<filename>
但是,如果没有可用的完整GC日志,则可以使用监视工具来调用它们,或者使用以下命令来启用它们:
jmap -histo:live pid
无论哪种方式,您都会获得类似于以下内容的信息:我们可以看到由JVM执行的一些操作, 单个垃圾收集器日志行可以使我们深入了解发生了什么,释放了多少内存以及整个操作花费了多长时间
0.134: [GC (Allocation Failure) [PSYoungGen: 65536K->10720K(76288K)] 65536K->40488K(251392K), 0.0190287 secs] [Times: user=0.13 sys=0.04, real=0.02 secs]
0.193: [GC (Allocation Failure) [PSYoungGen: 71912K->10752K(141824K)] 101680K->101012K(316928K), 0.0357512 secs] [Times: user=0.27 sys=0.06, real=0.04 secs]
1.235: [Full GC (System.gc()) [PSYoungGen: 10752K->0K(190464K)] [ParOldGen: 358209K->368152K(459264K)] 368961K->368152K(649728K), [Metaspace: 2652K->2652K(1056768K)], 1.1751101 secs] [Times: user=10.64 sys=0.05, real=1.18 secs]
2.612: [Full GC (Ergonomics) [PSYoungGen: 179712K->0K(190464K)] [ParOldGen: 368152K->166769K(477184K)] 547864K->166769K(667648K), [Metaspace: 2659K->2659K(1056768K)], 0.2662589 secs] [Times: user=2.14 sys=0.00, real=0.27 secs]
让我们看一下其中的一行,该行描述了使用System.gc()
方法从测试代码中有意执行的Full GC事件.
1.235: [Full GC (System.gc()) [PSYoungGen: 10752K->0K(190464K)] [ParOldGen: 358209K->368152K(459264K)] 368961K->368152K(649728K), [Metaspace: 2652K->2652K(1056768K)], 1.1751101 secs] [Times: user=10.64 sys=0.05, real=1.18 secs]
**分析:**除了事件类型之外,我们还看到了新生代中发生的事情,老年代中发生的事情,元空间区域中,,最后是
[PSYoungGen:10752K-> 0K(190464K)]
,空间从10752K减少到0K,分配的新生代空间总数为190464K[ParOldGen:358209K-> 368152K(459264K)]
,其内存使用量为358209K,而回收完成时为368152K。分配给老年代的总内存为459264K。这意味着这个GC周期最终并没有释放太多的老年代空间[Metaspace:2652K-> 2652K (1056768K)]
,以相同的内存使用量2652K开始和结束,整个区域占用1056768K368961K-> 368152K (649728K)
,在完成整个垃圾收集之后,我们从最初的368961K开始获得了368152K的内存,并且占用的整个内存空间为649728K[Times: user=10.64 sys=0.05, real=1.18 secs]
,JVM完成整个垃圾收集操作所需的时间为1.18秒。 user = 10.64
告诉我们在操作系统内核之外的用户模式代码中花费的CPU时间。 sys = 0.05
部分是进程本身在内核内部花费的CPU时间,这意味着CPU花在执行与系统相关的调用上的时间设定您的JVM性能目标,开始调整JVM的性能之前,首先必须设置性能目标
您不能一次专注于所有三个目标,因为任何三个目标的任何性能提升都会导致另一个或两个目标的性能下降
在考虑业务需求时,您必须决定哪两个与您的应用程序最相关。 无论哪种方式,JVM调整的目标都是优化垃圾收集器,以使您拥有高吞吐量,更少的内存消耗和低延迟。 但是,较少的内存/低延迟并不意味着内存或延迟越少或越低,性能就越好。 这取决于您选择关注的指标
执行性能调整时,请牢记以下原则,因为它们使垃圾收集更加容易
首先,您需要记住的是Java VM调整不能解决所有性能问题。因此,应仅在必要时进行。也就是说,调整是一个漫长的过程,在此过程中,您很可能会根据压力测试和基准测试结果执行正在进行的配置优化和多次迭代。在达到所需指标之前,您可能还需要多次调整参数,从而重新运行测试
通常,调优应该首先满足内存使用要求,延迟,最后满足吞吐量
要确定内存使用情况,首先需要知道活动数据的大小。
活动数据的大小是自应用程序进入稳定状态以来活动数据占用的Java堆的数量。必须以稳定状态而不是启动阶段来测量活动数据。
在启动阶段,JVM加载并启动应用程序的主要模块和数据; 因此,JVM参数还不稳定。
稳定阶段意味着应用程序已经运行了一段时间并进行了压力测试。 更具体地说,当应用程序达到在生产环境中满足业务高峰期要求的工作负载并在达到高峰后保持稳定时,它就处于稳定阶段。 只有这样,每个JVM性能参数才会处于稳定状态
确保使用默认的JVM参数执行测试,因为它可以让您查看稳定阶段应用程序需要多少内存。一旦应用以稳定状态运行,您就必须根据平均老年代和永久代占用率来估算内存占用量,在稳定状态期间查看Full GC日志,您也可以使用最长的Full GC进行估算。
GC日志是收集有意义且丰富的数据以帮助进行调整的最佳方法之一。 启用GC日志不会影响性能。 因此,即使在生产环境中也可以使用它们来检测问题。可通过上文GC LOG 分析确定内存占用量
一旦确定了内存占用量,下一步就是延迟调整。 在此阶段,堆内存大小和延迟不满足应用程序要求。 因此,需要根据应用程序的实际需求进行新的调试。 您可能必须再次调整堆大小,确定GC的持续时间和频率,并确定是否需要切换到另一个垃圾收集器
确定系统延迟要求
我们提到了性能目标,但我们没有为它们设定值。 这些目标是调整后需要满足的系统延迟要求。 有助于实现目标的指标包括
在JVM性能调整的最后一步,我们对到目前为止得到的结果进行吞吐量测试,然后根据需要进行一些调整。根据测试和整体应用程序要求,应用程序应具有设置的吞吐量指标。 当达到或超过此目标时,您可以停止调整。
但是,如果经过优化后,您仍然无法达到吞吐量目标,则需要重新实现它,并评估吞吐量需求与当前的吞吐量之间的差距。 如果差距大约为20%,则可以更改参数,增加内存并再次调试应用程序。 但是,如果差距大于20%,则需要将吞吐量目标作为吞吐量目标进行审查,并且设计可能无法满足整个Java应用程序的要求
对于垃圾回收,吞吐量调整有两个目的: 这些会导致低吞吐量
stop-the-world
事件