[译文]构建高性能Java应用程序——7个JVM调参手段

英文版权归GCeasy团队所有,中译文作者yangyang(aka davidkoree)。双语版可用于非商业传播,但须注明英文版作者、版权信息,以及中译文作者。翻译水平有限,请广大读者指正。

 

在本文撰写时(2020年3月),与JVM垃圾回收和内存调优相关的参数有600多个,如果再包括其他方面,JVM的参数总量可轻易超过1000 :) 。对常人来说这实在难以消化理解。因此,在本文中,我们挑选了7个重要且有用的JVM参数(组合)供你参考。

 

1. -Xmx and -XX:MaxMetaspaceSize

 

-Xmx可能是JVM最重要的配置参数,它定义了应用程序可分配的内存堆尺寸(heap size)的最大值(该短视频讲解了JVM中不同的内存区及其概念[注1])。举个例子,你可以这样定义你开发的应用程序的堆尺寸:

 

-Xmx2g

 

堆尺寸是一个重要角色,它影响着:

 

a. 应用程序的性能

b. 你在云计算服务上(如AWS, Azure等)的开支

 

同时它也带出一个问题:对于我的应用程序来说,配置多大的堆才合适,大一些还是小一些?答案是:“得看具体情况”。在另一篇文章里([注2]),我们对此分享了一些看法。

 

你也可以参考这篇文章([注3]):advantages of setting -Xms and -Xmx to same value

 

JVM将元数据(metadata),如类定义和方法定义,都存储在元数据空间(metaspace)里。默认情况下,这块内存存储区域是无限大的(只取决于你的容器/机器内存大小)。你需要使用-XX:MaxMetaspaceSize参数来设置一个存储元数据信息的内存上限值。举例如下:

 

-XX:MaxMetaspaceSize=256m

 

2. 垃圾回收算法

 

截至目前(2020年3月),OpenJDK提供了7种不同的垃圾回收算法:

 

a. Serial GC

b. Parallel GC

c. Concurrent Mark & Sweep GC

d. G1 GC

e. Shenandoah GC

f. Z GC

g. Epsilon GC

 

如果你不明确指定其中的某个算法,JVM会使用默认算法。Java 8(含)之前的版本,Parallel GC是默认值。从Java 9开始,G1 GC是默认值。

 

垃圾回收算法的选择,会对应用程序的性能产生重要影响。基于我们的研究和观测,Z GC算法有绝佳的优势。如果你运行的JVM 是11(含)以上版本,那么你可以考虑启用Z GC算法(例如:-XX:+UseZGC)。关于该算法的详细介绍可参考这篇文章([注4])。

 

下表列出了每个垃圾回收算法的配置方式:

 

GC Algorithm

JVM argument

Serial GC

-XX:+UseSerialGC

Parallel GC

-XX:+UseParallelGC

Concurrent Market & Sweep (CMS) GC

-XX:+UseConcMarkSweepGC

G1 GC

-XX:+UseG1GC

Shenandoah GC

-XX:+UseShenandoahGC

Z GC

-XX:+UseZGC

Epsilon GC

-XX:+UseEpsilonGC

 

3. 启用垃圾回收日志

 

垃圾回收日志记录了垃圾回收事件、内存召回、执行频率等信息。想要启用日志,则需要如下JVM参数:

 

JDK 1 ~ 8版本:

 

-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:{file-path}

 

JDK 9(含)以上版本:

 

-Xlog:gc*:file={file-path}

 

实例:

 

-Xlog:gc*:file={file-path}

 

-Xlog:gc*:file=/opt/workspace/myAppgc.log

 

通常情况下,垃圾回收日志用来调教垃圾回收的性能。不过日志里也包含了极为重要且细微的衡量指标,它们往往能预示出应用程序可用性及性能上的一些特征。在这里我们想重点提出一个衡量指标——垃圾回收吞吐量(GC Throughput)(关于其他衡量指标的介绍,请参考这篇文章[注5])。

 

