【JAVA】JVM 调优

【JAVA】JVM 调优

翻译文章:JVM Tuning: How to Prepare Your Environment for Performance Tuning

当涉及到Java应用程序时,要确保它们以最高性能运行,至关重要的是缩小代码与运行它的虚拟机之间的资源差距(如果有的话)。做到这一点的方法是深入并微调Java虚拟机

一、介绍

1. 什么是JVM调优?

  • 成本 –考虑到您的环境,您可以通过添加硬件而不是花费时间来调整JVM来获得更多收益
  • 所需的结果 –从长远来看,调整稳定性比提高性能更有效
  • 持续存在的问题 –在开始调整之前,您应该对系统进行彻底检查,以查看是否存在潜在的问题,因为调整可能会暂时延迟或隐藏它。如果不监视JVM,则无法进行JVM调优或调试。找出要监视的关键JVM性能指标是什么,以及哪些是当今可用的最佳监视工具
  • 内存泄漏 –无论如何调整,它们始终会导致垃圾回收问题

2. 应用优化考虑了所有性能层

尽管很关键,但是调整JVM不足以确保最佳性能。例如,如果应用程序的架构设计不佳或代码编写不佳,那么仅通过调整JVM就无法期望性能飞速增长

做得很好的调优将研究整个系统以及可能影响性能的所有层,包括数据库和操作系统。就是说,当您处于执行JVM调整的阶段时,请假定项目的体系结构和代码是最佳的或已调整。但是,在深入研究它之前,您必须设置性能优化目标并确定当前的性能问题。这些目标将作为基准,以便在优化后将其与应用进行比较,并确定是否需要进一步的干预

二、背景知识

1. JVM主要参数

JVM参数是特定于Java的值,这些值会更改Java虚拟机的行为

a. 堆内存

就JVM性能而言,一般都需要初始化堆内存

# 用于指定最小和最大堆大小的参数,unit 是初始化内存的单位, 可以是g (GB), m(MB), or k(KB)
-Xms<heap size>[unit]
-Xmx<heap size>[unit]

在设置JVM内存的最小和最大堆大小时,您可能需要考虑将它们设置为相同的值。这样,您就不必调整堆大小,从而节省了宝贵的CPU周期。如果您正在使用较大的堆,则可能还需要预触摸所有页面方法是将-XX:+AlwaysPreTouch 设置为启动项

b. 元空间

从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

2. 垃圾收集器

我们将在JDK中使用默认的parallel垃圾收集器(或throughput收集器), 可以通过XX:+UseParallelGC开启。 这个标志启用了新生代和老年代收集器的并行版本。也可以通过XX:+UseSerialGC 来进行串行垃圾收集器。串行收集器是单线程收集器,而并行收集器是多线程收集器

通过使用-XX:+PrintCommandLineFlags -version 检查Java版本的默认值

三、内存分配

深入研究,重要的是要注意内存分配参数

# 设置新生代空间的初始大小
-XX:NewSize
# 设置新生代空间的最大大小
-XXMaxNewSize
# 指定整个年轻代空间的大小,即eden和两个survivor空间
-Xmn

您将使用以下参数来计算有关老年代的空间大小:根据新生代空间的大小自动设置老年代的大小

# 初始的老年代空间等于
(-Xmx) - (-XX:NewSize)
# 老年代的最小大小为
(-Xmx) - (-XXMaxNewSize)

1. 内存不足

OutOfMemoryError可能是每个开发者的噩梦。 可能面临着难以复制和诊断的应用崩溃。 不幸的是,在大型应用程序中这种情况很常见。 幸运的是,JVM具有将堆内存写入文件的参数,您可以使用该参数进行故障排除

# 在抛出java.lang.OutOfMemoryError时命令JVM将堆转储到物理文件中
-XX:+HeapDumpOnOutOfMemoryError
# 指定目录或文件名的路径
-XX:HeapDumpPath=./java_pid<pid>.hprof
# 第一次发生OutOfMemoryError时,用于运行紧急用户定义的命令
-XX:OnOutOfMemoryError="<cmd args>;<cmd args>"
# 用于限制在抛出OutOfMemoryError之前,VM在GC中花费的时间比例
-XX:+UseGCOverheadLimit

四、垃圾收集

JVM具有四个垃圾收集器实现:

-XX:+UseSerialGC			# 串行垃圾收集器
-XX:+UseParallelGC			# 并行垃圾收集器
-XX:+UseConcMarkSweepGC		# CMS垃圾收集器
-XX:+UseG1GC				# G1垃圾收集器

1.GC日志分析

垃圾收集性能与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]

