一次极好的JVM日常实践分析
首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜 文章合集 : 🎁 juejin.cn/post/694164… Github : 👉 github.com/black-ant CASE 备份 : 👉 gitee.com/antblack/ca…
一. 前言
JVM 的概率背了不少,但是对于一个系统来说,出现JVM问题通常都是早期刚搭建时,或者代码写错了导致各种问题的出现。
一旦代码写的稳了,系统成熟了,反而很少碰到这类问题,陆陆续续也忘了不少。
这一次属于修改配置后发现JVM变化比较明显,想一想后也大概明白了原因,但是却是一次难得的机会去巩固一下各种概念。
二. 重新理解分代
2.1 概念复习
要做一个简单的JVM 分析,我们只需要知道这些概念 :
堆内存是什么
- 堆内存用于存储对象实例和数组
- 堆内存在程序运行时动态分配和扩展,但是可以通过 Xms 和 Xmx 进行初始分配
- 垃圾回收会对堆内存的对象进行管理
堆内存的垃圾回收
- 当JVM中的堆内存空间不足时,可能会触发垃圾回收
- 当某个分代的内存不足时,也可能触发垃圾回收
- 当系统处于空闲状态时,JVM可能会选择在此时触发垃圾回收
并行和并发的区别
- 并行 : 利用多个线程并行地执行垃圾回收操作,并行垃圾回收需要暂停应用程序的执行,因为它需要对整个堆进行扫描和处理。 并行的垃圾回收器吞吐更快
- 并发 : 在应用程序运行的同时进行垃圾回收,可以在不停止应用程序的情况下,与应用程序并发地执行可以在不停止应用程序的情况下,与应用程序并发地执行
并发的目标是尽量减少应用程序的停顿时间,以提高系统的响应性能、
FullGC 和 YoungGC 的区别
// Young GC
- 针对Java堆中的年轻代进行的垃圾回收操作
// Full GC
- 对整个Java堆,包括年轻代和老年代,进行的垃圾回收操作
- 耗时长,需要暂停应用的执行,会导致较长的停顿时间
2.2 从结果看过程 :运行异常的系统
以下是监控上截下来的一些图 :
从这个图可以明显看到前后2个分段:
- 老年代基本上占了使用总和
- 年轻代Survivor区和 年轻代Eden区 几乎为空
而随着重启,老年代清空,新生代基本上正常》》》
下面结合一个 GC 图来看具体的原因 :
- 首先 : 明显可以看到,老年代占用非常多,基本上等于全部内存
- 所以 : 当有新的对象创建的时候,由于老年代已经满了,无法接纳更多的对象,年轻代就无法被复制到老年代,从而频繁的发生youngGC
而这种变化也可以通过下面的具体变化图可以看到>>>>>>
以上是一个时间周期更短的图,包含了20个小时的分代情况,可以看到 :
- 修改前youngGC非常频繁,但是效果很差,基本上没什么变化
- 修改后youngGC变少了,但是内存回收的效果反而更好了
结论
系统中存在异常,或者没有正确的配置JVM最大堆内 , 导致老年代一直增多而没有被正确的回收,导致年轻代频繁的发生youngGC.
基于这个思路,把时间周期拉长,就可以看到一个更清晰的变化曲线了 :
三. 分析问题的原因
所以,老年代越来越多,是问题的根源。其实结论也很容易得到,就是代码和配置的问题。
但是既然要学,就把中间的弯弯绕绕全部理清楚。
3.1 概念储备
问题一 : 老年代什么环节被回收?
- 老年代是在FullGC环节或者老年代内存空间不足时被回收的
- FullGC 会对整个内存进行回收,包括年轻和老年代
- 老年代通常采用标记-清除(Mark-Sweep)或标记-整理(Mark-Compact)的垃圾回收算法
问题二 :那么老年代为什么不被回收?
- 对象仍然被引用 :被其他代码引用的对象是不能被垃圾回收给正确回收的,类似的还是长生命周期的对象
- 未满足GC策略 : 如果条件未满足或策略未触发,老年代也不会被回收
问题三 : 老年代要是满了会怎样
- 内存泄漏 : 不同于内存溢出,内存泄漏指无效对象仍然被引用,最终导致内存溢出
- 内存溢出 : OutOfMemoryError ,新生代和老年代都无法继续分配内存
- 系统性能下降 : 系统性能下降的根源是如果内存满了,会频繁的进行 FullGC , Stop the world 每次都会导致系统暂停,同时影响系统的吞吐量
3.2 再看问题
- S1 : 可以看到,fullGC 很少发生 = 说明还没到内存边界,并没有触发 fullGC
- S2 : 老年代逐渐变多 = 说明代码中确实存在问题
- S3 : 当老年代占用较少时,新生代的波动非常大 = 说明配置有问题,没有为应用配置合理的内存空间
基于上述得到的三个结论,开始对代码和配置进行梳理
上道具 : Arthas 好东西
用 Arthas Dump 下载后,执行分析后不难发现端倪 :
ConcurrentHashMap 和 地下的 ServiceImpl 占用空间都太大了,往里面翻一下代码就发现了问题 : HashMap 内的数据一直在插入,没有进行 remove 操作,而Map又还有应用,导致永久代一直增加。
3.3 那么正常的结果是什么样的
首先,年轻代会占大多数,永久代占一小部分,所以年轻代的波动和总理差不多。
其次由于GC,就会使年轻代一次次被回收一部分,形成波动
而随着老年代增加,则年轻代和总量就会逐渐分开,形成上下的结构。
总结
如果有相关的经验,其实这样的图像一眼就能看出问题,同时大概也能猜出原因。
上文的3种原因基本上在开始就猜测出来了,后面分析Dump 也是在往那个方向猜测,最后得出的结论也差不多。
补充 :
常见引起内存溢出的原因
- 分配的对象过大,超过了可用内存的限制。
- 内存泄漏导致无效对象占用了大量内存空间。
- 程序中存在死循环或递归调用,导致内存消耗过快。
- 虚拟机参数配置不当,导致内存不足
转载自:https://juejin.cn/post/7241496984175984696