垃圾回收吞吐量是指,你的应用程序在处理业务逻辑与处理垃圾回收时,各自花费的时间百分比。比方说,应用程序当前的垃圾回收吞吐量为98%,这表示应用程序98%的时间都在处理业务逻辑,剩余2%的时间则用在垃圾回收上。

 

现在我们来看看一个正常JVM的内存堆使用情况:

图:正常JVM的内存堆使用情况

 

这是一个“完美”的锯齿图,你可以注意到,当回收整个内存堆(full GC)的时候(即红色三角标注的时间点),内存使用量会直接降到谷底。

 

现在我们再来看看异常JVM的内存堆使用情况:

图:异常JVM的内存堆使用情况

 

你可以注意到图右侧,垃圾回收在反复不停的执行,可是内存使用量却没有降下来。这是一个典型的征兆,它表明应用程序遇到了某种内存问题。

 

如果你仔细看这张图,便会注意到,从上午8点开始,重复不断地有内存堆回收操作。然而应用程序从上午8点45分开始才有OutOfMemoryError错误。8点时的「垃圾回收吞吐量」大约是99%,8点后这一指标落至60%。因为当重复产生GC动作时,应用程序无法处理任何业务逻辑——它只在不停地执行垃圾回收操作。

 

作为主动出击的手段,当你观察到「垃圾回收吞吐量」开始下降,你可以将对应的JVM从负载均衡池(load balancer pool)中移除,使其不再处理新请求,避免因其弱化的性能而扩大对业务造成的负面影响。

[注6]通过GCeasy REST API,你可以实时监控与垃圾回收相关的衡量指标。

 

4. -XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath

 

作为一个严重的问题,OutOfMemoryError错误会在SLA(译注:服务等级协议)层面影响应用程序的可用性和性能。为了诊断OutOfMemoryError错误或其他内存相关的问题,你必须在应用程序发生OutOfMemoryError错误或在错误发生之前来捕获「堆转储文件」(译注:heap dump,亦称「堆快照」)。由于我们不清楚什么时候会发生OutOfMemoryError错误,手动捕获堆转储文件是十分困难的。然而,我们可以通过添加如下的JVM参数,来自动且持续的捕获堆转储信息:

 

-XX:+HeapDumpOnOutOfMemoryError and -XX:HeapDumpPath={HEAP-DUMP-FILE-PATH}

 

你需要通过-XX:HeapDumpPath定义堆转储文件的存放路径。一旦定义了上述2个参数,当OutOfMemoryError错误发生时,堆转储文件会被自动捕获,并存放到指定的路径。实例:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/crashes/my-heap-dump.hprof

 

你可以使用类似HeapHero, EclipseMAT的工具来分析被捕获的堆转储文件。

 

这篇文章[注7]有关于OutOfMemoryError错误相关的JVM参数的详细介绍。

 

5. -Xss

 

每个应用程序都可能有成百上千的线程,每个线程有自己的栈,每个栈存储如下信息:

 

a. 当前正在执行的方法/函数

b. Java的原始数据类型(Primitive datatypes)

c. 变量

d. 对象指针

e. 返回值

 

上述信息要素都会消耗内存,如果消耗超限,则会抛出StackOverflowError错误——与之相关的介绍和解决方案可参考这篇文章[注8]。然而,你可以通过-Xss参数来调整线程栈的大小限制,例如:

 

-Xss256k

 

如果你给-Xss设置了一个超大的值,那么内存会被堵塞和浪费。假设你给一个实际仅需要256kb的线程栈设置了-Xss2mb,导致的结果是你浪费了大量内存——并不是1792kb(2mb-256kb),想知道为什么吗?

 

我们来计算一下,如果你的应用程序有500个线程,-Xss2mb意味着它们要消耗1000mb内存(500*2mb/thread)。另外一方面,-Xss256kb则仅需要消耗125mb内存(500*256kb/thread),如此比较来看,你将为每个JVM节省875mb(1000mb-125mb)的内存空间——这便是巨大的差异。

 

注意:线程是在堆(heap,-Xmx参数)之外独立创建的。因此,上述1000mb是在-Xmx值设置之后再额外添加的。这个短片[注9]可以告诉你为什么线程在堆之外创建。

 

