• 《深入理解Java虚拟机》(八) 记录一次OOM问题分析实战


    一、问题分析思路

    1.考虑一个问题现象,系统刚启动在访问量比较小的时候运行流畅,随着访问量提高,开始卡顿;

    2.可以先排查数据库问题,例如Oracle可以检查临时表空间,慢sql统计,索引等等因素,如果依然无法解决;

    3.最终再考虑JVM调优,正常情况下是不需要JVM调优的,个人以为,JVM调优更多是针对比较极端的情景的。

    4.为什么会系统卡顿? 结合个人见过的问题,一般都是堆上发生的GC,导致系统停顿时间过长,或者过于频繁进行GC,它们都会造成性能损耗,不用猜,此时一般都是在OOM问题的边缘来回试探了。


    5.OOM问题表现:


    内存泄露

           比如出现异常或者代码bug,某些对象产生后就不能被回收。已知:一般情况下,对象在新生代上分配,如果多次收集后依然存活则进入老年代(大对象会直接进入老年代)。不能被回收的对象,会不断占用老年代空间 最终,老年代内存空间占满,新生代对象需要晋升时无法请求到足够内存空间,进入Full GC环节。


    性能较差代码导致大量对象无法被快速回收

           该问题表现为,在堆内存空间还有剩余时,假设堆上限8G,只要还剩余一定空间时,例如:几百M、或者1G,手动触发Full GC,老年代就会被回收掉,但是放任不管,最终肯定会因为对象分配速度过快导致堆内存占满。


    PS *
    • Full GC (STW操作,所有用户线程停顿)
    • Minor GC/Young GC–目标是新生代的垃圾收集,新生代空间不足时触发,新生代中对象根据存活次数判定是否晋升
    • Major GC/Old GC–目标是老年代的垃圾收集
    • Mixed GC --目标是整个新生代和部分老年代的垃圾收集(只有G1垃圾收集器存在该行为)。

    其实很好理解:Full GC是较难触发的(可以自行了解Full GC触发条件),一般都是较为极端的情况才会触发Full GC;
    区别是:
    • full gc频率极低,且full gc 能回收掉大量资源,那么你是个好人,你可以走了;
    • full gc频率高,并且不能有效回收内存空间,那么可以证明你不对劲,要把你抓起来。一般会指向OOM问题

    理解为:我大意了,我没有闪,直到最后知道Full GC到来的我眼泪掉下来。。。。。
    PS* 此时或许确实有许多对象可以被回收,但是了解回收算法的人都应该需要知道:

    • G1的标记整理算法,需要额外有剩余空间才能进行对象的整理,如果堆空间占满了,那么能整理到哪里去呢?此时Full GC就代表败北,你尽管去进行Full GC,能成功算我输。。。
    • CMS收集器, 标记-清除算法,碎片化问题,如果无法找到足够的连续内存空间分配某个对象,那么就会一直进行full GC。。。。
    • 除了这俩,应该不会有人再用更古老的收集器了吧?如果有可以看我另一篇博客: 深入理解Java虚拟机》(二) GC 垃圾回收机制

    有如下对话为证
    • 堆:我真的一滴都没有了
    • 但是分配内存的请求就像磨人的小妖精表示:我不管,你给我去挤,你去找
    • 于是乎,上就会一遍又一遍进行垃圾回收以期待能挤出那么一滴内存空间
    • 但是此时的体量非常大,每次回收计算引用关系、对象存活信息会非常耗时
    • 表现为,系统时时刻刻都会进行非常耗时full gc (stop the work!!!)
    • 到了最后绝望的死在了xx的肚皮上

    二、主要问题概述以及分析

    上班高峰期间,系统使用一段时间后崩溃,报错内存溢出;

    1.相关操作

    打印系统运行GC日志,初步分析堆内存分代情况;jvisualvm实时观察系统运行过程中堆内存占用、活动线程数量曲线。

    2.主要问题现象

    1. GC日志结合jvisualvm,发现当活动线程数量开始飙升,老年代内存占用飙升;
    2. 当堆内存空间尚未占满时,手动点击垃圾回收,老年代能被回收,不再出现内存溢出现象

    在这里插入图片描述

    在这里插入图片描述

    3.初步分析问题

    • 结合现象推测,某些操作耗时且频繁,导致老年代扩充速度快于堆内存回收速度,最终 内存溢出。
    • 处理:使用堆快照分析工具Jprofile查找问题位置,并处理;

    三、相关工具介绍

    四、实际问题快照分析

    PS * 由于是公司环境,有些日志就不能提供了

    1.通过Memory查看老年代内存占用情况

    在这里插入图片描述

    观察老年代曲线,如果该曲线持续攀升,则观察堆快照。

    2.选择Live Memory 视图,

    1. 选择Recorded Objects 观察堆内存活对象
      在这里插入图片描述

    2. 点击Mark Current Values记录某一时刻存活的所有对象,并与实时存活兑现数量对比
      在这里插入图片描述

    3. 根据size关键字,找到堆内占据最多内存空间的对象,或者多次GC操作后,实例数
      不减反增的对象;

    3.生成堆快照,分析对象信息

    在这里插入图片描述

    1. 以String为例,当前快照对比上一步记录的对象实例数,增加了36767个String类对象
      在这里插入图片描述

    2. Use -> selected instance,根据如图方式追踪对象的产生
      在这里插入图片描述

    3. 选择Allocations 视图追踪相关方法执行堆栈信息
      在这里插入图片描述

    最终分析该方法代码,定位问题。

    五、代码逻辑问题

    经过分析堆快照,发现问题:

    1. 系统里有一个提供给第三方终端设备使用的接口性能较差,查询非常耗时,且对方调用频率过于频繁,最终通过使用缓存技术解决了这个问题。

    2. 经过排查发现一个历史代码遗留的BUG,某些情景下方法入参为空时,导致了一个SQL查整表。

    六、性能问题

    1.问题现象

    处理完上述问题后,项目上又一次反馈系统卡顿;如图:内存占用较低,不存在内存即将溢出问题,经过排查,堆内也未发现异常对象
    在这里插入图片描述

    然后,留意到垃圾回收曲线已经变成波浪线,再次查看GC日志发现:

    1. eden区满,频繁新生代GC
    2. 新生代共计不足500M内存占用,回收耗时超过一秒
    3. 垃圾回收过程中Ref Proc,处理各种引用耗时近似垃圾回收耗时
      在这里插入图片描述
      (模拟日志,体现Ref Proc耗时情况)

    2.问题处理方法

    (G1垃圾收集器)

    	-XX:+ParallelRefProcEnabled 开启并行处理各种引用    
    	-XX:ParallelGCThreads=8 并行垃圾回收处理线程数
    	-XX:ConcGCThreads=3 并发标记线程数 ,约等于 ParallelGCThreads / 4 
    

    假设,cpu核心数 = X
    在这里插入图片描述

    3.处理结果

    1. 处理后GC曲线:
      在这里插入图片描述

    2. JVM运行状况:
      平均15分钟内2~3 次新生代GC,每次GC停顿时间低于10 毫秒

  • 相关阅读:
    Javascript之内置对象
    Javascript之匿名函数(私有变量)
    Web前端开发修炼之道 (2)
    11.标志寄存器
    10.CALL和RET指令
    Javascript之继承(其他方式)
    Javascript之Function类型
    5.[BX]和Loop指令
    JavaScript之执行环境及作用域
    Javascript之BOM(window对象)
  • 原文地址:https://www.cnblogs.com/bokers/p/14903041.html
Copyright © 2020-2023  润新知