「这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战」
前言
- 关于作者:励志不秃头的一个CURD的Java农民工
- 关于文章:以下内容单纯为作者觉得面试八股文中比较经常遇到的总结,同时会穿插一些作者面试遇到的问题作为记录,打*号的都是作者主观认为比较重要的
JVM实战
一般我们进行分析前,要学会看GC的日志,那么首先就要了解GC日志的打印参数,当然一般公司都是默认配置好的
- -verbose:gc —— 输出gc日志信息,默认输出到标准输出
- -XX:+PrintGC —— 输出GC日志。类似:-verbose:gc
- -XX:+PrintGCDetails —— 在发生垃圾回收时打印内存回收详细的日志, 并在进程退出时输出当前内存各区域分配情况
- -XX:+PrintGCTimeStamps —— 输出GC发生时的时间戳
- -XX:+PrintGCDateStamps —— 输出GC发生时的时间戳(以日期的形式,如 2022-01-27T21:53:59.234+0800)
- -XX:+PrintHeapAtGC —— 每一次GC前和GC后,都打印堆信息
- -Xloggc: —— 表示把GC日志写入到一个文件中去,而不是打印到标准输出中
日志分析
2022-11-20T17:19:43.265-0800: 0.822: [GC (ALLOCATION FAILURE) [PSYOUNGGEN: 76800K->8433K(89600K)] 76800K->8449K(294400K), 0.0088371 SECS] [TIMES: USER=0.02 SYS=0.01, REAL=0.01 SECS]
-
2020-11-20T17:19:43.265-0800:日志打印时间
-
0.822:gc发生时,Java虚拟机启动以来经过的秒数
-
[GC (Allocation Failure):发生了一次垃圾回收,这是一次Minor GC 。它不区分新生代 GC 还是老年代 GC,括号里的内容是gc发生的原因,这里的Allocation Failure的原因是新生代中没有足够区域能够存放需要分配的数据而失败。
-
[PSYoungGen: 76800K->8433K(89600K)]:
- PSYoungGen:表示GC发生的区域,区域名称与使用的GC 收集器是密切相关的
-
76800K->8433K(89600K):GC 前该内存区域已使用容量 - > GC 后该区域容量 (该区域总容量)
- 对年轻代执行了一次GC,GC之前都使用了76800KB了,但是GC之后只有8433KB的对象是存活下来的。
-
[Times: user=0.02 sys=0.01, real=0.01 secs]
- user:指的是CPU工作在用户态所花费的时间
- sys:指的是CPU工作在内核态所花费的时间
- real:指的是在此次GC事件中所花费的总时间
工具推荐
jstat
在服务器上输入
//每隔1秒钟更新出来最新的一行jstat统计信息,一共执行10次jstat统计
jstat -gc PID 1000 10
//结果打印
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
69888.0 69888.0 7798.8 0.0 559232.0 55689.8 349568.0 165270.2 93836.0 91655.6 11304.0 10839.4 2214 34.747 4 0.609 35.356
69888.0 69888.0 7798.8 0.0 559232.0 85044.0 349568.0 165270.2 93836.0 91655.6 11304.0 10839.4 2214 34.747 4 0.609 35.356
69888.0 69888.0 7798.8 0.0 559232.0 97263.3 349568.0 165270.2 93836.0 91655.6 11304.0 10839.4 2214 34.747 4 0.609 35.356
结果说明:
- S0C:这是From Survivor区的大小
- S1C:这是To Survivor区的大小
- S0U:这是From Survivor区当前使用的内存大小
- S1U:这是To Survivor区当前使用的内存大小
- EC:这是Eden区的大小
- EU:这是Eden区当前使用的内存大小
- OC:这是老年代的大小
- OU:这是老年代当前使用的内存大小
- MC:这是方法区(永久代、元数据区)的大小
- MU:这是方法区(永久代、元数据区)的当前使用的内存大小
- YGC:这是系统运行迄今为止的Young GC次数
- YGCT:这是Young GC的耗时
- FGC:这是系统运行迄今为止的Full GC次数
- FGCT:这是Full GC的耗时
- GCT:这是所有GC的总耗时
jmap、jhat
jmap -histo PID
按照各种对象占用内存空间的大小降序排列,把占用内存最多的对象放在最上面。
使用jmap生成堆内存转储快照
jmap -dump:live,format=b,file=dump.hprof PID
这个命令会在当前目录下生成一个dump.hrpof文件,这里是二进制的格式,不能直接打开看的,这是把这一时刻JVM堆内里所有对象的快照放到文件里去了,供后续去分析
使用jhat在浏览器中分析堆转出快照
jhat dump.hprof -port 7000
- jhat内置了web服务器,他会支持你通过浏览器来以图形化的方式分析堆转储快照
MAT
内存分析工具
使用
- 使用jmap命令导出一份线上系统的内存快照
- 启动MAT,Open a Heap Dump(如果dump内存出来很大,可以通过修改MemoryAnalyzer.ini文件中的-Xmx1024m配置)
JVM优化
原则:尽量让每次Young GC后的存活对象小于Survivor区域的50%,都留存在年轻代里。尽量别让对象进入老年代。尽量减少Full GC的频率,避免频繁Full GC对JVM性能的影响。 没有固定的参数调优,不同业务不同机器都会导致参数的不同
- 如果是超大的QPS
- 增加机器,尽量让每台机器承载更少的并发请求,减轻压力。
- 给年轻代的Survivor区域更大的内存空间,让每次Young GC后的存活对象务必停留在Survior中,别进入老年代。
- 老年代参数:-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction
- 含义:进行n次Full GC后会触发一次Compaction操作,也就是压缩操作(会把存活对象放到紧邻在一起,避免大量的内存碎片)
- 设置为0,每次Full GC后都压缩一次,避免内存碎片
- 定义好业务甚至小组的JVM模板,具体就看具体业务了
- 尽量给年轻代多一点,目的是让Survivor区域大一点,避免动态年龄规则使存活对象进入老年代
- 设置-XX:+CMSParallelInitialMarkEnabled:在CMS垃圾回收器的“初始标记”阶段开启多线程并发执行,主要是优化初始标记阶段的性能,尽量减少Stop the Word的时间
- -XX:+CMSScavengeBeforeRemark:在CMS的重新标记阶段之前,先尽量执行一次Young GC
- CMS的重新标记也是会Stop the World的,所以所以如果在重新标记之前,先执行一次Young GC,就会回收掉一些年轻代里没有人引用的对象。
- 所以如果先提前回收掉一些对象,那么在CMS的重新标记阶段就可以少扫描一些对象,此时就可以提升CMS的重新标记阶段的性能,减少他的耗时。 如果发生频繁的Full GC,要留意是不是下面这种情况
【Full GC(Metadata GC Threshold)xxxxx, xxxxx】
- 频繁Full GC不光是老年代触发的,有时候也会因为Metaspace区域的类太多而触发。
- 如果你在代码里大量用了类似上面的反射的东西,那么JVM就是会动态的去生成一些类放入Metaspace区域里的。
原因:
在JVM在发射过程中动态生成的类的Class对象,他们都是SoftReference软引用的。正常情况下是不会回收的,但是如果内存比较紧张的时候就会回收这些对象。是通过公式去判断是不是要回收:clock - timestamp <= freespace * SoftRefLRUPolicyMSPerMB。 - “clock - timestamp”代表了一个软引用对象他有多久没被访问过了
- freespace代表JVM中的空闲内存空间
- SoftRefLRUPolicyMSPerMB代表每一MB空闲内存空间可以允许SoftReference对象存活多久
JVM应该会随着反射代码的执行,动态的创建一些奇怪的类,他们的Class对象都是软引用的,正常情况下不会被回收,但是也不应该快速增长才对。但是实际上如果设置了SoftRefLRUPolicyMSPerMB = 0,那么任何软引用对象就可以尽快释放掉,不用留存,尽量给内存释放空间出来;这样就会导致所有的软引用对象,比如JVM生成的那些奇怪的Class对象,刚创建出来就可能被一次Young GC给带着立马回收掉一些。
优化手段:SoftRefLRUPolicyMSPerMB参数不要设置为0,可以设置为1000-5000毫秒
避免某种条件下,sql语句不带where,导致大内存直接进入老年代
- 在业务代码上,避免一些极端情况下SQL语句里不拼接where条件,务必要拼接上where条件,不允许查询表中全部数据。彻底解决那个时不时有大对象进入老年代的问题。 避免使用System.gc()
- 在平时普通项目中,不要显式在代码中使用该命令,同时可以使用参数-XX:+DisableExplicitGC,禁止显式执行GC
- 注意:在使用到netty框架的项目中,尽量不要使用参数-XX:+DisableExplicitGC,因为netty框架有某些地方显式使用了该命令
避免内存泄露
导致老年代存在了大量不需要使用的对象
OOM排查
线上环境发生OOM之后,首先保留线程,第一时间恢复,避免造成更大的影响
发生OOM的区域:
- Metaspace
- 栈内存
- 堆内存 Metaspace内存溢出情况
- 使用了默认参数,Metaspace内存只有几十M
- 动态生成类,代码中没有控制好 Metaspace内存一般设置为512MB 栈内存溢出情况
- 主要是递归操作,太深的递归或者递归没有出口,一般都是代码的bug导致的
- 一般栈内存设置为1MB 堆内存溢出情况
- 存在了过多的存活对象,导致GC无法回收出充足的内存放入更多的存活对象
生产案例
- 有尝试过使用消息中间件时,使用一个内存队列存放消费的消息,然后多线程消费;
- 这时候如果上游的消息持续暴增,线程来不及消费内存队列,内存队列越来越大,就会溢出
- 比较频繁GC,分析得知每一次GC,年轻代回收的对象都比较少,结合业务,内存队列的对象消费了就可以回收,可以调整JVM内存分配的对象;如JVM一共有8G内存,将7G分配给年轻代,比例为6:2:2,避免发生Minor GC时,Survivor区域不够存放存活对象或者超过百分之50%,导致存活对象直接进入了老年代
后记
对于JVM实战调优,可能我们大部分时间都用不上,但并不代表我们可以完全不了解,最起码真正发生时自己有思路,有话可说。我是新生代农民工L_Denny,下篇文章再见。