一、JVM参数

1.1 常用参数

//调整内存大小
-XX:MetaspaceSize=128m(元空间默认大小)
-XX:MaxMetaspaceSize=128m(元空间最大大小)
-Xms1024m(初始堆大小)
-Xmx1024m(最大堆大小)
-Xmn256m(新生代大小)
-Xss256k(栈最大深度大小)
 
//调整内存比例
 //伊甸园:幸存区
-XX:SurvivorRatio=8(伊甸园:幸存区=8:2)
 //新生代和老年代的占比
 -XX:NewRatio=4  //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
 
//修改垃圾回收器
//设置Serial垃圾收集器(新生代)
//-XX:+UseSerialGC
 //设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
//-XX:+UseParallelOldGC
 //CMS垃圾收集器(老年代)
//-XX:+UseConcMarkSweepGC
 //设置G1垃圾收集器
-XX:+UseG1GC
 
//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
 -XX:MaxGCPauseMillis
 
 //进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,JDK8默认值15,JDK9默认值7
 -XX:InitialTenuringThreshold=7
 //新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
  -XX:PretenureSizeThreshold=1000000
 
 //使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
 -XX:CMSInitiatingOccupancyFraction 
 //G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
 -XX:G1MixedGCLiveThresholdPercent=65
 
 //Heap Dump(堆转储)文件
 //当发生OutOfMemoryError错误时,自动生成堆转储文件。
-XX:+HeapDumpOnOutOfMemoryError 
 //错误输出地址
-XX:HeapDumpPath=/Users/a123/IdeaProjects/java-test/logs/dump.hprof
 
 //GC日志
-XX:+PrintGCDetails(打印详细GC日志)
-XX:+PrintGCTimeStamps:打印GC时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps:打印GC时间戳(以日期格式)
-Xlog:gc:(打印gc日志地址)

参数名称

含义

默认值

 

-Xms

初始堆大小

物理内存的1/64(<1GB)

默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。此值建议与-Xmx相同,避免每次垃圾回收完成后JVM重新分配内存。

-Xmx

最大堆大小

物理内存的1/4(<1GB)

默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制

-Xmn

年轻代大小(1.4or lator)

 

注意:此处的大小是(eden+ 2 survivor space).与jmap -heap中显示的New gen是不同的。

整个堆大小=年轻代大小 + 年老代大小 + 持久代大小.

增大年轻代后,将会减小年老代大小.此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8

-Xss

每个线程的堆栈大小

 

JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K.更具应用的线程所需内存大小进行 调整.在相同物理内存下,减小这个值能生成更多的线程.但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右

一般小的应用, 如果栈不是很深, 应该是128k够用的 大的应用建议使用256k。这个选项对性能影响比较大,需要严格的测试。(校长)

和threadstacksize选项解释很类似,官方文档似乎没有解释,在论坛中有这样一句话:"”

-Xss is translated in a VM flag named ThreadStackSize”

一般设置这个值就可以了。

-XX:NewRatio

年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)

 

-XX:NewRatio=4表示年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5

Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。

-XX:SurvivorRatio

Eden区与Survivor区的大小比值

 

设置为8,则两个Survivor区与一个Eden区的比值为2:8,一个Survivor区占整个年轻代的1/10

-XX:+DisableExplicitGC

关闭System.gc()

 

这个参数需要严格的测试

-XX:MaxTenuringThreshold

垃圾最大年龄

 

如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率.如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活 时间,增加在年轻代即被回收的概率

该参数只有在串行GC时才有效.

-XX:PretenureSizeThreshold

对象超过多大是直接在旧生代分配

0

单位字节 新生代采用Parallel Scavenge GC时无效

另一种直接在旧生代分配的情况是大的数组对象,且数组中无外部引用对象.

-XX:+CollectGen0First

FullGC时是否先YGC

false

 


1.2 减少停顿时间

STW:Stop The World,暂停其他所有工作线程直到收集结束。垃圾收集器做垃圾回收中断应用执行的时间。

可以通过-XX:MaxGCPauseMillis参数进行设置,以毫秒为单位,至少大于1。

//GC停顿时间,垃圾收集器会尝试用各种手段达到这个时间
 -XX:MaxGCPauseMillis=10

G1回收器默认200ms停顿时长。

1.3 提高吞吐量

吞吐量=运行时长/(运行时长+GC时长)。

通过-XX:GCTimeRatio=n参数可以设置吞吐量,99代表吞吐量为99%, 一般吞吐量不能低于95%。

-XX:GCTimeRatio=99
吞吐量太高会拉长停顿时间,造成用户体验下降。


1.4 调整堆内存大小

根据程序运行时老年代存活对象大小(记为x)进行调整,整个堆内存大小设置为X的3~4倍。年轻代占堆内存的3/8。

-Xms:初始堆内存大小。默认:物理内存小于192MB时,默认为物理内存的1/2;物理内存大192MB且小于128GB时,默认为物理内存的1/4;物理内存大于等于128GB时,都为32GB。

-Xmx:最大堆内存大小,建议保持和初始堆内存大小一样。因为从初始堆到最大堆的过程会有一定的性能开销,而且现在内存不是稀缺资源。

-Xmn:年轻代大小。JDK官方建议年轻代占整个堆大小空间的3/8左右。

//调整内存大小
-XX:MetaspaceSize=128m(元空间默认大小)
-XX:MaxMetaspaceSize=128m(元空间最大大小)
-Xms1024m(初始堆内存大小)
-Xmx1024m(最大堆内存大小)
-Xmn256m(新生代大小)
-Xss256k(栈最大深度大小)

1.5 调整堆内存比例

调整伊甸园区和幸存区比例、新生代和老年代比例。

Young GC频繁时,我们可以提高新生代在堆内存中的比例、提高伊甸园区在新生代的比例,令新生代不那么快被填满。

默认情况,伊甸园区:S0:S1=8:1:1,新生代:老年代=1:2。

//调整内存比例
 //伊甸园:幸存区
-XX:SurvivorRatio=8(伊甸园:幸存区=8:2)
 //新生代和老年代的占比
 -XX:NewRatio=4  //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2

1.6 调整升老年代年龄

JDK8时Young GC默认把15岁的对象移动到老年代。JDK9默认值改为7。

当Full GC频繁时,我们提高升老年龄,让年轻代的对象多在年轻代待一会,从而降低Full GC频率。

//进入老年代最小的GC年龄,年轻代对象转换为老年代对象最小年龄值,JDK8默认值15,JDK9默认值7
 -XX:InitialTenuringThreshold=7

1.7 调整大对象阈值

Young GC时大对象会不顾年龄直接移动到老年代。当Full GC频繁时,我们关闭或提高大对象阈值,让老年代更迟填满。

默认是0,即大对象不会直接在YGC时移到老年代。

//新生代可容纳的最大对象,大于则直接会分配到老年代,0代表没有限制。
  -XX:PretenureSizeThreshold=1000000

1.8 调整GC的触发条件

(1)CMS调整老年代触发回收比例

CMS的并发标记和并发清除阶段是用户线程和回收线程并发执行,如果老年代满了再回收会导致用户线程被强制暂停。所以我们修改回收条件为老年代的60%,保证回收时预留足够空间放新对象。CMS默认是老年代68%时触发回收机制。

//使用多少比例的老年代后开始CMS收集,默认是68%,如果频繁发生SerialOld卡顿,应该调小
 -XX:CMSInitiatingOccupancyFraction

(2)G1调整存活阈值

超过存活阈值的Region,其内对象会被混合回收到老年代。G1回收时也要预留空间给新对象。存活阈值默认85%,即当一个内存区块中存活对象所占比例超过 85% 时,这些对象就会通过 Mixed GC 内存整理并晋升至老年代内存区域。

//G1混合垃圾回收周期中要包括的旧区域设置占用率阈值。默认占用率为 65%
 -XX:G1MixedGCLiveThresholdPercent=65