**分析:**除了事件类型之外,我们还看到了新生代中发生的事情,老年代中发生的事情,元空间区域中,,最后是

  • 新生代垃圾收集器: 在新生代GC之后,[PSYoungGen:10752K-> 0K(190464K)],空间从10752K减少到0K,分配的新生代空间总数为190464K
  • 老年代垃圾收集器:[ParOldGen:358209K-> 368152K(459264K)],其内存使用量为358209K,而回收完成时为368152K。分配给老年代的总内存为459264K。这意味着这个GC周期最终并没有释放太多的老年代空间
  • 元空间区域: [Metaspace:2652K-> 2652K (1056768K)],以相同的内存使用量2652K开始和结束,整个区域占用1056768K
  • 总内存差异:368961K-> 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的性能之前,首先必须设置性能目标

  • 延迟: 运行垃圾回收事件所需的时间
  • 吞吐量:VM花在执行应用程序上的时间与花在执行垃圾收集上的时间的百分比
  • 占用空间:是垃圾收集器正常运行所需的内存量

您不能一次专注于所有三个目标,因为任何三个目标的任何性能提升都会导致另一个或两个目标的性能下降

  • 高吞吐量和低延迟导致更高的内存使用率
  • 高吞吐量和低内存使用量会导致更高的延迟
  • 低延迟和低内存使用率会导致较低的吞吐量

在考虑业务需求时,您必须决定哪两个与您的应用程序最相关。 无论哪种方式,JVM调整的目标都是优化垃圾收集器,以使您拥有高吞吐量,更少的内存消耗和低延迟。 但是,较少的内存/低延迟并不意味着内存或延迟越少或越低,性能就越好。 这取决于您选择关注的指标

1. 调优原则

执行性能调整时,请牢记以下原则,因为它们使垃圾收集更加容易

  • Minor GC collection: 意味着MinorGC应该收集尽可能多的死对象以减少Full GC的频率
  • GC内存最大化: 它表示GC在一个周期内可以访问的内存越多,清理效率越高,收集频率越低
  • 2/3: 您需要从三个绩效目标中选择两个

首先,您需要记住的是Java VM调整不能解决所有性能问题。因此,应仅在必要时进行。也就是说,调整是一个漫长的过程,在此过程中,您很可能会根据压力测试和基准测试结果执行正在进行的配置优化和多次迭代。在达到所需指标之前,您可能还需要多次调整参数,从而重新运行测试

通常,调优应该首先满足内存使用要求延迟,最后满足吞吐量

2. 衡量内存占用量

要确定内存使用情况,首先需要知道活动数据的大小。

活动数据的大小是自应用程序进入稳定状态以来活动数据占用的Java堆的数量。必须以稳定状态而不是启动阶段来测量活动数据。

  • 在启动阶段,JVM加载并启动应用程序的主要模块和数据; 因此,JVM参数还不稳定。

  • 稳定阶段意味着应用程序已经运行了一段时间并进行了压力测试。 更具体地说,当应用程序达到在生产环境中满足业务高峰期要求的工作负载并在达到高峰后保持稳定时,它就处于稳定阶段。 只有这样,每个JVM性能参数才会处于稳定状态

如何确定内存占用量

确保使用默认的JVM参数执行测试,因为它可以让您查看稳定阶段应用程序需要多少内存。一旦应用以稳定状态运行,您就必须根据平均老年代和永久代占用率来估算内存占用量,在稳定状态期间查看Full GC日志,您也可以使用最长的Full GC进行估算。

GC日志是收集有意义且丰富的数据以帮助进行调整的最佳方法之一。 启用GC日志不会影响性能。 因此,即使在生产环境中也可以使用它们来检测问题。可通过上文GC LOG 分析确定内存占用量

2. 调整延迟

一旦确定了内存占用量,下一步就是延迟调整。 在此阶段,堆内存大小和延迟不满足应用程序要求。 因此,需要根据应用程序的实际需求进行新的调试。 您可能必须再次调整堆大小,确定GC的持续时间和频率,并确定是否需要切换到另一个垃圾收集器

确定系统延迟要求

我们提到了性能目标,但我们没有为它们设定值。 这些目标是调整后需要满足的系统延迟要求。 有助于实现目标的指标包括

  • 可接受的Minor GC的平均频率,您可以将其与Minor GC的数量进行比较
  • 可接受的最大Full GC暂停时间,您可以将其与最长的Full GC周期进行比较
  • 最高Full GC暂停的可接受频率,您将其与Full GC的最高频率进行比较
  • 可接受的Minor GC平均暂停时间,您可以将其与Minor GC持续时间进行比较

3. 吞吐量调整

在JVM性能调整的最后一步,我们对到目前为止得到的结果进行吞吐量测试,然后根据需要进行一些调整。根据测试和整体应用程序要求,应用程序应具有设置的吞吐量指标。 当达到或超过此目标时,您可以停止调整。

但是,如果经过优化后,您仍然无法达到吞吐量目标,则需要重新实现它,并评估吞吐量需求与当前的吞吐量之间的差距。 如果差距大约为20%,则可以更改参数,增加内存并再次调试应用程序。 但是,如果差距大于20%,则需要将吞吐量目标作为吞吐量目标进行审查,并且设计可能无法满足整个Java应用程序的要求

对于垃圾回收,吞吐量调整有两个目的: 这些会导致低吞吐量

  • 最小化传递到老年代的对象数量
  • 减少FULL GC执行时间或stop-the-world事件
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值