行业资讯
OOM频繁发生别慌!手把手教你用工具揪出“内存刺客“
说出来你可能不信我上个月又双叒叕被OOM折腾了一回。凌晨三点应用告警群里疯狂弹消息Java进程直接挂掉重启看得我血压都上来了。说实话OOM这个东西吧十个Java应用九个都遇到过。新手遇到它就是一脸懵老手遇到它也得骂骂咧咧。这篇文章不整那些花里胡哨的理论就实打实地聊聊当你的应用频繁OOM时怎么用工具一步步揪出真凶。先别急着dump堆老规矩先看日志很多人一遇到OOM第一反应就是赶紧jmap dump堆啊但其实这是错的。第一步永远是先看日志。Java应用在OOM的时候默认会在控制台或者日志文件里抛出异常信息。这个异常信息很关键它会告诉你到底是什么类型的OOM。常见的OOM有这么几种[1]java.lang.OutOfMemoryError: Java heap space堆内存不够用了最常见的一种java.lang.OutOfMemoryError: GC overhead limit exceededGC回收了98%内存却还是腾不出空间基本等于内存泄漏java.lang.OutOfMemoryError: Metaspace元空间满了类加载太多java.lang.OutOfMemoryError: Direct buffer memory直接内存溢出Netty玩多了容易遇到java.lang.OutOfMemoryError: unable to create new native thread线程数爆了常见于线程池配置不当我那次遇到的是Java heap space堆内存4G全被吃光了。但光看这个还不够我得知道这内存是被谁吃掉的。jstat让你看清GC的全貌光知道OOM的类型还不够你得知道GC到底在干什么。jstat这个工具就是干这个的。jstat-gcpid1000这个命令的意思是每隔1000毫秒输出一次GC信息。重点关注这几个列S0C/S1C Survivor区容量ECEden区容量OC老年代容量YGC/YGCT年轻代GC次数和总耗时FGC/FGCTFull GC次数和总耗时GCTGC总耗时我当时看到的现象特别明显FGC次数每小时涨几百次每次耗时几百毫秒。这就说明老年代一直被填满GC根本回收不动典型的内存泄漏特征[2]。还有个命令更直观能看堆内存使用率jstat-gcutilpid1000输出里有个O列表示老年代使用率如果这玩意儿长期在95%以上晃悠那你基本可以确定是内存泄漏了。jmap把堆快照拓印下来确认了是内存泄漏的问题下一步就是dump堆。jmap是JDK自带的工具用起来很简单jmap-dump:formatb,file/tmp/heap.hprofpid不过这里有个坑要说一下。dump堆的时候JVM会暂停所有线程也就是STWStop-The-World。如果你的应用是核心业务最好在低峰期操作不然可能引发雪崩。还有一种更好的方式是配置JVM参数让OOM时自动dump-XX:HeapDumpOnOutOfMemoryError-XX:HeapDumpPath/tmp/heap.hprof这样应用每次OOM都会自动生成一份堆快照省得你半夜爬起来手动操作我那次就是这么干的设了自动dump后安心睡觉第二天再分析。dump出来的文件一般会很大我那次dump出来有6个多G。如果你要把文件拉到本地分析建议先压缩一下jmap -dump:live,formatb,file/tmp/heap.hprofpid加个live参数只保留存活对象文件能小不少传输也快。MAT内存分析的屠龙刀拿到堆快照后就该主角登场了——Eclipse MATMemory Analyzer Tool。这玩意儿绝对是分析内存问题的神器没有之一[2]。下载安装就不多说了官网下下来解压就能用。注意要改一下MemoryAnalyzer.ini里的内存配置这个内存要大于你的dump文件大小不然MAT会直接OOM没错分析工具自己OOM听着是不是很魔幻。-vmargs -Xmx8g打开dump文件后重点看这几个东西1. Leak Suspects报告MAT会自动帮你分析可能的内存泄漏点生成一份报告。点开就能看到哪些对象占的内存最多哪些对象本该被回收却还在内存里赖着不走。我那次MAT直接给我标红了一个HashMap里面装着几十万条UserSession对象。这就是元凶了。2. Dominator Tree支配树这个视图能看到谁支配着谁也就是哪些大对象是其他对象的主子。在支配树里最上面的一般就是占用内存最多的家伙。3. Histogram直方图按类的维度展示对象数量和占用内存。如果某个类的实例数明显异常比如本应该只有几百个结果出现了几十万那就是有问题的。4. 引用链分析Path to GC Roots这个最关键能看到对象为什么没被GC回收。比如你发现有个UserSession对象一直没被回收就可以用Path to GC Roots功能看它被谁引用着。结果一查发现是被一个静态的HashMap长年累月地引用着永远不会被释放。VisualVM实时监控的小能手如果说MAT是事后分析的神器那VisualVM就是实时监控的利器[1]。它能让你看到内存的实时变化曲线。VisualVM也是JDK自带的工具JDK8之前直接在bin目录下有jvisualvmJDK9之后需要单独下载。用它主要干两件事实时监控内存曲线看堆内存是不是有规律地增长比如每次定时任务跑完就涨一波那基本就是定时任务有问题手动触发堆dump在MBeans或者Monitor标签页可以直接生成堆快照VisualVM还有个插件叫VisualGC装上之后能看到各个内存分区的实时使用情况包括Eden、Survivor、Old、Metaspace的实时数据。配合jstat用绝配。async-profilerCPU和内存双修有些内存问题不是单纯的堆内存泄漏而是频繁创建大对象导致GC压力剧增。这种情况光看堆dump看不出来得用profiler工具。async-profiler是近两年比较火的一个工具不像传统的JProfiler那么重对生产环境的影响很小。可以用它来采样./profiler.sh-ealloc-d30-f/tmp/alloc.htmlpid这个命令的意思是采样30秒内的内存分配情况生成一个火焰图。从火焰图里能清晰地看到哪些代码路径在疯狂分配对象。我之前遇到过一个案例应用每隔几分钟就Full GC一次但用MAT看堆里又没多少对象。最后用async-profiler一查发现是某个日志框架在疯狂创建StringBuilder对象每次请求都创建几百个。这就不是内存泄漏了是代码写法的问题。元空间泄漏容易被忽视的角落说到OOM还有一种特别容易被忽略的情况——元空间Metaspace溢出。JDK8之后永久代PermGen被元空间取代了。元空间主要存放类的元数据比如类信息、方法信息、字段信息等。如果应用加载了大量动态类比如用了CGLIB、反射、或者JSP元空间就可能被撑爆。我之前有个项目用了大量的Groovy脚本动态编译结果运行两天后元空间就满了。这种情况光看堆内存是看不出来的得用jstat-gcmetacapacitypid或者直接看JVM参数有没有限制元空间大小-XX:MaxMetaspaceSize512m没限制的话默认会一直涨直到把机器内存吃光。线程数爆了另类的OOM还有一种OOM是unable to create new native thread。这种情况不是堆内存的问题而是线程数太多把系统资源耗尽了。Linux下线程数是有限制的可以通过ulimit -u查看。如果你的应用创建了大量线程比如每个请求都new一个线程很快就会达到上限。排查这种问题比较简单用jstack pid看看线程栈就行jstackpid/tmp/thread.txt然后统计一下线程数grep-c^/tmp/thread.txt如果线程数明显异常比如上万那基本可以确定是线程池没配好或者代码里在某个地方死循环创建线程。真实案例那个被静态集合坑惨的夜晚说回我那次凌晨三点被叫醒的故障吧。应用是个电商系统每到晚上高峰期就频繁OOM。dump堆之后用MAT分析直方图里UserSession类有40多万个实例每个大概1KB左右占了将近400MB内存。点开Path to GC Roots一看路径是这样的UserSession → HashMap$Node → HashMap → XXXService (静态字段)问题很明确有个Service类里有个静态的HashMap每次用户登录都往里塞一个Session但从来不清除。改完之后直接用Caffeine缓存替代原来的HashMap设置过期时间30分钟。线上效果立竿见影内存使用率从95%降到了40%以下OOM彻底消失。总结OOM排查的万能套路最后总结一下我的排查套路基本上任何OOM问题都能按这个流程来看日志确认是哪种类型的OOMjstat看GC判断是内存泄漏还是分配过快jmap dump堆拿到第一手现场证据MAT分析定位具体的泄漏对象和引用链async-profiler补刀看代码层面的对象分配热点改代码上线解决问题后观察内存曲线工具是死的思路是活的。OOM这玩意儿看似可怕但只要掌握了工具的使用方法配合一点点耐心基本上都能找到根因。别忘了线上一定要配HeapDumpOnOutOfMemoryError这个参数不然真的出事了你连现场都没得查。
郑州网站建设
网页设计
企业官网