目录
java 应用
1 CPU 负载过高
1.1 分析问题
1.2 解决方案
2 内存占用过多
2.1 从内存回收方面
2.2 从代码层面
java 应用
1 CPU 负载过高
1.1 分析问题
首先我们通过 top 命令进行分析, 找出消耗最多 CPU 的 java 进程 id .
找出对应的进程 id 后, 我们可以通过 top -Hp 进程 id 命令来找出该进程中占用 CPU 最多的前几个线程 id.
我们使用 jstack -l 进程 pid> /tmp/java_pid.log 输出 java 的堆栈日志到文件 /tmp/java_pid.log.
我们将刚刚查询到的 java 进程中占用 CPU 最多的前几个线程 id. 进行转化为 16 进制.
printf "%X" 线程 id
我们在 java 堆栈日志文件中找到上面转化为 16 进制的线程的 pid 对应的 日志.
实际操作步骤流程图:
补充: 有时可能是我们代码创建线程过多导致的问题:
- # 查看该进程有多少线程
- ps p 9534 -L -o pcpu,pmem,pid,tid,time,tname,cmd|wc -l
1.2 解决方案
我们把对应的线程 id 的日志拿给我们的开发, 进行定位错误, 这里容易定位出的错误是:
线程处于 WAITING(等待状态)
线程 BLOCKED(阻塞)
我可以把定位到代码位置, 告诉开发, 让开发查看对应的代码是否有问题.
2 内存占用过多
Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分成若干个不同的数据区域.
这些组成部分一些是线程私有的, 其他的则是线程共享的.
线程私有的:
程序计数器
虚拟机栈
本地方法栈
线程共享的:
堆
方法区
直接内存
2.1 从内存回收方面
Java 堆是垃圾收集器管理的主要区域, 因此也被称作 GC 堆(Garbage Collected Heap). 从垃圾回收的角度, 由于现在收集器基本都采用分代垃圾收集算法, 所以 Java 堆还可以细分为: 新生代和老年代: 再细致一点有: Eden 空间, From Survivor,To Survivor 空间等. 进一步划分的目的是更好地回收内存, 或者更快地分配内存.
在 JDK 1.8 中移除整个永久代, 取而代之的是一个叫元空间 (Metaspace) 的区域(永久代使用的是 JVM 的堆内存空间, 而元空间使用的是物理内存, 直接受到本机的物理内存限制). 关于 metaspace 的详细讲解看: JVM 源码分析之 Metaspace 解密 https://mp.weixin.qq.com/s/SsXbRvtvawKDHstFpU4uog
java 实际的内存使用是这样的, 大多数情况下, 对象在新生代中 eden 区分配. 当 eden 区没有足够空间进行分配时, 虚拟机将发起一次 Minor GC(新生代 GC). 将 eden 区的一些存活对象移动到 Survivor 区, 当 Survivor 区的大小, 不够储存 eden 区的存活对象时, 那么就会将它移动到老年区(Old Generation ), 当老年区满了时候将触发一次 Full GC .
在实际工作中, 我们可以使用 jmap -heap pid 来查看当前的进程的 java 堆的分布情况.
- [root@iz23nb5ujp69 ~]# jmap -heap 11764
- Attaching to process ID 11764, please wait...
- Debugger attached successfully.
- Server compiler detected.
- JVM version is 25.73-b02
- using thread-local object allocation.
- Parallel GC with 2 thread(s)
- Heap Configuration:
- MinHeapFreeRatio = 0 #GC 后, 如果发现空闲堆内存占到整个预估堆内存的 40%, 则放大堆内存的预估最大值, 但不超过固定最大值. 默认该值是 40
- MaxHeapFreeRatio = 100 #GC 后, 如果发现空闲堆内存占到整个预估堆内存的 100%, 则收缩堆内存预估最大值. 默认的是 70
- MaxHeapSize = 2147483648 (2048.0MB) # 最大的堆内存
- NewSize = 715653120 (682.5MB) # 新生代初始大小
- MaxNewSize = 715653120 (682.5MB) # 新生代最大大小
- OldSize = 1431830528 (1365.5MB) #老年代
- NewRatio = 2 # 新生代和老年代的 内存比例: 1:2 默认值
- SurvivorRatio = 8 # Eden 区与 Survivor 区的大小比值, 设置为 8, 则两个 Survivor 区与一个 Eden 区的比值为 2:8, 一个 Survivor 区占整个年轻代的 1/10,Eden 区和 Survivor 区 的实际比例值是会变动的
- MetaspaceSize = 21807104 (20.796875MB) # 元空间大小
- CompressedClassSpaceSize = 1073741824 (1024.0MB) # 压缩时可用的最大的内存
- MaxMetaspaceSize = 17592186044415 MB #元空间可用最大大小
- G1HeapRegionSize = 0 (0.0MB) #G1 收集器的内存大小
- Heap Usage:
- PS Young Generation # 新生代
- Eden Space: # Eden
- capacity = 372768768 (355.5MB)
- used = 185979712 (177.36407470703125MB)
- free = 186789056 (178.13592529296875MB)
- 49.89144154909459% used # eden 可用区使用率, 该值满了将触发 young gc
- From Space: # Survivor1
- capacity = 175112192 (167.0MB)
- used = 47983120 (45.76026916503906MB)
- free = 127129072 (121.23973083496094MB)
- 27.401358781460516% used
- To Space: # Survivor2
- capacity = 167772160 (160.0MB)
- used = 0 (0.0MB)
- free = 167772160 (160.0MB)
- 0.0% used
- PS Old Generation # 老年代
- capacity = 1431830528 (1365.5MB)
- used = 257274632 (245.35620880126953MB)
- free = 1174555896 (1120.1437911987305MB)
- 17.9682320616089% used # 老年代 可用区使用率, 该值满了将触发 full gc
适当的 young gc 可以让清理一些不存活的对象, 但是短时间大量的 young GC 是会导致 Full GC 的, 那么 Full gc 是尽量不要产生的, 当一个应用, 产生大量的 full GC 是不正常的, 过多的 GC 和 Full GC 是会占用很多的系统资源(主要是 CPU), 影响系统的吞吐量.
young gc:
Metadata GC
full gc :
GC 日志解析
对了, 如何在日志中打印 GC 日志, 我们在后面的配置中会讲到.
- [GC (Allocation Failure) [DefNew: 279616K->19156K(314560K), 0.0595827 secs] 279616K->19256K(1013632K), 0.0601044 secs] [Times: user=0.03 sys=0.02, real=0.06 secs]
- GC:
表明进行了一次垃圾回收, 前面没有 Full 修饰, 表明这是一次 Minor GC.
Allocation Failure:
表明本次引起 GC 的原因是因为在年轻代中没有足够的空间能够存储新的数据了.
279616K->19156K(314560K) 260460
三个参数分别为: GC 前该内存区域 (这里是年轻代) 使用容量, GC 后该内存区域使用容量, 该内存区域总容量.
0.0595827 secs
表示 GC 耗时
279616K->19256K(1013632K)
堆区垃圾回收前的大小, 堆区垃圾回收后的大小, 堆区总大小.
- 0.0071945 secs
- Times: user=0.01 sys=0.00, real=0.01 secs
分别表示用户态耗时, 内核态耗时和总耗时
新生代清理的内存: 279616 - 19156 = 260460k
堆区减少的内存: 279616 - 19256 = 260360k
新生代存到老年代的 数据为 260460k - 260360k
那么如何查询一个应用发生 young Gc 和 Full GC 的次数和耗时时间. 我们可以使用 jstat .
jstat 查询 GC 次数和 Full Gc 次数
- jstat -gcutil pid 2000 10 (每隔 2 秒输出一次结果, 输出 10 次)
- [root@iz23nb5ujp69 ~]# jstat -gcutil 3626 2000 10
- S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
- 56.01 0.00 8.21 88.19 98.44 96.92 27350 353.229 41 32.416 385.645
- 56.01 0.00 8.30 88.19 98.44 96.92 27350 353.229 41 32.416 385.645
- 56.01 0.00 8.41 88.19 98.44 96.92 27350 353.229 41 32.416 385.645
- 56.01 0.00 8.56 88.19 98.44 96.92 27350 353.229 41 32.416 385.645
- 56.01 0.00 9.00 88.19 98.44 96.92 27350 353.229 41 32.416 385.645
- 56.01 0.00 9.27 88.19 98.44 96.92 27350 353.229 41 32.416 385.645
- 56.01 0.00 9.34 88.19 98.44 96.92 27350 353.229 41 32.416 385.645
- 56.01 0.00 9.46 88.19 98.44 96.92 27350 353.229 41 32.416 385.645
- 56.01 0.00 9.57 88.19 98.44 96.92 27350 353.229 41 32.416 385.645
- 56.01 0.00 9.70 88.19 98.44 96.92 27350 353.229 41 32.416 385.645
S0 - Heap 上的 Survivor space 0 区已使用空间的百分比
S1 - Heap 上的 Survivor space 1 区已使用空间的百分比
E - Heap 上的 Eden space 区已使用空间的百分比
O - Heap 上的 Old space 区已使用空间的百分比
M - 表示的是 Klass Metaspace 以及 NoKlass Metaspace 两者总共的使用率
CSS - 表示的是 NoKlass Metaspace 的使用率
YGC - 从应用程序启动到采样时发生 Young GC 的次数
YGCT- 从应用程序启动到采样时 Young GC 所用的时间(单位秒)
FGC - 从应用程序启动到采样时发生 Full GC 的次数
FGCT- 从应用程序启动到采样时 Full GC 所用的时间(单位秒)
GCT - 从应用程序启动到采样时用于垃圾回收的总时间(单位秒)
FGC Scavenge GC 要慢, 因此应该尽可能减少 Full GC.
导致 young Gc 和 full GC 的原因有哪些:
young 可用区 设置的太小 ,young gc 设置的太小就会导致 , 多次 young gc, 多次 young gc 也就导致 oldGeneration 不断增大, 最终导致 full gc
Old Generation 设置的太小, 当 Old Generation 太小的话就会导致 经常占满, 然后会进行 full GC .
System.gc()被显示调用 , 垃圾回收不要手动触发, 尽量依靠 JVM 自身的机制.
Meta(元数据)区可用内存设置的太少.
jvm 默认使用的配置
我们拿我们的 tomcat 应用来说, 我们如果使用默认的配置, 我们使用 jmap -heap 线程 查看
- [root@www apache-tomcat-8.5.38]# jmap -heap 7568
- Attaching to process ID 7568, please wait...
- Debugger attached successfully.
- Server compiler detected.
- JVM version is 25.131-b11
- using thread-local object allocation.
- Mark Sweep Compact GC
- Heap Configuration:
- MinHeapFreeRatio = 40 # 这个默认值在上面已经说到了, 对于 heap, 我们尽量不要让它自动调整
- MaxHeapFreeRatio = 70 # 这个默认值在上面已经说到了, 对于 heap, 我们尽量不要让它自动调整
- MaxHeapSize = 480247808 (458.0MB)
- NewSize = 10485760 (10.0MB)
- MaxNewSize = 160038912 (152.625MB)
- OldSize = 20971520 (20.0MB) # 默认的值分配不合理
- NewRatio = 2
- SurvivorRatio = 8
- MetaspaceSize = 21807104 (20.796875MB)
- CompressedClassSpaceSize = 1073741824 (1024.0MB)
- MaxMetaspaceSize = 17592186044415 MB
- G1HeapRegionSize = 0 (0.0MB)
上面的配置很大一部分是不合理的, 对于线上应用来说.
JVM 配置参数(根据自己的项目情况调整)
- -Xms2048m # 堆的最小内存, 建议和最大内存设置的一致, 以避免每次垃圾回收完成后 JVM 重新分配内存, 提高 GC 运行的效率.
- -Xmx2048m # 堆的最大内存, 建议和最小内存设置的一致, 以避免每次垃圾回收完成后 JVM 重新分配内存, 提高 GC 运行的效率.
- -XX:MaxHeapFreeRatio=100 #GC 后, 如果发现空闲堆内存占到整个预估堆内存的 100%, 则收缩堆内存预估最大值. 默认的是 70
- -XX:MinHeapFreeRatio=0 #GC 后, 如果发现空闲堆内存占到整个预估堆内存的 0%, 则放大堆内存的预估最大值, 但不超过固定最大值. 默认该值是 40
- -Xmn900m #设置新生代的内存, 如果我们设置了 Xmx 和 Xms 为一致的话, 那么该值的默认值为 Xmx 值的 1/3.
- -XX:MetaspaceSize=64M # 初始化的 Metaspace 大小, 也是最小大小 java 8 后, 用 Meta 代替了永久代, 默认该值为 20M(因系统而异), 如果日志中出现了 Meta GC, 那么可以提高该值.
- -XX:MaxMetaspaceSize= # 这个参数用于限制 Metaspace 增长的上限, 防止因为某些情况导致 Metaspace 无限的使用本地内存, 影响到其他程序.
- -XX:MinMetaspaceFreeRatio=40 #GC 后, 如果发现空闲 Meta 内存占到整个预估 Meta 内存的 40%, 则放大 Meta 内存的预估最大值, 但不超过固定最大值. 默认该值是 40
- -XX:MaxMetaspaceFreeRatio=70 #GC 后, 如果发现空闲 Meta 内存占到整个预估 Meta 内存的 70%, 则收缩 Meta 内存预估最大值. 默认的是 70
- -XX:MaxNewSize= #设置新生代的最大值, 一般默认为整个堆的 1/3
- -XX:NewRatio=N #设置新生代和老年代的比值, 默认为 2 表示 新生代占用 1/3
- -XX:SurvivorRatio=N #设置新生代中的 Eden 和两个 Survivor 的比值, 默认为 8 表示, eden 占用 8/10, 时间中 jvm 会自动调整 Eden 和 Survivor 的值的.
- -XX:MaxTenuringThreshold=N #新生代的对象的年龄 (年龄计数器) 达到 N 值后移动到老年代, 默认值为 15
- -XX:ParallelGCThreads=n #设置垃圾收集器在并行阶段使用的线程数, 建议设置为与处理器数目相等
- -XX:+DisableExplicitGC #关闭 System.gc() 看你的程序是否需要 System.gc(), 再来决定
- -XX:MaxDirectMemorySize # 来指定最大的堆外内存
选用 GC 回收器, 不同的回收器, 对应的延迟和内存不一致
- ## 针对 Meta 细化设置
- -XX:CompressedClassSpaceSize #这个参数主要是设置 Klass Metaspace 的大小, 不过这个参数设置了也不一定起作用, 前提是能开启压缩指针, 假如 - Xmx 超过了 32G, 压缩指针是开启不来的. 如果有 Klass Metaspace, 那这块内存是和 Heap 连着的.
- ## 垃圾收集器的选择
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU).CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验, 减少回收的停顿时间),CMS GC 算法主要是针对老生代, 持久代. 所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值. 还有 G1 收集器是(java1.9 的默认收集器).jdk1.8 默认垃圾收集器 Parallel Scavenge(新生代)+Parallel Old(老年代)
如果要使用 CMS 收集器的话,
- -XX:+UseConcMarkSweepGC # 开启 CMS GC 垃圾收集器
- -XX:+UseCMSInitiatingOccupancyOnly #只有开启了这个参数, CMSInitiatingOccupancyFraction 这个参数才会生效
- -XX:CMSInitiatingOccupancyFraction= #触发 cms gc 的老生代使用率, 当老生代使用达到该阈值之后, 就将触发 GC, 该参数必须配合 UseCMSInitiatingOccupancyOnly 使用才有效
- -XX:+CMSClassUnloadingEnabled/-XX:-CMSClassUnloadingEnabled # 在使用 CMS 时, 是否开启类卸载 如果开启 在 full gc 是会顺带扫描 metaSpace/PermGen
- -XX:+ParallelRefProcEnabled # 尽量开启并行处理在任何地方
- -XX:+CMSScavengeBeforeRemark #开启在 cms gc remark 之前做一次 ygc, 减少 gc roots 扫描的对象数, 从而提高 remark 的效率
其它参数
- -Xss256k # 每个线程的堆栈大小, Xss 越大, 每个线程的大小就越大, 占用的内存越多, 能容纳的线程就越少
- #Xss 越小, 则递归的深度越小, 容易出现栈溢出 java.lang.StackOverflowError, 减少局部变量的声明, 可以节省栈帧大小, 增加调用深度
- -XX:+PrintGCDetails # 日志中输入 GC 详情日志.
- -XX:+PrintHeapAtGC # 打印 GC 前后的详细堆栈信息
- -XX:+PrintGCTimeStamps # 打印 GC 发生的时间戳
- -XX:+PrintGCDateStamps # 输出 GC 的时间戳(以日期的形式, 如 2013-05-04T21:53:59.234+0800)
- # 指定 GC 日志和错误日志, OOM
- -XX:ErrorFile=/tmp/gc/hs_err_pid%p.log # 发生错误时错误日志保存的地方
- -Xloggc:/tmp/gc/gc.log # gc 日志记录的地方
- -XX:+HeapDumpOnOutOfMemoryError #启用当抛出 OutOfMemoryError 异常时, 将堆转储到文件
- -XX:HeapDumpPath=/tmp/gc #当启用 HeapDumpOnOutOfMemoryError 时, 储存 dump 文件的路径
- -XX:+PrintGCApplicationStoppedTime # 启用打印应用暂停的时间
- -XX:+TraceClassLoading # 记录你的类到底是从哪个文件加载进来的
还有一些参数见文章: https://blog.csdn.net/see__you__again/article/details/51998038
希望达成的: young gc 频率适中, 如果 young gc 次数较少的话, 一次 young gc 的耗时就会比较长, 那么最求的平衡就是: young gc 频率和 young gc 耗时 达到两者的平衡值. Full Gc 尽量不要有.
参考配置
-Xms2048m -Xmx2048m -XX:MaxHeapFreeRatio=100 -XX:MinHeapFreeRatio=0 -Xmn900m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=256m -XX:MinMetaspaceFreeRatio=0 -XX:SurvivorRatio=7 -XX:MaxMetaspaceFreeRatio=100 -XX:MaxTenuringThreshold=14 -XX:ParallelGCThreads=2 -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+PrintGCDetails
调优完之后的效果是 ,20-30 分钟发生一次 Minor GC(young gc ) 每次的 GC 耗时, 无 Full GC 和 Meta 导致的 GC.
案例分析:
GC 日志
- [GC (Metadata GC Threshold) [PSYoungGen: 282234K->101389K(523264K)] 589068K->410894K(1921536K), 0.1611903 secs] [Times: user=0.18 sys=0.00, real=0.16 secs]
- [Full GC (Metadata GC Threshold) [PSYoungGen: 101389K->0K(523264K)] [ParOldGen: 309505K->258194K(1398272K)] 410894K->258194K(1921536K), [Metaspace: 268611K->268101K(1294336K)], 1.8562117 secs] [Times: user=1.80 sys=0.08, real=1.86 secs]
我们可以在日志中看到触发了一次普通的 GC 和一次 Full GC , 两次 GC 的原因都是 Meta 区 GC 导致的, 我们看 Full Gc 的日志, young 区的内存没有使用完, old 区的内存也没有占用满, 只有 Meta 区的内存占用满了, 那么导致这个问题的就是 Meta 区设置的太小.
扩展:
查看 java 的一些默认配置
java -XX:+PrintFlagsInitial
示例: 查看 Meta(元空间的默认大小)
java -XX:+PrintFlagsInitial |grep MetaspaceSize
2.2 从代码层面
从代码层面的话, 我们就需要分析出 每个程序的内存分布情况, 每个类的实例数, 占用内存最大的类, 和他们活动的时长, 是否有内存泄漏啊, 内存泄漏的疑点.
内存泄漏: 对象已经死了, 无法通过垃圾收集器进行自动回收, 无法释放内存.
内存溢出: 程序在申请内存时, 没有足够的内存空间供其使用.
1 生成堆
分析的前提是我们需要拿到分析的数据:
jmap -dump:live,format=b,file=/tmp/dump1628.dat pid
2 下载并安装 MAT
然后我们可以使用 MAT 分析工具进行分析堆文件.
MAT 下载链接 : http://www.eclipse.org/mat/ (需要 java 环境)
3 将堆信息导入到 MAT 进行分析
内容借鉴于: https://www.cnblogs.com/AloneSword/p/3821569.html
Histogram: 列出内存中的对象, 对象的个数以及大小.
Dominator Tree: 可以列出那个线程, 以及线程下面的那些对象占用的空间.
Top Consumers: 以图形的形式列出最大的 object
Duplicate Classes: 列出一个类被多个类加载引用的类名
Leak Suspects: 包含疑似内存泄漏和系统概述的报告
Top Components: 列出占用超过总堆 1% 的组件
关于每一个分类的详情见博客: https://www.cnblogs.com/AloneSword/p/3821569.html
关于内存的调优, 我们还可以使用工具 JConsole 和 Java VisualVM. 等等
来源: https://www.cnblogs.com/operationhome/p/10537018.html