前面的10篇 都是基础的知识,包括类加载的过程 类加载的细节,jvm内存模型 垃圾回收 等等,
这一篇我们开始实战了解一下 各种疑难杂症:怎么监控 怎么发现 怎么解决
内存溢出 内存泄漏
这两个概念在垃圾回收器里面已经讲过了,
当一个对象我们已经不再使用 但是我们又没有明确的断掉它 可达性分析法觉得他不能回收 一直存在堆内存里面——这就是内存溢出
随着内存溢出 越来越多 最后吧内存撑爆 造成OOM ——这就是内存泄漏
下面介绍一款企业级的JVM监控 :Prometheus 负责收集监控数据 grafana负责可视化显示
首先当我们看到内存趋势图 要会分辨,内存图频繁上升是正常的 因为程序的运行过程就会创建对象,但是一直上升下不来 这就是有问题的 说明有对象没回收 然后越积越多
JVM内存问题经典案例
先来一个最经典的,大家学习线程池的时候 书或者博客资料上面 都会有这个例子;
就是 线程池中使用 threadlocal
我们知道 threadlocal里面存的是本地线程的副本,当一个线程销毁时 它对应的threadlocal的副本数据 也就销毁了,
但是线程池是一个例外 我们讲过 为了节省线程 不断 新建和销毁的成本 所以设计了线程池,线程池里面核心线程都是活的, 如果我们不手动执行 remove, 那么threadlocal 就会一直占用堆内存
如果遇到大数据量和高数量的线程池 就有可能造成OOM
并发请求量过大 这个很好理解 也很常见,一个系统的并发请求量是有限的,如果有大量的用户短时间内同时访问 访问过程中大量创建对象 就很容易造成 堆内存溢出 OOM.
过多的线程创建
这种情况也会有,不管是线程池的参数设置,还是程序中某些设计里面 手动创建了大量的线程
,每个线程都占用一定的内存,可能会导致内存溢出。
超大对象 或者大文件IO 读写
这种情况也很多 ,比如你的对象里面有那种text 大字段 , 你select的时候 一次性select几万条数据出来 可能瞬间就把内存撑爆了。
还有大文件读写,比如你的web 写了一个文件下载功能,但是你没有限制文件下载的大小,用户直接下一个超级大的文件,jvm也会oom
mq的消息补偿机制是什么 是不是消费者消费失败 还可以再拉取一次消息
怎么样定位内存问题
接下来说最最重要的, 我们上面说了 内存溢出的几个案例 和原理, 那么线上系统当我们真正遇到内存溢出的时候,我们怎样去定位?
我们平时生活中生病去查病因的时候 回去医院拍个片子,这个片子上面记录的就是我们身体状况的一个快照。
同样线上服务出现了oom 或者其他内存问题的时候 我们也要拍一个片子 把内存溢出那个时间点的 快照拿到 去观察下 内存的情况
这个快照就是heap profile文件, 这个文件怎么生成呢?
一种方式是配置参数
这样如果发生了oom 系统就回自动的输出快照文件到你的指定目录
但是这种方式比较被动,如果系统发生了内存问题 内存占用量比较大,但是还没破那个店 没有oom 这时候你迫不及待就想分析解决 那么你就主动生成这个文件:
看 arthas 又出现了! 很方便
heap profile文件里面记录的就是内存的详细情况。
然后快照文件是导出来了,怎么看它呢? 你直接txt打开吗? 肯定是不行的
有2种打开方式 一种是MAT软件打开 (这种不太方便以为线上机器 安装MAT 还有再导出结果 不是很顺手)
第二种方式就是线上诊断系统,现在很多企业都会有自己自研的或者包装的(也可能他们的诊断系统内核就是MAT)诊断系统 你只需要把快照文件上传。
然后当你用正确的工具打开它时 看直方图:
这里看看深堆大小排序 排在最前面的 就是占用内存最大的对象
但是仅仅看直方图还不够,我们希望精确的找到 是哪一个接口方法 造成了内存问题:
我们打开直方图 旁边那个按钮 支配树, 然后我们找到占用内存排序顶端的这个线程
你看这个名字 http-nio-8081 一看就是Tomcat创建的用户线程。
然后接下来重点来了 下面这么多方法 我们应该看哪一个
这时候回忆起我们之前学过的基础的 springmvc中 有一个核心方法 HandlerMethod 就是下面红框箭头标出来的那个
是他负责把请求对象 发到 对应的控制器方法。
所以我们点开它 ,然后最后一栏描述中 就清清楚楚的表明了 是哪一个接口方法造成了 内存溢出: