跳至主要內容

JVM调优

wangdx大约 9 分钟

参考网址

JVM 参数解析open in new window

我所使用的生产 Java 17 启动参数open in new window

参数解析

DisableExplicitGC

如果 jvm 参数中设置了-XX:+DisableExplicitGC,那么代码中手动调用 System.gc()就不会生效。而有些框架中因为是使用的堆外内存,必须手动调用 System.gc()来释放。如果禁用掉就会导致堆外内存使用一直增长,造成内存泄露。

UnlockDiagnosticVMOptions

用于解锁一些诊断性的虚拟机选项的参数。在 Java 虚拟机中,有一些高级和诊断性功能默认是被禁用的,以确保稳定性和安全性。

+UnlockExperimentalVMOptions

这些是与 HotSpot JVM 中的实验性功能相关的选项,这些功能可能尚未准备好投入生产。这些选项允许你尝试新的 HotSpot JVM 功能,并且需要通过指定以下内容来解锁它们: 例如,在 JDK 11 中使用 ZGC 垃圾收集器可以这样开启:java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC

OmitStackTraceInFastThrow

https://blog.csdn.net/m0_38000428/article/details/111877623

最近在排查线上日志的时候发现有个日志捕捉了 NullPointerException,但是没有异常堆栈信息 ,只有 java.lang.NullPointerException:null 这一条信息,无法知道是从哪里抛出来的如图:

经过分析是JIT编译器对异常进行了优化,当代码中的某个位置抛出同一个异常很多次后,JIT服务端编译器(C2)
会将其优化成抛出一个事先编译好的、类型匹配的异常,异常堆栈信息就看不到了。

MaxRAMPercentage

https://zhuanlan.zhihu.com/p/557566748

在容器环境下,Java 只能获取服务器的配置,无法感知容器内存限制。您可以通过设置 -Xmx 来限制 JVM 堆大小,但该方式存在以下问题:

当规格大小调整后,需要重新设置堆大小参数。 当参数设置不合理时,会出现应用堆大小未达到阈值但容器 OOM 被强制关闭的情况。

推荐的 JVM 参数设置

-XX:+UseContainerSupport
-XX:InitialRAMPercentage=70.0
-XX:MaxRAMPercentage=70.0
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/home/admin/nas/gc-${POD_IP}-$(date '+%s').log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/home/admin/nas/dump-${POD_IP}-$(date '+%s').hprof

AlwaysPreTouch

https://blog.csdn.net/weixin_38106322/article/details/108985436

而使用【-XX:+AlwaysPreTouch】参数能够达到的效果就是,在服务启动的时候真实的分配物理内存给 JVM,而不再是虚拟内存,效果是可以加快代码运行效率,缺点也是有的,毕竟把分配物理内存的事提前放到 JVM 进程启动时做了,自然就会影响 JVM 进程的启动时间,导致启动时间降低几个数量级。

推荐

2024-4-16 更新: 关于  -XX:GuaranteedSafepointInterval=0  可能引起 JVM internal 内存不断上升的 bug 的疑问以及解决方案:

这个不是 bug:https://bugs.openjdk.org/browse/JDK-8304316

这个不是  -XX:GuaranteedSafepointInterval=0  引起的:https://bugs.openjdk.org/browse/JDK-8305994 。而是 Java 15,16 引入的 bug,17 低版本存在这个问题,21 修复的,在 17.0.8 port 回 17 的。这个也能优化第一个问题。

但是这两个都可以通过升级你的  JDK 小版本至少为 17.0.8 以上修复(引入了  GuaranteedAsyncDeflationInterval  这个配置保证了 idle 的 monitor 一定被清理)。如果你还不能升级,可以去掉  -XX:GuaranteedSafepointInterval=0  来一定程度上(大概 90% 的概率没问题)避免不断上升,但是完全避免还是需要升级 17.0.8 以上

JVM 参数升级提示工具:jacoline.dev/inspect JVM 参数词典:chriswhocodes.com Revolut(英国支付巨头)升级 Java 17 实战:https://www.bilibili.com/video/bv1SA4y1d7sZ

目前正常微服务综合内存占用+延迟+吞吐量,还是 G1 更优秀。但是如果你的微服务本身压力没到机器极限,要求延迟低,那么 ZGC 最好。如果你是实现数据库那样的需求(大量缓存对象,即长时间生存对象,老年代很大,并且还会可能分配大于区域的对象),那么必须使用 ZGC。

使用 G1GC 启动参数:

-XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -XX:-OmitStackTraceInFastThrow -Xlog:gc*=debug:file=${LOG_PATH}/gc%t.log:utctime,level,tags:filecount=50,filesize=100M -Xlog:jit+compilation=info:file=${LOG_PATH}/jit_compile%t.log:utctime,level,tags:filecount=10,filesize=10M -Xlog:safepoint=debug:file=${LOG_PATH}/safepoint%t.log:utctime,level,tags:filecount=10,filesize=10M -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -Dnetworkaddress.cache.ttl=10 -XX:MaxRAMPercentage=45 -XX:InitialRAMPercentage=45 -XX:+AlwaysPreTouch -Xss512k -XX:MaxDirectMemorySize=1024m -XX:MaxMetaspaceSize=384m -XX:ReservedCodeCacheSize=256m -XX:+DisableExplicitGC -XX:MaxGCPauseMillis=50 -XX:-UseBiasedLocking -XX:GuaranteedSafepointInterval=0 -XX:+UseCountedLoopSafepoints -XX:+SafepointTimeout -XX:SafepointTimeoutDelay=1000 -XX:StartFlightRecording=disk=true,maxsize=4096m,maxage=3d -XX:FlightRecorderOptions=maxchunksize=128m --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/jdk.internal.access=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED

使用 ZGC 启动参数:

-XX:+UnlockDiagnosticVMOptions -XX:+UnlockExperimentalVMOptions -XX:-OmitStackTraceInFastThrow -Xlog:gc*=debug:file=${LOG_PATH}/gc%t.log:utctime,level,tags:filecount=50,filesize=100M -Xlog:jit+compilation=info:file=${LOG_PATH}/jit_compile%t.log:utctime,level,tags:filecount=10,filesize=10M -Xlog:safepoint=debug:file=${LOG_PATH}/safepoint%t.log:utctime,level,tags:filecount=10,filesize=10M -Dfile.encoding=UTF-8 -Djava.security.egd=file:/dev/./urandom -Dnetworkaddress.cache.ttl=10 -XX:MaxRAMPercentage=45 -XX:InitialRAMPercentage=45 -XX:+AlwaysPreTouch -Xss512k -XX:MaxDirectMemorySize=1024m -XX:MaxMetaspaceSize=384m -XX:ReservedCodeCacheSize=256m -XX:+DisableExplicitGC -XX:+UseZGC -XX:-UseBiasedLocking -XX:GuaranteedSafepointInterval=0 -XX:+UseCountedLoopSafepoints -XX:+SafepointTimeout -XX:SafepointTimeoutDelay=1000 -XX:StartFlightRecording=disk=true,maxsize=4096m,maxage=3d -XX:FlightRecorderOptions=maxchunksize=128m --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/jdk.internal.access=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED

其中,需要做成环境变量外部可以配置的是:

-XX:MaxRAMPercentage=45      -XX:InitialRAMPercentage=45 -Xss512k -XX:MaxDirectMemorySize=1024m      -XX:MaxMetaspaceSize=384m -XX:ReservedCodeCacheSize=256m  里面的参数

-XX:StartFlightRecording=disk=true,maxsize=4096m,maxage=3d  其中的 4096m 以及 3d

-XX:MaxGCPauseMillis=50:这个只有使用 G1GC 的需要

JVM 日志相关:

JVM 日志配置请参考:https://zhuanlan.zhihu.com/p/111886882

需要异步输出日志,防止卡死 JVM:-Xlog:async

GC 日志:-Xlog:gc*=debug:file=${LOG_PATH}/gc%t.log:utctime,level,tags:filecount=50,filesize=100M

JIT 编译日志:-Xlog:jit+compilation=info:file=${LOG_PATH}/jit_compile%t.log:utctime,level,tags:filecount=10,filesize=10M

Safepoint 日志:-Xlog:safepoint=debug:file=${LOG_PATH}/safepoint%t.log:utctime,level,tags:filecount=10,filesize=10M

关闭堆栈省略:这个只会省略 JDK 内部的异常,比如      NullPointerException 这种的:-XX:-OmitStackTraceInFastThrow,我们应用已经对于大量报错的时候输出大量堆栈导致性能压力的优化,参考:https://zhuanlan.zhihu.com/p/428375711

系统属性(环境变量)启动参数:

-Dfile.encoding=UTF-8:指定编码为 UTF-8,其实 Java 18 之后默认编码就是 UTF-8 了,这样避免不同操作系统编译带来的差异(Windows 默认是 GB2312,Linux 默认是 UTF-8),参考:https://openjdk.java.net/jeps/400

-Djava.security.egd=file:/dev/./urandom:更换 random 为 urandom 避免高并发加密证书通信的时候的生成随机数带来的阻塞(例如高并发      https 请求,高并发 mysql 连接通信),参考:https://zhuanlan.zhihu.com/p/259874076

-Dnetworkaddress.cache.ttl=10:将 DNS 缓存降低为 10s 过期,咱们 k8s 内部有很多通过域名解析的资源(通过 k8s 的 coreDNS),解析的 ip 可能会过期,漂移成新的 ip,默认的 30s 有点久,改成 10s,但是这会增加 coreDNS 的压力。

内存控制相关:

以下需要做成可以在外部配置的环境变量:

堆内存控制:-XX:MaxRAMPercentage=45 -XX:InitialRAMPercentage=45 -XX:MinHeapFreeRatio=0 -XX:MaxHeapFreeRatio=100 (使用系统可用内存百分比配置堆,堆内存一般占用系统内存的少一半,其他的给堆外内存使用,比如 GC,元空间,Tracing 等等,同时设置 MinHeapFreeRatio 为最小值,MaxHeapFreeRatio 为最大值是为了避免 GC 的时候动态伸缩堆大小)

线程栈大小控制:-Xss512k

直接内存(各种 Direct Buffer)大小控制:-XX:MaxDirectMemorySize=1024m

元空间控制:-XX:MaxMetaspaceSize=384m

JIT 即时编译后(C1 C2       编译器优化)的代码占用内存:-XX:ReservedCodeCacheSize=256m

内存初始化(针对堆内存,申请并 commit 了 InitialRAMPercentage 大小后立刻填充 0 让系统真的把内存分配给 JVM):-XX:+AlwaysPreTouch

除了以上内存,JVM 还有其他内存占用,无法通过显示的配置限制,参考:https://www.zhihu.com/question/58943470/answer/2440458704

GC 控制相关:

通用参数:

-XX:+DisableExplicitGC:关闭显示 GC(System.gc()触发的 FullGC),防止 netty 这种误检测内存泄漏显示调用

G1GC 参数:

Java 9 之后默认 GC 就是 G1GC,所以不用显示指定使用 G1GC

在 Java 14 之后 G1GC 有巨大突破,目前 Java 17 中已经不需要调非常复杂的参数了,可以只调整目标最大 STW(Stop-the-world) 时间来均衡 CPU 占用,内存占用与延迟。

-XX:MaxGCPauseMillis=50:目标最大 STW(Stop-the-world) 时间,这个越小,GC 占用 CPU 资源,占用内存资源就越多,微服务吞吐量就越小,但是延迟低。这个需要做成可配置的

ZGC 参数:

ZGC 不用调优,是自适应的

-XX:+UseZGC:使用 ZGC

安全点控制

关于安全点,可以查看这篇文章:https://zhuanlan.zhihu.com/p/161710652

-XX:-UseBiasedLocking:禁用偏向锁,偏向锁其实未来会被完全移除(参考:),目前咱们都是高并发的环境,偏向锁基本没啥用并且还有负面影响

-XX:GuaranteedSafepointInterval=0:禁用定时安全点任务,没必要,咱们不是那种热点代码经常改变,资源珍贵的场景,并且如果是 ZGC 本身就会定时进入安全点进行 GC 检查,更没必要了

-XX:+UseCountedLoopSafepoints:防止大有界循环带来的迟迟不进入安全点导致 GC STW 时间过长

-XX:+SafepointTimeout      -XX:SafepointTimeoutDelay=1000:防止其他情况下导致进入安全点时间过长导致 STW 时间过长,这里配置的是 1s。但是没有指定 AbortVMOnSafepointTimeout 为 true,所以对你的 jmap 以及 jstack 命令没有影响

JFR 配置

JFR 使用请参考:https://zhuanlan.zhihu.com/p/161710652

-XX:StartFlightRecording=disk=true,maxsize=4096m,maxage=3d -XX:FlightRecorderOptions=maxchunksize=128m

模块化限制

--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED --add-opens java.base/java.nio=ALL-UNNAMED --add-opens java.base/java.security=ALL-UNNAMED --add-opens java.base/java.text=ALL-UNNAMED --add-opens java.base/java.time=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/jdk.internal.access=ALL-UNNAMED --add-opens java.base/jdk.internal.misc=ALL-UNNAMED

Java 16 将  --illegal-access  的默认值从 permit 改成了 deny (JEP: https://openjdk.java.net/jeps/396),Java 17 直接移除了这个选项 (JEP: https://openjdk.java.net/jeps/403),所以现在要打破模块化封装,必须通过这个命令具体打破某些模块向某些模块的暴露。这里包含了一些常用的可能会被反射访问的 java.base 下的 package,向所有未命名模块暴露(我们自己的项目一般不会指定模块名,如果你指定了就换成具体你的模块名)

这个也能从下面的报错中看出:

Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang" to unnamed module @7586beff

现在启动参数配置有点复杂,没法指定某个模块下的所有包都向某个模块暴露,并且未来也没有这个打算,参考:https://jigsaw-dev.openjdk.java.narkive.com/Zd1RvaeX/add-opens-for-entire-module

微信搜索“干货满满张哈希”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种 offer:

我会经常发一些很好的各种框架的官方社区的新闻视频资料并加上个人翻译字幕到如下地址(也包括上面的公众号),欢迎关注:

知乎:https://www.zhihu.com/people/zhxhash

B 站:https://space.bilibili.com/31359187 作者:Hashcon https://www.bilibili.com/read/cv17105485/ 出处:bilibili

上次编辑于: