一、JVM参数
1.1 常用参数
参数名称 | 含义 | 默认值 | |
-Xms | 初始堆大小 | 物理内存的1/64(<1GB) | 默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制。此值建议与 |
-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,暂停其他所有工作线程直到收集结束。垃圾收集器做垃圾回收中断应用执行的时间。
G1回收器默认200ms停顿时长。
1.3 提高吞吐量
吞吐量=运行时长/(运行时长+GC时长)。
通过-XX:GCTimeRatio=n参数可以设置吞吐量,99代表吞吐量为99%, 一般吞吐量不能低于95%。
1.4 调整堆内存大小
根据程序运行时老年代存活对象大小(记为x)进行调整,整个堆内存大小设置为X的3~4倍。年轻代占堆内存的3/8。
-Xms:初始堆内存大小。默认:物理内存小于192MB时,默认为物理内存的1/2;物理内存大192MB且小于128GB时,默认为物理内存的1/4;物理内存大于等于128GB时,都为32GB。
-Xmx:最大堆内存大小,建议保持和初始堆内存大小一样。因为从初始堆到最大堆的过程会有一定的性能开销,而且现在内存不是稀缺资源。
-Xmn:年轻代大小。JDK官方建议年轻代占整个堆大小空间的3/8左右。
1.5 调整堆内存比例
调整伊甸园区和幸存区比例、新生代和老年代比例。
Young GC频繁时,我们可以提高新生代在堆内存中的比例、提高伊甸园区在新生代的比例,令新生代不那么快被填满。
默认情况,伊甸园区:S0:S1=8:1:1,新生代:老年代=1:2。
1.6 调整升老年代年龄
JDK8时Young GC默认把15岁的对象移动到老年代。JDK9默认值改为7。
当Full GC频繁时,我们提高升老年龄,让年轻代的对象多在年轻代待一会,从而降低Full GC频率。
1.7 调整大对象阈值
Young GC时大对象会不顾年龄直接移动到老年代。当Full GC频繁时,我们关闭或提高大对象阈值,让老年代更迟填满。
默认是0,即大对象不会直接在YGC时移到老年代。
1.8 调整GC的触发条件
(1)CMS调整老年代触发回收比例
CMS的并发标记和并发清除阶段是用户线程和回收线程并发执行,如果老年代满了再回收会导致用户线程被强制暂停。所以我们修改回收条件为老年代的60%,保证回收时预留足够空间放新对象。CMS默认是老年代68%时触发回收机制。
(2)G1调整存活阈值
超过存活阈值的Region,其内对象会被混合回收到老年代。G1回收时也要预留空间给新对象。存活阈值默认85%,即当一个内存区块中存活对象所占比例超过 85% 时,这些对象就会通过 Mixed GC 内存整理并晋升至老年代内存区域。
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垃圾收集器(新生代)
设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
设置CMS垃圾收集器(老年代)
设置G1垃圾收集器
1.10 案例
案例1:
二、参数配置相关问题
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 参数配置最佳实践,来自阿里云:
2.4 堆大小和规格内存的参数值可以相同吗?
不可以。因为系统自身组件存在内存开销。
例如使用 SLS 进行日志收集时会占用一小部分的内存空间,所以不能将 JVM 堆大小设置为和规格内存大小相同的数值,需要为这些系统组件预留足够的内存空间。
三、容器环境下JVM参数
3.1 堆大小参数
(1)java版本>=8u191
JDK8u191后新增了容器支持开关-XX:UseContainerSupport
,并且默认开启。
并增加了这些参数:
- MaxRAMPercentage 堆的最大值百分比。
- InitialRAMPercentage 堆的初始化的百分比。
- MinRAMPercentage 堆的最小值的百分比。
建议使用内存参数参数:
计算方法(这里做了简化,实际计算要复杂些):最大堆大小 = MaxRAM(默认为容器最大可使用内存) * MaxRAMPercentage / 100
。
注意:如果使用了-Xmx参数,则不会进入上面的堆大小的计算逻辑,而直接将MaxHeapSize(最大堆大小)等同于我们设置的-Xmx。
不建议通过-Xms -Xmx限制堆大小
您可以通过设置-Xms和-Xmx来限制堆大小,但该方式存在以下两个问题:
- 当规格大小调整后,需要重新设置堆大小参数。
- 当参数设置不合理时,会出现应用堆大小未达到阈值但容器OOM被强制关闭的情况。
说明 应用程序出现OOM问题时,会触发Linux内核的OOM Killer机制。该机制能够监控占用过大内存,尤其是瞬间消耗大量内存的进程,然后它会强制关闭某项进程以腾出内存留给系统,避免系统立刻崩溃。
(2)8u121<java版本<8u191
如果是使用OracleJDK需要额外开启实验参数 -XX:UnlockExperimentalVMOptions
。
建议使用如下参数:
或自行设置
(3)java版本<8u121
不要在容器化环境使用。
3.2 案例
-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
3、查找出java进程:jps命令
4、统计gc信息统计: jstat –gcutil 9 3000 每三秒打印一次,9为进程id,3000时间(单位秒)
4.1.2 非容器应用使用
1、查找出java进程:jps命令
2、统计gc信息统计: jstat –gc 19576 1000 10 每三秒打印一次,9为进程id,3000时间(单位秒)
jstat -gcutil
显示内容与 -gc
基本相同,但输出主要关注已使用空间占总空间的百分比。
jstat -gccause
与 -gcutil
功能一样,但是会额外输出导致最后一次或当前正在发生的 GC 产生的原因。
4.2 MAT
4.1 概述
打开Mat后File>OpenHeapDump打开一个对应的dump文件后,此时对应的打开后结果如图所示:
各个功能入口:
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可以看到当前类的回收情况,内存地址,等
补充解释:
- 字段一:表示当前类所对应的对象数量
- 字段二: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。
使用场景
- 开始 Dump 分析时,首先应使用 Dominator tree 了解各支配树起点对象所支配内存的大小,进而了解哪几个起点对象是 GC 无法释放大内存的原因。
- 当个别对象支配树的 Retained Heap 很大存在明显倾斜时,可以重点分析占比高的对象支配关系,展开子树进一步定位到问题根因,如下图中可看出最终是 SameContentWrapperContainer 对象持有的 ArrayList 过大。
(1)聚合定位爆发点
有些情况下可能并没有支配起点对象的 Retained Heap 占用很大内存(比如 class X 有100个对象,每个对象的 Retained Heap 是10M,则 class X 所有对象实际支配的内存是 1G,但可能 Dominator tree 的前20个都是其他class 的对象),这时可以按 class、package、class loader 做聚合,进而定位目标。
下图中各 GC Roots 所支配的内存均不大,这时需要聚合定位爆发点。
在 Dominator tree 展现后按 class 聚合,如下图:
可以定位到是 SomeEntry 对象支配内存较多,然后结合代码进一步分析具体原因。
在一些操作后定位到异常持有 Retained Heap 对象后(如从代码看对象应该被回收),可以获取对象的直接支配者,操作方式如下。
(2)需要查看当前该ConcurrentHashMap@0x60191cfa8对象都引用了那些数据,以及当前该对象是被那几个对象所引用的,如何查看?
鼠标在当前所要查看的对象右键,点击List Objects可以看到分别提供了:
- with outgoing references(查看当前该对象的所有的引用信息)
- with incoming references(查看当前该对象是被那几个对象所引用的;