JVM内存调优
java 应用的内存优化一部分在jvm的调优,本篇主要讲述 内存溢出的问题定位和问题分析
相关的的方式方法。
详细的 jvm部分优化可以参考文章: JVM 相关汇总
内存泄漏
内存泄漏(memory leak):在Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上,这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。
少量的内存泄漏可以容忍,但是如果发生持续的内存泄漏,就像滚雪球雪球越滚越大,不管有多大的内存迟早会被消耗完,最终导致的结果就是内存溢出。但是产生内存溢出并不是只有内存泄漏这一种原因。
内存泄漏绝大多数情况都是由堆内存泄漏引起的,没有特别说明则讨论的都是堆内存泄漏。
常见的复现场景
- java后端应用处理完请求后没有删除用户相关数据,导致随着访问量增大内存占满后OOM;
- 一些离线的调度任务(Elastic-job、Quartz)
解决内存溢出问题
1. 排查和发现问题
常用工具
visualVM 可视化的jvm内存分析工具,随jdk一起;
- 优点:功能丰富,实时监控CPU、 内存、线程等详细信息
- 缺点:
-适用于开发、测试环境,不适用于生产环境,VisualVM部分功能会停止所有的线程,这会影响到用户的使用
-对大量集群化部署的Java进程需要手动进行管理
Arthas
Arthas 是一款阿里开源的线上监控诊断产品,通过全局视角实时查看应用 load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断, 包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。arthas (aliyun.com)优点:
- 功能强大,不止于监控基础的信息,还能监控单个方法的执行耗时等细节内容。
- 支持应用的集群管理,使用阿里arthas tunnel可以管理所有的需要监控的程序
- 开发人员可以在线解决生产问题。无需 JVM 重启,无需代码更改。 Arthas 作为观察者永远不会暂停正在运行的线程。
Prometheus + Grafana
Prometheus+Grafana是企业中运维常用的监控方案,其中Prometheus用来采集系统或者应用的相关数据,同时具备告警功能。Grafana可以将Prometheus采集到的数据以可视化的方式进行展示。
优点:
- 支持系统级别和应用级别的监控,比如linux操作系统、Redis、MySQL、Java进程
- 支持告警并允许自定义告警指标,通过邮件、短信等方式尽早通知相关人员进行处理
** 内存使用分析**
2. 诊断故障原因
(1) 代码导致内存泄漏
实际上,代码中的内存泄漏大部分能在测试环境上被排除。在测试环境上采用压力测试,发送大量的请求到服务端测试。
- equals()和hashCode()未重写导致的内存泄漏
问题:在定义新类时没有重写正确的equals()和hashCode()方法。在使用HashMap的场景下,如果使用这个类对象作为key,HashMap在判断key是否已经存在时会使用这些方法,如果重写方式不正确,会导致相同的数据被保存多份。
- 内部类引用外部类
非静态的内部类默认会持有外部类,尽管代码上不再使用外部类,所以如果有地方引用了这个非静态内部类,会导致外部类也被引用,垃圾回收时无法回收这个外部类
匿名内部类对象如果在非静态方法中被创建,会持有调用者对象,垃圾回收时无法回收调用者。
使用静态方法,可以避免匿名内部类持有调用者对象。
** 线程池中使用 threadLocal 注意remove操作
**大量对象被保持在静态字段中
解决方案:
- 尽量减少将对象长时间的保存在静态变量中,如果不再使用,必须将对象删除(比如在集合中)或 者将静态变量设置为null。
- 使用单例模式时,尽量使用懒加载,而不是立即加载。
- Spring的Bean中不要长期存放大对象,如果是缓存用于提升性能,尽量设置过期时间定期失效。
- 资源没有正常关闭
Java 7 开始,使用try-with-resources语法可以用于自动关闭资源。
(2) 大量并发下导致内存OOM
瞬时流量、DDOS等会造成短暂的内存无法分配和GC不及时,最终造成OOM;
使用内存快照诊断
- 当堆内存溢出时,需要在堆内存溢出时将整个堆内存保存下来,生成内存快照(Heap Profile )文件。
- 生成内存快照的Java虚拟机参数:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=
- -XX:+HeapDumpOnOutOfMemoryError 发生OutOfMemoryError错误时,自动生成hprof内存快照文件。
- XX:HeapDumpPath=
指定hprof文件的输出路径。
- 使用内存分析工具MAT(Memory Analyzer Tool)打开hprof文件,并选择内存泄漏检测功能,MAT会自行根据内存快照中保存的数据分析内存泄漏的根源。
如果并未产生内存溢出,也可以导出运行中系统的内存快照,比较简单的方式有两种,注意只需要导出标记为存活的对象:
- 通过JDK自带的jmap命令导出,格式为:
jmap -dump:live,format=b,file=文件路径和文件名 进程ID - 通过arthas的heapdump命令导出,格式为:
heapdump –live 文件路径和文件名
加上 live 后会自动做一次Full GC,只保留存活的对象,生成快照
** 使用MAT分析内存溢出的场景**
模拟程序:
1 | /** |
这里我使用的内存分析工具是 Eclipse Memory Analyzer,分析内存溢出主要分3个步骤:
- 占用内存过大的对象有哪些(Histogram)
- 被谁引用的(dominator_tree)
- 定位到具体的代码(thread_overview)
Histogram(直方图):查看内存占用 dominator_tree(支配树):关系引用图 thread_overview:查看线程的概览信息
** 超大内存快照分析**
在程序员开发用的机器内存范围之内的快照文件,直接使用MAT打开分析即可。但是经常会遇到服务器上的程序占用的内存达到10G以上,开发机无法正常打开此类内存快照,我们可以找一台内存足够的服务器,在服务器上使用MAT。
此时需要下载服务器操作系统对应的MAT。下载地址:Eclipse Memory Analyzer Open Source Project | The Eclipse Foundation
然后通过MAT中的脚本生成分析报告(体积很小,包括了一些静态页面),下载到开发机上
1 | ./ParseHeapDump.sh 快照文件路径 org.eclipse.mat.api:suspects org.eclipse.mat.api:overview org.eclipse.mat.api:top_components |
- suspects:内存泄漏检测报告
- overview:总览图
- top_components:组件图
注意:默认MAT分析时只使用了1G的堆内存,如果快照文件超过1G,需要修改MAT目录下的 MemoryAnalyzer.ini配置文件调整最大堆内存。
还可以使用 HeapHero 智能分析堆内存快照,官方网站:Brilliant Graphs, metrics and java heap dump analysis anti-patterns reported (heaphero.io)
在线定位问题
- 修复问题
- 模拟和验证是否解决
案例分析
解决思路:
- 服务出现OOM内存溢出时,生成内存快照。
- 使用MAT分析内存快照,找到内存溢出的对象。
- 尝试在开发环境中重现问题,分析代码中问题产生的原因。
- 修改代码。
- 测试并验证结果。
我们查看Tomcat的工作线程对象的支配树,可以看到HandlerMethod,即Controller中的方法接口,查看它的outgoing references。
- outgoing references:该对象引用了哪些对象
- incoming references:该对象被哪些对象所引用
根据description可以找到当前线程正在执行哪个接口方法
** 案例2 – Mybatis在使用foreach进行sql拼接时导致的内存溢出**
问题根源:
Mybatis在使用foreach进行sql拼接时,会在内存中创建对象,如果foreach处理的数组或者集合元素个数过多,会占用大量的内存空间。
解决思路:
- 限制集合元素个数
- 将id缓存到redis或者内存缓存中,通过缓存进行校验
问题根源:
Excel文件导出如果使用POI的XSSFWorkbook,在大数据量(几十万)的情况下会占用大量 的内存。
解决思路:
- 使用poi的SXSSFWorkbook
- hutool提供的BigExcelWriter减少内存开销
- 使用easy excel,对内存进行了大量的优化,但由于是分片写,导出时间较长
ThreadLocal有个经典的应用场景:当一个用户请求到服务时,拦截器会根据请求头信息组装一个用户信息的对象放到ThreadLocal中,这样方便我们后面在Controller层、Service层等地方使用,但是在拦截器的afterCompletion方法中,必须要将ThreadLocal 中的数据清理掉。
存在问题:
- 线程池参数设置不当,会导致大量线程的创建或者队列中保存大量的数据。
- 任务没有持久化,一旦走线程池的拒绝策略或者服务宕机、服务器掉电等情况很有可能会丢失任务。
存在问题:
- 队列参数设置不正确,会保存大量的数据。
- 需要自行实现持久化的机制,否则数据会丢失。