我们的建议是先从一个较小值开始设置(例如256kb),并随着吞吐性能回归和AB测试来逐步调教-Xss参数。只在遇到StackOverflowError错误时适当增大该参数值,否则考虑保持一个可用的最小值。

 

6. -Dsun.net.client.defaultConnectTimeout and -Dsun.net.client.defaultReadTimeout

 

现代应用程序与远程应用之间可能通过各种协议(如SOAP, REST, HTTP, HTTPS, JDBC, RMI等)进行连接/通讯。有时候远程应用的响应时间可能很长,甚至无任何响应。

 

如果你没有恰当地设置超时等待时间,并且远程应用也没能快速响应,那么你的应用程序进程/资源就会卡住。这种情况下,你的应用程序的可用性势必受到影响,卡顿并且变慢,甚至「假死」。为了确保应用程序高可用,你必须恰当设置超时。

 

下面2个强大的网络超时属性作用于JVM层面,所有使用java.net.URLConnection的协议句柄都能全局生效:

 

1. sun.net.client.defaultConnectTimeout 定义了向主机发起连接的超时等待时间(单位:毫秒)。举HTTP连接为例,此值表明向HTTP服务器发起连接的超时等待时间。

 

2. sun.net.client.defaultReadTimeout 定义了连接某个资源并读取其输入流的超时等待时间(单位:毫秒)。

 

举例,你可以将上述2个属性值设为2秒:

-Dsun.net.client.defaultConnectTimeout=2000

-Dsun.net.client.defaultReadTimeout=2000

 

注意,它们的默认值都是-1,表示没有超时设置。关于这2个属性的更多介绍,可参考这篇文章[注10]。

 

7. -Duser.timeZone

 

你的应用程序可能在业务上存在对时间敏感的情况。具体的说,如果它是一个证券系统,你可能无法在早上9:30之前进行交易。为了实现类似的时间约束,你可能用到了java.util.Date, java.util.Calendar对象,这些对象默认从底层操作系统获取时区信息,如果你的应用跨时区运行,这种实现方式会带来问题。看看下面的场景:

 

a. 如果你的应用程序横跨多个数据中心运行,比如它们分布于三藩市、芝加哥、新加坡,那么自然地,分布在这些数据中心的JVM有各自的时区且彼此不同,也就会产生不同的行为,进而导致不一致的结果。

 

b. 如果你的应用程序部署在云端,在你未知晓的情况下,它们可能被部署到了不同的数据中心,因此也可能会有不同的结果。

 

c. 你的运维团队可能也会在未通知开发人员的情况下更改时区设置,这同样会导致结果与你预期不符。

 

为了防止类似上述问题发生,强烈建议你利用系统属性,即JVM的-Duser.timezone参数设定时区。比如,你想设置美国东部夏令时(EDT),可以这样写:

 

-Duser.timezone=US/Eastern

 

结论

 

在本文中,我们试图概括一些重要的JVM调优参数以及它们的影响面。希望对你有所帮助!

 

参考链接:

注1 https://www.youtube.com/watch?v=uJLOlCuOR4k&t=9s

注2 https://blog.heaphero.io/2019/06/21/large-or-small-memory-size-for-my-app/

注3 https://blog.gceasy.io/2020/03/09/advantages-of-setting-xms-and-xmx-to-same-value/

注4 https://wiki.openjdk.java.net/display/zgc/Main

注5 https://blog.gceasy.io/2019/03/13/micrometrics-to-forecast-application-performance/

注6 https://www.youtube.com/watch?v=6G0E4O5yxks

注7 https://blog.heaphero.io/2019/06/21/outofmemoryerror-related-jvm-arguments/

注8 https://blog.fastthread.io/2018/09/24/stackoverflowerror/

注9 https://www.youtube.com/watch?v=uJLOlCuOR4k&t=9s

注10 https://blog.fastthread.io/2018/09/02/threads-stuck-in-java-net-socketinputstream-socketread0/

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值