jvm故障问题排查心得「内存优化技术」java虚拟机内存优化实战案例分析指南(代码片段)

洛神灬殇 洛神灬殇     2023-01-06     392

关键词:

问题总结

  1. 内存多占1G左右,CPU利用率没有明显变化,但随着CMS收集抖动,最高达40%,CPU load平均高出1.0左右
  2. 几乎0停顿,相比于之前每隔5分钟应用停顿3-4s,调优后的应用几乎没有停顿时间,每次”stop the world” 由 youngGC 引起,最高也不过200+ms
  3. GC总时间开销显著减小20%多,吞吐量显著提升
  4. 应用超过500ms的请求响应时间减少3%

参数对比

调优前

-Dfile.encoding=UTF-8 -server -Xms8000M -Xmx8000M -Xmn5000M -Xss256K - 
XX:ThreadStackSize=256 -XX:StackShadowPages=8  -verbose:gc -XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps -XX:PermSize=128m -XX:MaxPermSize=128m -XX:+UseParallelGC

调优后

-Dfile.encoding=UTF-8 -server -Xms10000M -Xmx10000M -Xmn5000M -
XX:MaxTenuringThreshold=1 -XX:SurvivorRatio=30 -XX:TargetSurvivorRatio=50 
-Xnoclassgc -Xss256K -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:PermSize=256m -
XX:MaxPermSize=256m  -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -
XX:CMSInitiatingOccupancyFraction=80  -XX:ParallelGCThreads=24  -XX:ConcGCThreads=24 
-XX:+CMSParallelRemarkEnabled -XX:+CMSScavengeBeforeRemark 
-XX:+ExplicitGCInvokesConcurrent -XX:+UseTLAB  -XX:TLABSize=64K

经验分享

在开始前,我们需要一些数据,因为jvm调优没有一个标准的答案,根据实际应用不同而不同,但也不是完全没有章法可言,从一个实际的应用,我们也可以找出一些规律来,找出一些比较公用的,比如下面三条:

  1. 应用平均和最大暂停时间(stop the world)
  2. 吞吐量,真正运行时间/(GC时间+真正运行时间),而相对的GC开销为:GC时间/(GC时间+真正运行时间)
  3. URL的请求响应时间

查看可以设置的所有参数

使用 -XX:+PrintFlagsFinal 参数:可以查看当前版本的虚拟机所能设置的所有参数,还可以看到其默认值。我使用6u26版本的java虚拟机,一共有663个参数,很多参数不必完全搞懂什么意思,而且很多优化项在JDK6版本中已经默认开启,所以我们只需要了解一些常用的即可。

最大堆的设置

  • 在单机web server的情况下,最大堆的设置建议在物理内存的1/2到2/3之间,如果是16G的物理内存的话,最大堆的设置应该在8000M-10000M之间

  • Java进程消耗的总内存肯定大于最大堆设置的内存:堆内存(Xmx)+ 方法区内存(MaxPermSize)+ 栈内存(Xss,包括虚拟机栈和本地方法栈)线程数 + NIO direct memory + socket 缓存区(receive37KB,send25KB)+ JNI代码 + 虚拟机和GC本身 = java的内存。

  • 我们经常碰到内存巨高的线上问题,留更多的内存给“意外情况”是一件好事也是一件坏事,好事是更多的内存可以给“错误”提供扩展空间,提升“容错性”,不至于马上宕机,但另一方面来说技术人员不会第一时间收到“吃swap”这个告警信息。

GC策略的选择

  • GC调优是JVM调优很重要的一步,当前比较成熟的GC基本上有三种选择,serial、Parallel和CMS,大型互联网应用基本上选择后两种,但Parallel的暂停时间实在太长,以 -Xmx 8000M -Xmn5000M 为例,平均一次youngGC需要100ms-200ms,而FullGC最长需要6s,平均也要4s,虽然当前没有哪种GC策略能完全做到没有暂停时间,但太长的“stop the world”时间也让人无法忍受

  • serial 和ParallelGC都是完全stop the world的GC,而CMS分为六步骤

初始标记(stop the world)
1093.220: [GC [1 CMS-initial-mark: 4113308K(5120000K)] 4180786K(10080000K), 0.0732930 
secs] [Times: user=0.07 sys=0.00, real=0.07 secs]
运行时标记(并发)
1094.275: [CMS-concurrent-mark: 0.980/0.980 secs]
[Times: user=19.95 sys=0.51, real=0.98 secs]
运行时清理(并发)
1094.305: [CMS-concurrent-preclean: 0.028/0.029 secs] 
[Times: user=0.10 sys=0.02, real=0.03 secs]

CMS: abort preclean due to time 1099.643:
[CMS-concurrent-abortable-preclean: 5.288/5.337 secs] 
[Times: user=12.64 sys=1.19, real=5.34 secs]

重新标记(stop the world,这个例子remark前执行了一次youngGC)
1099.647: [GC [YG occupancy: 3308479 K (4960000 K)]
1099.648: [GC 1099.649: [ParNew: 3308479K->42384K(4960000K), 0.1420310 secs]
7421787K->4180693K(10080000K), 0.1447160 secs] [Times: user=2.69 sys=0.03, real=0.15 secs]

1099.793: [Rescan (parallel) , 0.0121000 secs]1099.805: [weak refs processing, 0.0664790 secs] 
[1 CMS-remark: 4138308K(5120000K)] 4180693K(10080000K), 0.2254870 secs] 
[Times: user=3.00 sys=0.05, real=0.23 secs]

运行时清理(并发)
1104.895: [CMS-concurrent-sweep: 4.970/5.020 secs] 
[Times: user=12.43 sys=1.05, real=5.02 secs]
复原(并发)

1104.908: [CMS-concurrent-reset: 0.012/0.012 secs]
[Times: user=0.03 sys=0.01, real=0.01 secs]

要想知道应用真正的停顿时间,可以使用PrintGCApplicationStoppedTime 参数:

63043.344: [GC [PSYoungGen: 5009217K->34119K(5049600K)] 
5985479K->1034614K(8121600K), 0.1721890 secs] [Times: user=2.62 sys=0.01, real=0.18 secs]
  • Total time for which application threads were stopped: 0.1806210 seconds
  • Total time for which application threads were stopped: 0.0074870 seconds

这样看来,真正应用暂停的时间要比stop the world时间还要稍长一点点。

  • 本次调优我基本上放弃了ParallelGC而选择了CMS,CMS在old区很大的时候绝对是个利器,它不仅能大幅降低应用“stop the world”时间,而且还能增加应用的响应时间和小部分吞吐量。
  • CMS还有一种增量模式:iCMS,适用于单CPU模式,会将回收动作分作小块进行,但会增加回收时间,降低吞吐量,对于多CPU来说,可以不用考虑这种模式。

  • PrintFlagsFinal参数可以得知CMS的UseCMSCompactAtFullCollectionCMSParallelRemarkEnabled参数,在JDK6里一直都是默认为true的,所以我们不必显示设置它。

  • 从维护角度来看,在设置参数之前,我们应该首先看看这个参数是不是默认已经开启了,如果默认已经开启了我们就不必要再显示设置它

年轻代(eden和Survivor)、年老代的设置选择了GC策略之后,年轻代和年老代的设置就很重要了,如果一味的追求响应时间,可以尽量把年轻代调大一点,youngGC的回收频率减小了,但回收时间也增大了,5000M的年轻代,平均回收时间在150+ms,3000M的年轻代平均回收时间在90+ms

如果一味的增大年轻代,CMS前提下的年老代的威力也发挥不出来,更容易出现promotion failed导致一次FullGC

但如果一味的调小年轻代,虽然单次回收时间减小,但回收频率会陡增,应用STW时间也会增加,总体年轻代回收的时间也可能会增大,所以调整年轻代和年老代的比例就是一个找平衡的过程

我的经验是年轻代的比例在2/8到4/8之间,具体情况要看实际应用情况而定。

我们都知道年轻代采用的是“copy”算法,有两个survivor空间,每次回收总有一个是空的,另一个存放的是前几次youngGC存留下来而且还不够提升到old资格的对象,所以有三个参数很重要:

  • -XX:MaxTenuringThreshold=15:对象晋升到old的年龄,parallelGC和Serial默认是15,CMS默认是4设置的越大,对象就越难进入到old区,youngGC反复copy的时间就会增大

  • -XX:SurvivorRatio=8,eden和survivor的比例,默认是8,也就是说如果eden为2400M,那么两个survivor都为300M,如果MaxTenuringThreshold设置的很小,那么survivor区的使用率就会降低,反之,survivor的使用率就会增大

  • -XX:TargetSurvivorRatio=80,survivor空间的利用率,默认是50