1.9 选择合适的垃圾回收器

JVM调优最实用、最有效的方式是升级垃圾回收器,根据CPU核数,升级当前版本支持的最新回收器。

CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。

CPU多核,关注吞吐量 ,那么选择Parallel Scavenge+Parallel Old组合(JDK8默认)。

CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择ParNew+CMS,吞吐量降低但是低停顿。

CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。

设置Serial垃圾收集器(新生代)


//修改垃圾回收器

//设置Serial垃圾收集器(新生代)

-XX:+UseSerialGC

设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器  


//修改垃圾回收器

//设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器

-XX:+UseParallelOldGC

设置CMS垃圾收集器(老年代)  

//CMS垃圾收集器(老年代)

-XX:+UseConcMarkSweepGC

设置G1垃圾收集器  

//修改垃圾回收器

//设置G1垃圾收集器

-XX:+UseG1GC


1.10 案例

案例1:

-Xms2048m -Xmx2048m -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

详细说明
-Xms
设置JVM初始内存大小。建议与-Xmx相同,避免每次垃圾回收完成后JVM重新分配内存。
-Xmx
设置JVM最大可用内存大小。为避免容器OOM,请为系统预留足够的内存大小。
-XX:+PrintGCDetails
输出GC详细信息。
-XX:+PrintGCDateStamps
输出GC时间戳。日期形式,例如2019-12-24T21:53:59.234+0800。
-Xloggc:/home/admin/nas/gc-${POD_IP}-$(date '+%s').log
GC日志文件路径。需保证Log文件所在容器路径已存在,建议您将该容器路径挂载到NAS目录以便自动创建目录以及实现日志的持久化存储。
-XX:+HeapDumpOnOutOfMemoryError
JVM发生OOM时,自动生成DUMP文件。
-XX:HeapDumpPath=/home/admin/nas/dump-${POD_IP}-$(date '+%s').hprof
DUMP文件路径。需保证DUMP文件所在容器路径已存在,建议您将该容器路径挂载到NAS目录,以便自动创建目录以及实现日志的持久化存储。






二、参数配置相关问题

2.1 为什么JVM参数设置了6 GB,但是内存使用率却很低?

虽然JVM参数已设置-Xms6g -Xmx6g,但是操作系统不会马上分配6 GB的物理内存,需要实际使用后才分配。因此,内存使用率在应用启动的时候,会相对较低,后续会出现攀爬现象。

2.2 容器出现137退出码的含义是什么?

当容器使用内存超过限制时,会出现容器OOM,导致容器被强制关闭。此时业务应用内存可能并未达到JVM堆大小上限,所以不会产生Dump日志。建议您调小JVM堆大小的上限,为容器内其他系统组件预留足够多的内存空间。

2.3 推荐的堆大小设置。

内存规格大小

JVM堆大小

1 GB

600 MB

2 GB

1434 MB

4 GB

2867 MB

8 GB

5734 MB

JVM 参数配置最佳实践,来自阿里云:

JVM调优_堆内存

2.4 堆大小和规格内存的参数值可以相同吗?

不可以。因为系统自身组件存在内存开销。

例如使用 SLS 进行日志收集时会占用一小部分的内存空间,所以不能将 JVM 堆大小设置为和规格内存大小相同的数值,需要为这些系统组件预留足够的内存空间。

三、容器环境下JVM参数

3.1 堆大小参数

(1)java版本>=8u191

JDK8u191后新增了容器支持开关-XX:UseContainerSupport,并且默认开启。
并增加了这些参数:

  • MaxRAMPercentage 堆的最大值百分比。
  • InitialRAMPercentage 堆的初始化的百分比。
  • MinRAMPercentage 堆的最小值的百分比。

建议使用内存参数参数:

-XX:MaxRAMPercentage=70.0 -XX:InitialRAMPercentage=50.0

计算方法(这里做了简化,实际计算要复杂些):
最大堆大小 = MaxRAM(默认为容器最大可使用内存) * MaxRAMPercentage / 100

注意:如果使用了-Xmx参数,则不会进入上面的堆大小的计算逻辑,而直接将MaxHeapSize(最大堆大小)等同于我们设置的-Xmx。

不建议通过-Xms -Xmx限制堆大小

您可以通过设置-Xms和-Xmx来限制堆大小,但该方式存在以下两个问题:

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

说明 应用程序出现OOM问题时,会触发Linux内核的OOM Killer机制。该机制能够监控占用过大内存,尤其是瞬间消耗大量内存的进程,然后它会强制关闭某项进程以腾出内存留给系统,避免系统立刻崩溃。

(2)8u121<java版本<8u191

如果是使用OracleJDK需要额外开启实验参数 -XX:UnlockExperimentalVMOptions

建议使用如下参数:

-XX:UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=2 -XX:MinRAMFraction=2

或自行设置

-Xmx:{用户自定义}

(3)java版本<8u121

不要在容器化环境使用。

3.2 案例

-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



-XX:+UseContainerSupport

使用容器内存。允许JVM从主机读取cgroup限制,例如可用的CPU和RAM,并进行相应的配置。当容器超过内存限制时,会抛出OOM异常,而不是强制关闭容器。

-XX:InitialRAMPercentage

设置JVM使用容器内存的初始百分比。建议与-XX:MaxRAMPercentage保持一致,推荐设置为70.0。

-XX:MaxRAMPercentage

设置JVM使用容器内存的最大百分比。由于存在系统组件开销,建议最大不超过75.0,推荐设置为70.0。

-XX:+PrintGCDetails

输出GC详细信息。

-XX:+PrintGCDateStamps

输出GC时间戳。日期形式,例如2019-12-24T21:53:59.234+0800。

-Xloggc:/home/admin/nas/gc-${POD_IP}-$(date '+%s').log

GC日志文件路径。需保证Log文件所在容器路径已存在,建议您将该容器路径挂载到NAS目录,以便自动创建目录以及实现日志的持久化存储。

-XX:+HeapDumpOnOutOfMemoryError

JVM发生OOM时,自动生成DUMP文件。

-XX:HeapDumpPath=/home/admin/nas/dump-${POD_IP}-$(date '+%s').hprof

DUMP文件路径。需保证DUMP文件所在容器路径已存在,建议您将该容器路径挂载到NAS目录,以便自动创建目录以及实现日志的持久化存储。


四、定位问题工具

4.1 jstat

4.1.1 容器应用使用

1、列出docker容器:docker ps

2、标准输入和关联终端:docker exec -it 容器ID  bash

JVM调优_堆内存_02

3、查找出java进程:jps命令

JVM调优_堆内存_03


4、统计gc信息统计: jstat –gcutil 9 3000 每三秒打印一次,9为进程id,3000时间(单位秒)

JVM调优_jvm调优_04


4.1.2 非容器应用使用

1、查找出java进程:jps命令

2、统计gc信息统计: jstat –gc 19576 1000 10 每三秒打印一次,9为进程id,3000时间(单位秒)

JVM调优_jvm调优_05


JVM调优_老年代_06

jstat -gcutil

显示内容与 -gc 基本相同,但输出主要关注已使用空间占总空间的百分比。

jstat -gccause

与 -gcutil 功能一样,但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因。

JVM调优_垃圾收集器_07


4.2 MAT

4.1 概述

打开Mat后File>OpenHeapDump打开一个对应的dump文件后,此时对应的打开后结果如图所示:

JVM调优_堆内存_08

各个功能入口:

JVM调优_堆内存_09

