1 JVM有哪些参数可以调?
-
GC垃圾回收器是JVM中最标志性的⼀个功能特性,⽽GC的性能极⼤程度决定了整个JAVA程序执⾏的性能。因此,对整个JVM调优或许难度太⼤,但是对GC进⾏调优,是每个JAVA程序员都应该掌握的技能;
-
关于 JVM 的参数,JVM 提供了三类参数;
- 第⼀类是标准参数:
- 以
-
开头,所有 HotSpot 都⽀持。例如java -version
。这类参数可以使⽤java -help
或者java -?
全部打印出来; - 以下是⼀些常⽤的标准参数:
--list_modules
:查看当前JAVA进程中的模块;--show-module-resolution
:查看当前JAVA进程中各个模块的依赖关系;-verbose:class
:显示类加载的信息;-verbose:gc
:显示GC事件;
- 以
- 第⼆类是⾮标准参数:
- 以
-X
开头,是特定 HotSpot 版本⽀持的指令。例如java -Xms200M -Xmx200M
,这类指令可以⽤java -X
全部打印出来; - 这⼀类参数⼀般都还是⽐较稳定,除⾮有重⼤的版本升级,⼀般不会有太⼤的变化;
- 例如:
-Xint
表示当前 Java 进程采⽤解释执⾏;-Xcomp
表示当前 Java 进程采⽤编译执⾏;-Xmixed
则表示采⽤两种执⾏引擎混合的⽅式执⾏;-Xbatch
禁⽤后台编译;- 默认情况下,JVM 将该⽅法作为后台任务进⾏编译,在解释器模式下运⾏该⽅法,直到后台编译完成;
-Xbatch
标志禁⽤后台编译,以便所有⽅法的编译都作为前台任务进⾏,直到完成。此选项等效于-XX:-BackgroundCompilation
;
- 以
- 最后⼀类是不稳定参数:
- 这也是 JVM调优的噩梦,这些参数的数量非常的多;
- 以
-XX
开头,这些参数是跟特定 HotSpot 版本对应的,很有可能换个版本就没有了; - JDK中的以下⼏个指令可以帮助开发者了解这⼀类不稳定参数:
java -XX:+PrintFlagsFinal
:所有最终⽣效的不稳定指令;java -XX:+PrintFlagsInitial
:默认的不稳定指令;java -XX:+PrintCommandLineFlags
:当前命令的不稳定指令;
- 第⼀类是标准参数:
-
接下来,我们可以通过在 java 指令后加⼊相关的参数进⾏定制;
-
例如对于数字型的参数,可以直接在 java 指令后指定一个数字;
-
⽽对于boolean型的参数,可以通过在参数前⾯添加⼀个加号表示设置为true,添加⼀个减号则表示设置为false;
-
例如:
java -XX:ActiveProcessorCount=1 -XX:+AggressiveHeap -XX:+PrintFlagsFinal -version
-
2 从RocketMQ学习常用GC调优三部曲
-
在真实项⽬当中,要如何合理的定制GC相关的运⾏参数呢?
-
关于这个问题,其实没有正确答案。要是有绝对正确的答案,那么JDK早就将这些参数的默认值调整为这个正确答案了,我们也就不需要学习了。 ⽽我们想要学习合理定制GC的参数,唯⼀的⽅法也就是多练,多尝试;
-
但是,绝⼤部分朋友都会遇到的⼀个共同的困惑。真实项⽬的线上服务器管控是⾮常严格的,⼤概率我们是接触不到的。接触不到服务器意味着我们没有服务优化的经验,⽽没有经验,就更不会让我们去接触服务器了。那怎么打破这个死局呢?建议是跟优秀的开源软件学习。因为这是所有⼈都能够接触到的,质量最靠谱的 java 程序了;
-
例如,在RocketMQ中,有⼀个核⼼服务NameServer,这也是⼀个Java编写的应⽤程序。它的运⾏脚本是这样的:
-
从这个脚本中可以看到,RocketMQ在运⾏这个java程序时,定制了⾮常多的参数。⽽这其中有很多参数,我们应该是已经有过接触的了;
-
这段脚本当中,我们唯⼀感到陌⽣的,应该就是其中那个
JAVA_MAJOR_VERSION
指令了,其实那个指令就是打印出当前环境的JDK版本,等价于下面的shell命令:JAVA -version 2>&1 | awk -F '"' '/version/ {print $2}' | awk -F '.' '{print $1}' # 执行结果: 17
-
这样就能看到,在上图的
choose_gc_options
函数中,其实就是根据JDK版本不同,定制不同的GC参数。⽽这,其实就是我们需要重点学习的,如何在真实环境当中选择合适的参数组合了;
-
-
接下来,我们也就以RocketMQ的这⼀个脚本作为示例,来整体回顾⼀下如何定制⼀个JAVA程序合理的运⾏参数;
-
我们先来重点关注
choose_gc_options
函数中的调优参数; -
对于9以前的版本,RocketMQ选择的GC参数是这样的:
JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" JAVA_OPT="${JAVA_OPT} -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSInitiatingOccupancyFraction=70 -XX:+CMSParallelRemarkEnabled -XX:SoftRefLRUPolicyMSPerMB=0 -XX:+CMSClassUnloadingEnabled -XX:SurvivorRatio=8 -XX:-UseParNewGC" JAVA_OPT="${JAVA_OPT} -verbose:gc -Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps" JAVA_OPT="${JAVA_OPT} -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=30m"
- 基础内存设置
-server
:使用服务器模式运行JVM,适合长时间运行的服务端应用-Xms4g
:初始堆内存大小为4GB-Xmx4g
:最大堆内存大小为4GB(与Xms相同,避免堆动态调整)-Xmn2g
:年轻代(Young Generation)大小为2GB-XX:MetaspaceSize=128m
:元空间初始大小128MB-XX:MaxMetaspaceSize=320m
:元空间最大大小320MB
- 垃圾回收器设置(CMS收集器)
-XX:+UseConcMarkSweepGC
:使用CMS(并发标记清除)垃圾收集器-XX:+UseCMSCompactAtFullCollection
:在Full GC时进行内存压缩-XX:CMSInitiatingOccupancyFraction=70
:老年代使用率达到70%时开始CMS收集-XX:+CMSParallelRemarkEnabled
:启用并行标记-XX:SoftRefLRUPolicyMSPerMB=0
:软引用在每MB堆中的存活时间(0表示立即回收)-XX:+CMSClassUnloadingEnabled
:允许CMS回收永久代/PermGen中的类元数据-XX:SurvivorRatio=8
:Eden区与Survivor区的大小比例为8:1:1-XX:-UseParNewGC
:禁用ParNew收集器(与CMS搭配的年轻代收集器)
- GC日志设置
-verbose:gc
:启用GC日志-Xloggc:${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log
:GC日志文件路径(%p
是进程ID,%t
是时间戳)-XX:+PrintGCDetails
:打印详细的GC信息-XX:+PrintGCDateStamps
:在GC日志中添加时间戳
- GC日志轮转
-XX:+UseGCLogFileRotation
:启用GC日志轮转-XX:NumberOfGCLogFiles=5
:保留5个GC日志文件-XX:GCLogFileSize=30m
:每个GC日志文件最大30MB
- 基础内存设置
-
⽽对于9以后的版本,RocketMQ选择的GC参数是这样的:
JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m" JAVA_OPT="${JAVA_OPT} -XX:+UseG1GC -XX:G1HeapRegionSize=16m -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30 -XX:SoftRefLRUPolicyMSPerMB=0" JAVA_OPT="${JAVA_OPT} -Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M"
- 基础内存设置
-server
:使用JVM的服务器模式(适用于长时间运行的服务端应用)-Xms4g -Xmx4g
:堆内存初始和最大值均为4GB(避免动态调整堆大小)-XX:MetaspaceSize=128m
:元空间(Metaspace,替代永久代)初始大小128MB-XX:MaxMetaspaceSize=320m
:元空间最大320MB(防止无限增长)
- G1 垃圾回收器配置
-XX:+UseG1GC
:启用 G1垃圾收集器(JDK 9+ 默认,适用于大堆内存)-XX:G1HeapRegionSize=16m
:每个G1 Region的大小为16MB(默认根据堆大小自动计算,手动指定可优化大对象分配)-XX:G1ReservePercent=25
:保留25%的堆内存作为"to-space"(避免内存耗尽导致Full GC)-XX:InitiatingHeapOccupancyPercent=30
:当堆占用率达到30%时,启动并发标记周期(CMS默认70%,G1可更激进)-XX:SoftRefLRUPolicyMSPerMB=0
:软引用(Soft Reference)立即回收(避免缓存占用过多内存)
- GC日志配置
-Xlog:gc*
:记录所有GC相关日志(替代旧的-XX:+PrintGCDetails
)file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log
:日志文件路径(%p
=进程ID,%t
=时间戳)time,tags
:在日志中输出时间戳和GC阶段标签filecount=5,filesize=30M
:日志轮转,保留5个文件,每个最大30MB(替代旧的-XX:+UseGCLogFileRotation
)
- 基础内存设置
-
-
我们可以发现,上面的参数都有一些共同点。其实不管在哪个JDK版本下,RocketMQ的这个脚本中,定制GC参数⼤概都分为这三个步骤:
- 调整内存布局;
- 选择具体的GC算法,并定制GC算法的部分参数;
- 打印GC⽇志;
-
其实,如果对RocketMQ有⼀定的了解,那么会发现,在RocketMQ中,对另外⼀个关键服务Broker,其实也是按照这样的思路来定制参数的;
-
对于NameServer和Broker这两种服务,他们的业务场景是不⼀样的,但是RocketMQ都是按照相同的思路进⾏参数调优的。⽽这种思路,其实也是我们学来⽤到我们⾃⼰项⽬当中的。当然,我们要学习的是这种参数调优的思路,⽽不是复制粘贴;
-
接下来,我们就按照这个思路进⾏详细的梳理。
3 基于JDK17优化JVM内存布局
-
所有的⽅法以及GC活动,都发生在JVM内存中,所以第⼀步就是需要定制JVM整体的内存布局。在前面的章节中⽤Arthas看过,在JVM运⾏时,内存整体会分为堆区和⾮堆区;
- 其中堆区Heap是JVM所管理的内存中最⼤的⼀块,主要⽤于存放各种对象实例。由于 Java 实现了GC垃圾⾃动回收,所以对于堆内存的管理⽅式,会根据GC不同,⽽有很⼤的差距;
- 另外⼀块⾮堆区则是 JVM 中相对不是很繁忙的⼀块内存。这⼀块内存进⾏GC垃圾回收的频率相对较低,因此对这块内存的管理⽅式相⽐堆区更加固定;
-
⽽我们⾸先要做的,就是定制JVM的堆区和⾮堆区的整体布局。接下来再根据选择的GC算法,定制其中的管理细节。
3.1 定制堆内存大小
- 堆内存的⼤⼩极⼤地决定了JAVA程序执⾏的性能,但也并不是越⼤越好。更⼤的堆内存,固然让JVM能够存下更多的对象,但同时也加⼤了GC线程的压⼒;
- 主要涉及到以下几个参数:
-Xms
:设置堆内存的初始⼤⼩;- 默认单位是bytes。这个值必须是1024的整数,并且必须⼤于1M。如果不想设置这么精确,也可以在数字后⾯加k或者K表示KB,m或者M表示MB,g或者G表示GB。例如:
-Xms62991456
、-Xms6144k
、-Xms6m
; - 如果不设置这个值,JVM将会默认将堆内存的初始⼤⼩设置为⽼年代与年轻代的内存之和;
- 另外,有⼀个不稳定参数
-XX:InitalHeapSize
也可以⽤来设置堆内存的初始⼤⼩。如果他出现在-Xms
之后,那么初识对内存⼤⼩将最终由这个参数指定;
- 默认单位是bytes。这个值必须是1024的整数,并且必须⼤于1M。如果不想设置这么精确,也可以在数字后⾯加k或者K表示KB,m或者M表示MB,g或者G表示GB。例如:
-Xmx
:设置堆内存的最⼤⼤⼩;- 配置⽅式和
-Xms
⼀样; - 不过,它的默认值可以在运⾏时基于操作系统⾃⾏决定;
-Xmx
配置等同于-XX: MaxHeapSize
;
- 配置⽅式和
-XX:MinHeapFreeRatio -XX:MinHeapSize
:设置⼀次GC后所允许的堆空间的最⼩值;- 如果剩余的堆空间降落到这个阈值之下,这时堆空间就会启动⼀次扩充;
- 这两个参数⼀个是⽐例,⼀个是⼤⼩;
- 在服务端进⾏部署时,通常将
-Xms
和-Xmx
设置成相同的值,减少 Java 应⽤在运⾏过程中的临时内存申请⾏为,同时也是为了保证JVM的内存在程序运行前都预先设置好。但是如果内存资源⽐较紧张,那就需要JVM能够按需索取内存。先申请⼀⼩部分内存,内存不够了再申请⼀部分新的内存空间。这时,就可以关注-XX:MinHeapFreeRatio -XX:MinHeapSize
这两个参数; - 可以结合指令
java -XX:+PrintCommandLineFlags -version
分析。
3.2 定制非堆内存大小
- ⾮堆空间存储的内容相⽐堆空间,就更安分⼀些;
- ⾮堆空间的⼤部分内容通常变动都不会很⼤,往往也可以更多地交由JVM统⼀进⾏管理;
- 例如ClassSpace主要存储类模板,⽽⼀个JAVA程序的类信息,绝⼤部分都是在JAVA程序启动过程中就加载到内存中,并且⼏乎不会有什么变化(虽然也可以通过⾃定义类加载器,在运⾏时加载更多的类,但⽆论是使⽤频率还是类数量,通常都⾮常少)。
3.2.1 设置元空间
- MetaSpace元空间主要存储的是 Java 类的⼀些元信息。这包括类的结构信息,如字段、⽅法、注解等,以及运⾏时常量池、字段和⽅法字节码等;
- 简单来说,MeatSpace存储了 Java 程序运⾏所必须得类型信息;
- 这些信息,很显然我们不可能具体限制它的⼤⼩。它只要需要,多⼤的空间都必须提供;
- 在JDK8以前的版本中,类的元数据存储在永久代(PermGen)中。然⽽,从JDK8以后,永久代被移除,取⽽代之的就是MetaSpace元空间;
- 与永久代不同的是,MetaSpace并不使⽤JVM的内存,⽽是直接使⽤本地内存,这意味着Metaspace的⼤⼩不再受限于JVM内存⼤⼩的限制,⽽是受操作系统可⽤内存的限制;
- 需要注意的是,这样也不意味着Metaspace的⼤⼩完全不受限,如果本地内存耗尽,还是会导致OutOfMemmoryError异常的;
- 通常需要通过下⾯参数控制元空间⼤⼩:
-XX:MetaspaceSize
:元空间⼤⼩- 这⾥设置的并不是元空间具体的⼤⼩,⽽是当元空间⼤⼩超过这个阈值时,就会触发⼀次GC;
- ⽽在后续运⾏过程中,触发GC的阈值会根据元空间的使⽤情况进⾏⾃动调整;
- 元空间的默认值取决于平台;
-XX:MaxMetaspaceSize
:元空间最⼤值- 设置元空间⼤⼩的最⼤值;
- 默认元空间是⽆限制的;
- 元空间的⼤⼩取决于应⽤本身以及操作系统可提供的内存⼤⼩;
- ⼀个应⽤程序中,元空间内的数据⼤⼩是不应该经常发⽣变动的,所以设定⼀个合理的最⼤值,可以尽早避免⼀些⾮正常的元空间数据暴涨对操作系统的影响。
3.2.2 设置线程栈空间
- Java 进程在运⾏时,会为每个进程开辟⼀块内存⽤来执⾏线程中的对应指令;
- 这整个内存是⼀个栈结构,先进后出;
- 线程中执⾏的每个⽅法对应栈空间中的⼀个⽅法帧;
- ⽅法帧中主要包含了程序计数器、操作数栈、局部变量表、返回地址等⼏个标准部分;
- 另外,某些具体虚拟机实现还会添加⼀些附加信息。例如HotSpot中还添加了动态链接库以及⼀些⾃定义的附加信息;
- 线程栈空间⼤部分的内存都会随着⽅法结束⽽释放,所以通常不需要单独设置;
- 但是如果应⽤中的⽅法嵌套⾮常多,或者有很多⻓期执⾏的复杂⽅法,那么就需要调整栈空间⼤⼩;
- 如果栈空间内存不够,就会抛出StackOverFlowException;
- 主要涉及到下⾯参数:
-Xss
:设置线程栈空间的⼤⼩;- 栈空间默认值⼤⼩,在Linux和MacOS系统中,都是1024KB。在Windows中,则需要依靠配置的虚拟内存⼤⼩决定。例如
-Xss1m
、-Xss1024k
这样; - 配置栈空间⼤⼩,还可以⽤另外⼀个参数:
-XX:ThreadStackSize
。这个参数的作⽤和-Xss
是差不多的,只是配置⽅式稍有不同。例如-XX:ThreadStackSize=1K
、-XX:ThreadStackSize=1024k
。
- 栈空间默认值⼤⼩,在Linux和MacOS系统中,都是1024KB。在Windows中,则需要依靠配置的虚拟内存⼤⼩决定。例如
3.2.3 设置热点代码缓存空间
- 在 Java 中,
-server
选项表示JVM以服务器模式运⾏;- 在服务器模式下,HotSpot 虚拟机会将执⾏频率⾼的热点代码识别出来,提前进⾏编译,并将编译结果缓存起来;
- 后续执⾏时,就可以以编译执⾏的⽅式直接读取缓存,⽽不⽤再⼀⾏代码⼀⾏代码地进⾏解释执⾏;
- ⽽这些热点代码,就保存在⾮堆区的CodeSpace中;
- 热点代码缓存空间也涉及到⼏个核⼼参数:
-XX:InitialCodeCacheSize=size
:设定代码缓存空间的初始⼤⼩;-XX:ReservedCodeCacheSize=size
:设定代码缓存空间的最⼤⼤⼩;- 代码缓冲区最⼤⼤⼩默认值是240MB;
- 如果禁⽌提前编译(参数
-XX:-TieredCompilation
),那么最⼤的⼤⼩默认是48MB。如果⾃⼰指定,这个值不能⽐初始值⼩;
-XX:+SegmentedCodeCache
:启⽤代码缓存分割;- 这是JDK8中没有的⼀个参数,在JDK17中默认启⽤;
- 这个参数的作⽤主要是优化代码缓存空间的内存使⽤。如果没有打开这个选项,那么所有的代码缓存是⼀个⼤的内存⽚段,这不利于内存空间的灵活使⽤;
- 这个机制如果需要⽣效,还需要启⽤提前编译
-XX:+TieredCompilation
并且-XX:ReservedCodeCacheSize >= 240 MB
; - JDK8中没有这个特性;
- 如果对这个代码缓存分割感兴趣,后⾯⼏个参数或许能帮你⼤概了解⼀下,JVM底层是如何对代码缓存进⾏分割的;
-XX:ProfiledCodeHeapSize=size
-XX:NonNMethodCodeHeapSize=size
-XX:NonProfiledCodeHeapSize=size
- 启⽤代码缓存分割后,JVM底层就是将代码缓存划分成这三个部分的。
3.2.4 应用程序类数据共享
-
关于元数据区,补充⼀个⽐较⼩众的JVM优化机制;
-
Application Class Data Sharing(应⽤程序列数据共享,简称AppCDS)是⼀种旨在提⾼运⾏相同代码的多个JVM的启动时间,并减少它们的内存占⽤的⼀种优化机制;
- 使⽤AppCDS机制,可以在JVM第⼀次运⾏时,对它加载过的类的数据进⾏收集并归档,记录到数据⽂件中;
- 之后,这些数据⽂件还可以被后续的JVM进程使⽤;
- 相⽐于每次从class⽂件中加载类信息,AppCDS可以节省JVM初始化过程中的时间和资源;
# 将类信息归档到hello.jsa⽂件中 java -Xshare:dump -XX:SharedArchiveFile=hello.jsa -version # 使⽤归档⽂件启动,并打印类加载⽇志 java -XX:SharedArchiveFile=hello.jsa -Xlog:class+load -version # 有hello.jsa⽂件,加载的最后⼀个类 [0.021s][info][class,load] java.nio.charset.CoderResult source: shared objects file # 删掉hello.jsa⽂件后,依然可以加载类,但是⽐有归档⽂件时会慢⼀些 [0.038s][info][class,load] java.nio.charset.CoderResult source: jrt:/java.base
-
在部署微服务应⽤时,这会是⼀个可选的⽅式。
4 基于JDK17定制JVM的GC参数
-
设定完整体的内存布局后,接下来就到了最重要的环节,优化GC参数。不同的GC算法,对内存的管理⽅法也不同;
-
注意,这同样是⼀个没有标准答案的过程,不要希望⼀套配置打天下。多上阵,多试错才是唯⼀正确的⽅法。⽐如以后学习RocketMQ时,⾃⼰多尝试尝试修改不同的参数组合,会有什么不同的效果;
-
从上⾯RocketMQ的案例也能看到,在优化整体布局时, 在JDK8以前的版本时还设定了
-Xmn
参数,但是在JDK8以后的版本中就没有设置这个参数,这其实也跟GC算法有关;-Xmn
:设置分代收集的GC中年轻代的最⼤⼤⼩;- 堆中的年轻代⽤于存放新new出来的对象。年轻代的GC会⽐其他区域更频繁;
- 年轻代太⼩,就会导致过于频繁的Young GC,⽽如果年轻代过⼤,⼜会导致youngGC的回收效果变差,加⼤Full GC的压⼒,从⽽进⼀步影响整个程序的运⾏效率;
- 官⽅明确建议,对于基础的分代收集器,建议保持年轻代的⼤⼩在整个堆内存的25%到50%之间;
- ⽽对于G1垃圾回收器,由于G1不再有严格固定的年轻代,所有官⽅明确建议,对G1垃圾回收器,不要设置
-Xmn
参数;
-
在JDK17中,已经取消了CMS算法,所以接下来主要针对JDK17中的G1和ZGC,分析相关常⻅的重要参数。
4.1 G1重要参数
-
优化G1的配置之前,还是需要先回顾⼀下G1的基本思想;
- G1 GC 是⼀种分代的、并发的、基于区域的垃圾回收器;
- 它将堆内存划分为多个独⽴的区域(Regions),每个区域可以是 Eden 区、Survivor 区或者 Old 区;
- 为了保持系统的响应性,G1 GC 会尽量在达到⽤户设定的停顿时间⽬标(通过
-XX:MaxGCPauseMillis
参数指定)前进⾏垃圾回收;
- G1 虽然还是⼀个分代的垃圾回收器,但是它的各个Region的划分并不固定;
- 所以在使⽤G1垃圾回收器时,请忘记以往⾮常熟悉的堆内存分代模型;
- ⽐如
-Xmn
(年轻代空间⼤⼩)、-XX:NewRatio
(年轻代与⽼年代⽐例)、-XX:SurvivorRatio
(年轻代中eden区与suvivor区的⽐例)等这些参数;
- 对于G1垃圾回收器,最为核⼼的参数就是三个:
-XX:+UseG1GC
:启动G1;-Xmx
:堆内存⼤⼩;-XX:MaxGCPauseMillis
:期望 G1 达到的最⼤停顿时间,默认200毫秒;
- G1 GC 是⼀种分代的、并发的、基于区域的垃圾回收器;
-
接下来我们就以JDK17为标准,结合RocketMQ的运⾏脚本整理⼀下在实际项⽬中,还有哪些需要了解的G1相关参数;
-
-XX:+UseG1GC
:使⽤G1垃圾回收器;- 在JDK17中,是默认选项;
- G1垃圾回收器适合那些需要⼤量堆内存(建议是6GB以上),同时还需要有稳定的GC延迟(STW延迟时间稳定在0.5秒以下)的应⽤;
-
-XX:G1HeapRegionSize=size
:设定每个Region的⼤⼩;- 这个值必须是2的N次幂,且范围在1MB到32MB之间。这是 G1 最为重要的⼀个参数;
- 这个参数的默认值是不固定的,通常JVM会将堆内存划分为2048个Region,每个Region的⼤⼩就是:堆内存 / 2048;
- 在
2 从RocketMQ学习常用GC调优三部曲
可以看到,RocketMQ将这个参数设定成了16M,这是⼀个偏⼤的设置了;- 较⼤的Region区域可以减少GC的频率,从⽽降低停顿时间的影响;
- 但是,这也意味着增加了每次GC回收的停顿时间(STW);
- 所以,设定这个参数时,需要在停顿时间和数据吞吐量之间进⾏权衡;
-
-XX:G1ReservePercent=percent
:这个参数是告诉G1,为了满⾜停顿时间的整体⽬标时,应该保留多少⽐例的堆空间作为空闲;- 这样,在突发的内存分配需求和垃圾回收效率下降时,G1仍然有⾜够的缓冲空间来避免⻓时间的停顿;
- 这个参数的默认值是10%,这意味着堆内存的10%将被保留为空闲状态。这显然是⼀种空间换时间的策略,⽽RocketMQ将这个值设定为25%,这也是为了追求性能做的⼀种取舍;
- 显然也能猜到,RocketMQ这样的设定,如果在内存不太够的情况下,反⽽会造成内存更加紧张;
-
-XX:InitiatingHeapOccupancyPercent=percent
:这个参数的默认值是45,表示当整个堆中,⽼年代Region达到45%时,G1就会开始并发标记周期;- RocketMQ将这个参数设定为30,显然是为了更积极的进⾏GC。但是,并不意味着每次堆内存⽤到了30%就开始GC,与此相关的还有另外两个参数:
-XX:G1UseAdaptiveIHOP
和-XX:G1AdaptiveIHOPNumInitialSamples
;-XX:G1UseAdaptiveIHOP
是⼀个bool型的参数,默认是启动的。这个参数启动后,G1只会在-XX:G1AdaptiveIHOPNumInitialSamples
参数指定的前⾯⼏次GC活动中按照-XX:InitiatingHeapOccupancyPercent
参数进⾏计算;- 之后,G1会⾃动根据⽬标调整参数;
-XX:G1AdaptiveIHOPNumInitialSamples
的默认值是3;
- 所以,RocketMQ采⽤这种⽐较激进的设计,其实只是为了让RocketMQ的服务在启动时更稳健;
- RocketMQ将这个参数设定为30,显然是为了更积极的进⾏GC。但是,并不意味着每次堆内存⽤到了30%就开始GC,与此相关的还有另外两个参数:
-
-XX:SoftRefLRUPolicyMSPerMB=time
:这个参数就⽐较隐蔽了。它表示在每MB的堆内存中,软引⽤经过多⻓时间才被认为过期。默认值是1000,表示1秒。此处RocketMQ直接设置为0;
Java中4种引⽤的级别和强度由⾼到低依次为:强引⽤ -> 软引⽤ -> 弱引⽤ -> 虚引⽤
-
强引⽤ StrongReference:
-
强引⽤是最普遍的引⽤,如果⼀个对象有强引⽤,那垃圾回收期绝不会回收该对象;
-
软引⽤ SoftReference:
-
如果⼀个对象只有软引⽤,则内存空间充⾜时,垃圾回收期不变会回收;
-
但是如果内存空间不⾜,垃圾回收器就会回收该对象,节省内存空间;
-
弱引⽤ WeakReference
-
弱引⽤与软引⽤的区别在于,只有弱引⽤的对象拥有更短暂的⽣命周期;
-
垃圾回收器线程在扫描它所管辖的内存区域时,⼀旦发现只具有弱引⽤的对象,不管当前内存是否⾜够,都会⽴即回收;
-
不过,由于垃圾回收器线程是⼀个优先级⾮常低的线程,因此不⼀定会很快发现这些只具有弱引⽤的对象;
-
弱引⽤可以重新声明成强引⽤;
-
虚引⽤ PhantomReference
-
虚引⽤顾名思义,就是形同虚设。与其他⼏种引⽤都不同,虚引⽤并不会决定对象的⽣命周期;
-
如果⼀个对象仅持有虚引⽤,那么它就和没有任何引⽤⼀样,在任何时候都可能被垃圾回收器回收;
-
设置虚引⽤的唯⼀⽬的就是在这个对象被GC回收时,收到⼀个系统的通知或者后续添加进⼀步的处理,例如触发对象的
finalize
⽅法; -
还有 Java 当中使⽤直接内存directBuffer时,也会使⽤虚引⽤来保证回收对象时能释放对应的直接内存;
- 这个参数对于控制内存使⽤和避免内存泄漏⾮常有⽤;
- 通过调整这个参数,可以平衡内存使⽤和对象⽣命周期的需求;
- 如果设置得太低,可能会导致频繁地清理软引⽤对象,增加 GC 的负担;
- 如果设置得太⾼,则可能会导致内存占⽤过⾼,甚⾄发⽣ OutOfMemoryError;
- 软引⽤在项⽬当中通常⽤得⽐较少。所以,如果我们的项⽬中也⼏乎没有⽤到过软引⽤,那么不妨可以考虑下参照RocketMQ的经验进⾏⼀下尝试;
-
-
-
另外,相⽐ JDK8,JDK17中新增了⼏个G1相关的参数:
-XX:ParallelGCThreads=threads
:- 设置GC的⼯作线程数;
- 默认值取决于GC算法以及CPU核⼼数;
- ⽐如对于G1,可以通过以下⽅式设置线程数为2:
-XX:ParallelGCThreads=2
;
-XX:G1HeapWastePercent=percent
:设置堆空间的浪费⽐;- HotSpot虚拟机在对空间的使⽤⽐例低于这个值时不会启动GC周期;
- 默认值是5%;
-XX:G1OldCSetRegionThresholdPercent=percent
:- 设置⼀次混合GC中需要清理的Old区的内存⽐例;
- 默认值是堆空间的10%;
- 这也是G1⾮常重要的⼀个参数,将它调⼤,可以降低G1的频率,但是会让每⼀次GC的时间变⻓;
-XX:G1MixedGCCountTarget=number
:设置G1垃圾回收器的线程上限;- HotSpot会为了达到清理
G1OldCSetRegionThresholdPercent
⽐例的Old区的⽬标,会⾃动计算需要启动⼏个G1垃圾回收器,但是垃圾回收器的个数不会超过这个上限; - 默认值是8。
- HotSpot会为了达到清理
4.2 ZGC重要参数
-
ZGC从JDK11版本开始引⼊,JDK13开始发布正式版本。后续每个JDK版本都对ZGC有⼀定的优化,到JDK17版本已经基本稳定,官⽅建议已经可以⽤于⽣产环境;
-
但是⽬前业界使⽤ZGC的场景还是⽐较少,
- ⼀⽅⾯是很多项⽬的JDK版本还没有跟上;
- 另⼀⽅⾯也是因为⼤部分项⽬中⽤到的内存还没有达到需要 ZGC 出⻢的量级;
-
因此实际使⽤ZGC的案例现在还⽐较少,所辖下面就整理⼀下官⽅对于ZGC核⼼参数的配置说明。⽬前阶段,了解⼀下即可;
-
-XX:+UseZGC
Enables the use of the Z garbage collector (ZGC). This is a low latency garbage collector, providing max
pause times of a few milliseconds, at some throughput cost. Pause times are independent of what
heap size is used. Supports heap sizes from 8MB to 16TB.
-
-XX:ZAllocationSpikeTolerance=factor
Sets the allocation spike tolerance for ZGC. By default, this option is set to 2.0. This factor describes the
level of allocation spikes to expect. For example, using a factor of 3.0 means the current allocation rate
can be expected to triple at any time.
-
-XX:ZCollectionInterval=seconds
Sets the maximum interval (in seconds) between two GC cycles when using ZGC. By default, this option
is set to 0 (disabled).
-
-XX:ZFragmentationLimit=percent
Sets the maximum acceptable heap fragmentation (in percent) for ZGC. By default, this option is set to
- Using a lower value will cause the heap to be compacted more aggressively, to reclaim more
memory at the cost of using more CPU time.
-
-XX:+ZProactive
Enables proactive GC cycles when using ZGC. By default, this option is enabled. ZGC will start a
proactive GC cycle if doing so is expected to have minimal impact on the running application. This is
useful if the application is mostly idle or allocates very few objects, but you still want to keep the heap
size down and allow reference processing to happen even when there are a lot of free space on the
heap.
-
-XX:+ZUncommit
Enables uncommitting of unused heap memory when using ZGC. By default, this option is enabled.
Uncommitting unused heap memory will lower the memory footprint of the JVM, and make that
memory available for other processes to use.
-
-XX:ZUncommitDelay=seconds
Sets the amount of time (in seconds) that heap memory must have been unused before being
uncommitted. By default, this option is set to 300 (5 minutes). Committing and uncommitting memory
are relatively expensive operations. Using a lower value will cause heap memory to be uncommitted
earlier, at the risk of soon having to commit it again.
-
-
从参数数量就能看到, ZGC暴露出来的参数⾮常少。⽽算法⽅⾯,ZGC更多的被设计为根据运⾏环境进⾏⾃适应调整;
- ⽐如GC的并发线程数这类参数,ZGC都已经完成了⾃⾏调整;
- 在绝⼤部分情况下,对于ZGC,不调整就是最好的调整;
- 根据现在公布的情况来看 ,对于ZGC基本只需要指定堆⼤⼩
–Xmx
即可; - ⽽优化配置的⽅式只需要在内存使⽤量与GC运⾏频率之间找到⼀个平衡点。
5 GC日志处理
-
在JDK8以前,JVM中的⽇志打印是⽐较混乱的,很多⽇志被分散在多个不同的地⽅,也需要很多不同的参数进⾏打印;
-
从JDK8往后,JVM的⽇志得到了极⼤的精简,所有⽇志打印相关的指令,都集中到了
-Xlog
选项当中; -
这⾥就直接整理⼀个列表,⽐较JDK8与JDK17在⽇志⽅⾯的参数差异:
-
例如RocketMQ中对JDK8以后的版本,统⼀采⽤以下参数打印GC⽇志:
-Xlog:gc*:file=${GC_LOG_DIR}/rmq_srv_gc_%p_%t.log:time,tags:filecount=5,filesize=30M
-Xlog:gc*
:表示打印每次GC的详细信息,等同于JDK8中的-XX:PrintGCDetails
;- 后⾯的file分为三个部分:
- 第⼀部分是⽂件名;
- 第⼆部分表示历史⽂件的后缀,有这⼏个选项:
time(t)
、utctime(utc)
、uptime (u)
、timemillis(tm)
、uptimemillis(um)
、timenanos(tn)
、uptimenanos(un)
、hostname(hn)
、pid (p)
、tid(ti)
、level(l)
、tags(tg)
; - 第三部分表示历史⽂件的个数和⼤⼩。RocketMQ中的配置就表示保留5个⽂件,每个⽂件写满30M就切换下⼀个⽂件。
6 其他JVM调优小经验
-
接下来,仔细查看下RocketMQ的启动脚本,会发现⼀⾏注释掉了的配置信息:
#JAVA_OPT="${JAVA_OPT} -Xdebug -Xrunjdwp:transport=dt_socket,address=9555,server=y,suspend=n"
-
⼀个精益求精的开源软件,为什么要保留这么⼀⾏注释掉的配置呢?其实这⾥就隐藏了⼀个⾮常有⽤的开发技巧——远程进⾏断点调试;
-
远程断点调试,简单来理解, 就是允许我们⽤本地Debug断点调试的⽅式,去调试在远端服务器上运⾏的应⽤程序;
- 这个技巧对于开发⼀些复杂软件,是⾮常有⽤的;
- 因为软件运⾏情况怎么样,只有部署到真实服务器上才能得到真正的验证;
-
我们可以⾃⼰做个⼩案例来理解⼀下这种远程调试的⽅案:
-
示例代码:
package com.roy; import java.util.Scanner; //-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 public class RemoteDebugTest { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String command=""; int count = 0; do{ command = scanner.next(); System.out.println("第"+(++count)+"个指令:"+command); }while (!"quit".equals(command)); System.out.println("接收到退出指令"); System.exit(-1); } }
-
上面主要就是通过监听控制台输⼊,来模拟⼀个⻓时间运⾏的计算程序。假设这个文件就部署在远程服务器上,我们将这个⽂件在控制台启动,同时加上下面的配置信息:
java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005 com.roy.RemoteDebugTest # 执行结果: Listening for transport dt_socket at address: 5005
java
:启动 Java 虚拟机-Xdebug
:启用 JVM 的调试功能-Xrunjdwp
:指定使用 JDWP(Java Debug Wire Protocol)协议进行调试transport=dt_socket
:使用 socket 传输方式server=y
:以服务器模式运行(等待调试器连接)suspend=y
:启动时暂停程序执行,直到调试器连接(如果是n
则会立即执行)address=5005
:监听 5005 端口
com.roy.RemoteDebugTest
:要调试的主类- 最后一行输出表示 JVM 正在 5005 端口等待调试器连接
-
启动后,就会在当前服务器上启动⼀个应⽤监听,监听端⼝就是address参数指定的端⼝号。这样,就相当于在服务器上运⾏了⼀个 Java 程序。并且启动后,这个应⽤程序会阻塞住,等待开启远程监听;
-
接下来,我们就可以在本地IDEA配置远程调试;
-
⾸先需要在IDEA中配置⼀个远端调试的运⾏环境:
-
然后,就可以在IDEA中打好断点,然后启动这个远程运⾏的环境;
-
接下来,在远程服务器的控制台中输入内容,本地 IDEA 也会在远端服务器运⾏到断点代码处时,⾃动进⼊断点调试模式;
-
-
我们这个⼩案例是⽤本地机器模拟的远端服务器,接下来⾃⼰尝试⼀下,把自己感兴趣的项⽬放到远端服务器上试试看;
-
但是要注意⼀下,这种远程调试的⽅式显然是不能⽤在⽣产环境的,
- 因为打开远程调试后,服务端的应⽤程序就会被阻塞,其必须监听到调试请求才会正常执⾏;
- 如果把IDEA中的调试任务终⽌了,远端的应⽤程序就会重新回归到阻塞状态;
-
当然,这样的⼩技巧其实并不需要你⼀定记得⾮常熟练,因为⼤部分的 Java 程序其实对服务器配置没有那么敏感;
- 但是如果后⾯需要接触⼀些spark、flink这样⼤型的计算框架时,记住这个从RocketMQ中学习过这样⼀个⼩技巧,就⾮常好了;
JDK⼯具官⽹⽂档:Java Development Kit Version 17 Tool Specifications;
JDK17的java指令的官⽅⽂档:The java Command;
JDK8的java指令的官⽅⽂档:java。