如果设置SurvivorRatio为65536,MaxTenuringThreshold为0就表示禁止使用survivor空间,在这种模式下,对象直接进入old区,而且我发现在这种模式下,photo的resin启动时间大大减少,以前170s在这种模式下只需要90+s,足足降低了一半,因为这个,我顿时对这种模式产生的兴趣,但CMS的压力就增大了,威力根本发挥不出来了,GC的时间没有减少反而增加,remark的时间也增大到3s,最后不得不忍痛割爱放弃了这种模式

-XX:+CMSScavengeBeforeRemark:这个参数还蛮重要的,它的意思是在执行CMS remark之前进行一次youngGC这样能有效降低remark的时间,之前我没有加这个参数,remark时间最大能达到3s,加上这个参数之后remark时间减少到1s之内

-XX:+UseCMSCompactAtFullCollection,用于指定在Full GC之后进行内存整理,内存整理会使得垃圾收集停顿时间变长,CMS提供了另外一个参数。

-XX:CMSFullGCsBeforeCompaction,用于设置在执行多少次不压缩的Full GC之后,跟着再来一次内存整理。

另外,我发现survivor空间并没有像预期的那样大(eden的1/8),通过跟踪JVM的启动过程中发现,JVM在一定的条件下(可能跟parallelGC和默认SurvivorRatio有关会动态调整survivor的大小,避免内存浪费。

jvm故障问题排查心得「内存诊断系列」jvm内存与kubernetes中pod的内存容器的内存不一致所引发的oomkilled问题总结(下)(代码片段)

承接上文之前文章根据《【JVM故障问题排查心得】「内存诊断系列」JVM内存与Kubernetes中pod的内存、容器的内存不一致所引发的OOMKilled问题总结(上)》我们知道了如何进行设置和控制对应的堆内存和容器内存的之间的关... 查看详情

jvm故障问题排查心得「内存诊断系列」jvm内存与kubernetes中pod的内存容器的内存不一致所引发的oomkilled问题总结(上)

背景介绍在我们日常的工作当中,通常应用都会采用Kubernetes进行容器化部署,但是总是会出现一些问题,例如,JVM堆小于Docker容器中设置的内存大小和Kubernetes的内存大小,但是还是会被OOMKilled。在此我们介绍一下K8s的OOMKilled的E... 查看详情

jvm技术专题内存问题分析和故障排查规划指南「实战篇」(代码片段)

使用dstat和top查看内存使用最高的应用使用dstat查到内存占用最高的是java应用,使用2253M内存,但是这台服务器跑了好几个java,具体哪个进程使用top看下资源情况使用top可以看到java应用整体内存使用率超过了70%,... 查看详情

jvm故障问题排查心得「线程诊断系列」通过jstack线程状态分析虚拟机线程问题指南(代码片段)

前提概要学习研究threaddump文件是一种很不错的能力哦,因为它可以帮助我们在危急关头去解决和分析问题,接下来,就让我们开始分析和研究一下jstackdump文件吧。jstackDump日志文件中的线程状态dump文件里,值得关... 查看详情

jvm故障问题排查心得「内存诊断系列」xmx和xms的大小是小于docker容器以及pod的大小的,为啥还是会出现oomkilled?

为什么我设置的大小关系没有错,还会OOMKilled?这种问题常发生在JDK8u131或者JDK9版本之后所出现在容器中运行JVM的问题:在大多数情况下,JVM将一般默认会采用宿主机Node节点的内存为NativeVM空间(其中包含了堆空间、直接内存空... 查看详情

jvm技术专题gc问题分析和故障排查规划指南「实战篇」(代码片段)

...,但是很多小伙伴都建议我多针对于实际优化角度和故障排查而言,进行相关的分析和总结,所以有了目前的【线上解决方案系列】希望大家会喜欢!什么是GC问题堆内内存泄漏 查看详情

jvm故障问题排查心得「内存诊断系列」xmx和xms的大小是小于docker容器以及pod的大小的,为啥还是会出现oomkilled?(代码片段)

为什么我设置的大小关系没有错,还会OOMKilled?这种问题常发生在JDK8u131或者JDK9版本之后所出现在容器中运行JVM的问题:在大多数情况下,JVM将一般默认会采用宿主机Node节点的内存为NativeVM空间(其中包含了... 查看详情

jvm故障问题排查心得「内存诊断系列」docker容器经常被kill掉,k8s中该节点的pod也被驱赶,怎么分析?

背景介绍最近的docker容器经常被kill掉,k8s中该节点的pod也被驱赶。我有一个在主机中运行的Docker容器(也有在同一主机中运行的其他容器)。该Docker容器中的应用程序将会计算数据和流式处理,这可能会消耗大量内存。该容器... 查看详情

jvm故障问题排查心得「内存诊断系列」docker容器经常被kill掉,k8s中该节点的pod也被驱赶,怎么分析?(代码片段)

背景介绍最近的docker容器经常被kill掉,k8s中该节点的pod也被驱赶。我有一个在主机中运行的Docker容器(也有在同一主机中运行的其他容器)。该Docker容器中的应用程序将会计算数据和流式处理,这可能会消耗大量... 查看详情

jvm线上故障排查基本操作--内容问题排查

内存问题排查说完了CPU的问题排查,再说说内存的排查,通常,内存的问题就是GC的问题,因为Java的内存由GC管理。有2种情况,一种是内存溢出了,一种是内存没有溢出,但GC不健康。内存溢出的情况可以通过加上-XX:+HeapDumpOnOutO... 查看详情

阿里架构技术分享java虚拟机,jvm内核-原理,诊断与优化+内存模型

...,谢谢大家指出与留言。一、目录JVM启动流程JVM基本结构内存模型编译和解释运行的概念二、JVM启动流程jvm启动的时候一定是由java命令,或者javaw命令;java启动命令会跟一个启动类JavaXXX,启动类 查看详情

jvm线上故障排查基本操作

#前言对于后端程序员,特别是Java程序员来讲,排查线上问题是不可避免的。各种CPU飚高,内存溢出,频繁GC等等,这些都是令人头疼的问题。楼主同样也遇到过这些问题,那么,遇到这些问题该如何解决呢?首先,出现问题,... 查看详情

jvm线上故障排查基本操作(转)

前言  对于后端程序员,特别是Java程序员来讲,排查线上问题是不可避免的。各种CPU飚高,内存溢出,频繁GC等等,这些都是令人头疼的问题。楼主同样也遇到过这些问题,那么,遇到这些问题该如何解决呢?  首先,出现... 查看详情

书籍推荐:《实战java虚拟机——jvm故障诊断与性能优化》下载

本书详细介绍Java虚拟机的基本原理和优化诊断方法。其中重点介绍Java虚拟机的体系结构、常用的虚拟机参数、Java虚拟机的垃圾回收原理、算法以及目前虚拟机所支持的各种垃圾回收器及其区别、特点和使用方法。在实践和调优... 查看详情

java线上故障排查

   Java线上故障主要会包括CPU、磁盘、内存以及网络问题,而大多数故障可能会包含不止一个层面的问题,所以进行排查时候尽量四个方面依次排查一遍。同时例如jstack、jmap等工具也是不囿于一个方面的问题的,基本... 查看详情

java虚拟机:jvm内存模型

...的了解那块的内存区域出现问题,以便于快速的解决生产故障。 先看一张图,这张图能很清晰的说明JVM内存结构布局。Jav 查看详情

jvm故障排查

参考技术A背景:  服务在正常运营中,偶尔出现进程被杀死的情况,所以总结一下排查问题的方法1.应用日志  如:根据配置的logback-spring,logback配置的日志路径下去寻找2.容器日志 如:tomcat崩溃,去catalina.201X-XX-... 查看详情

java内存使用异常导致cpu100%原因(线上jvm排查之二)

前言:写这篇博客的初衷是发现线上JVM问题以及排查,觉得有必要记录并分享出来,有感于很多市面上的教程讲述JVM的教程,甚至还有很多深入到Java字节码和JVM底层源码领域等深入知识,我不否认这些理论和源码也很重要,但... 查看详情