Overview页签下分别包含了:Actions,Reports,Step By Step 三大块功能;每一块功能下的子集所对应的作用分别是:

  • Actions:
  • Histogram 列出每个类所对应的对象个数,以及所占用的内存大小;
  • Dominator Tree 以占用总内存的百分比的方式来列举出所有的实例对象,注意这个地方是直接列举出的对应的对象而不是类,这个视图是用来发现大内存对象的
  • Top Consumers:按照类和包分组的方式展示出占用内存最大的一个对象
  • Duplicate Classes:检测由多个类加载器所加载的类信息(用来查找重复的类)
  • Reports:
  • Leak Suspects:通过MAT自动分析当前内存泄露的主要原因
  • Top Components:Top组件,列出大于总堆1%的组件的报告
  • Step By Step:
  • Component Report:组件报告,分析属于公共根包或类加载器的对象;

上述所有被标注加粗的部分,是内存溢出dump分析时较为常用的功能点也是下面主要讲解的内容。

4.2.1 Histogram

通过Histogram 列出每个类所对应的对象个数,以及所占用的内存大小;

此处选中一个ClassName单击后,通过左上角Inspector可以看到当前类的回收情况,内存地址,等

JVM调优_jvm_10

补充解释:

  • 字段一:表示当前类所对应的对象数量
  • 字段二:Shallow Size是对象本身占据的内存的大小,不包含其引用的对象。对于常规对象(非数组)的Shallow Size由其成员变量的数量和类型来定,而数组的ShallowSize由数组类型和数组长度来决定,它为数组元素大小的总和;
  • 字段三:Retained Size=当前对象大小+当前对象可直接或间接引用到的对象的大小总和。(间接引用的含义:A->B->C,C就是间接引用) ,并且排除被GC Roots直接或者间接引用的对象;

关于红框内的Statics,Attributes,Classhierarchy,Value则分别表示当前类的静态变量,属性,当前类的层次结构图,以及当前类所对应的值Value;

注意:当前Histogram的列属性:ClassName,Objects,ShallowHeap,RetainedHeap这几个列属性下面都是有提供一个输入框,通过该输入框可以进行相关类的检索,比如:在ClassName下输入一个正则.quark.那么则获取到所有包路径为quark的类信息;

4.2.3 Dominator Tree

以占用总内存的百分比的方式来列举出所有的实例对象,可以用来发现大内存对象;

笔者使用频率的 Top1。

JVM调优_老年代_11

使用场景

  • 开始 Dump 分析时,首先应使用 Dominator tree 了解各支配树起点对象所支配内存的大小,进而了解哪几个起点对象是 GC 无法释放大内存的原因。
  • 当个别对象支配树的 Retained Heap 很大存在明显倾斜时,可以重点分析占比高的对象支配关系,展开子树进一步定位到问题根因,如下图中可看出最终是 SameContentWrapperContainer 对象持有的 ArrayList 过大。

JVM调优_垃圾收集器_12

(1)聚合定位爆发点

有些情况下可能并没有支配起点对象的 Retained Heap 占用很大内存(比如 class X 有100个对象,每个对象的 Retained Heap 是10M,则 class X 所有对象实际支配的内存是 1G,但可能 Dominator tree 的前20个都是其他class 的对象),这时可以按 class、package、class loader 做聚合,进而定位目标。

下图中各 GC Roots 所支配的内存均不大,这时需要聚合定位爆发点。

JVM调优_jvm_13

在 Dominator tree 展现后按 class 聚合,如下图:

JVM调优_堆内存_14

可以定位到是 SomeEntry 对象支配内存较多,然后结合代码进一步分析具体原因。

JVM调优_老年代_15

在一些操作后定位到异常持有 Retained Heap 对象后(如从代码看对象应该被回收),可以获取对象的直接支配者,操作方式如下。

JVM调优_垃圾收集器_16

(2)需要查看当前该ConcurrentHashMap@0x60191cfa8对象都引用了那些数据,以及当前该对象是被那几个对象所引用的,如何查看?

鼠标在当前所要查看的对象右键,点击List Objects可以看到分别提供了:

  • with outgoing references(查看当前该对象的所有的引用信息)
  • with incoming references(查看当前该对象是被那几个对象所引用的;

JVM调优_堆内存_17