原文:前京东陌陌高级架构师的直播笔记分享(Java 内存问题排查和解决:内存概览,内存问题出现的原因,问题代码,案例分析)
上一周我有幸观看了高级架构师李国讲师的直播,内容是关于 Java 内存问题排查和解决。
下面是我做的笔记,在这里分享一下。
直播背景
直播讲师
李国,曾任京东、陌陌高级架构师。负责过京东金融调用链系统 SGM,以及数据库中间件 CDS 的开发工作。曾负责陌陌基础社交业务线的整体架构工作,对高并发下的 JVM 调优有丰富的经验。
主题
-
了解 JVM 和操作系统的内存管理基本概念
-
了解内存溢出和内存泄漏的原因和症状
-
根据实例诊断/发现/解决内存问题
想了解更多,欢迎关注我的微信公众号:Renda_Zhang
内存
Linux 系统内存概览
-
编译后地址是逻辑内存,需要经过翻译映射到物理内存
-
MMU 负责地址的转换
-
可用内存 = 物理内存 + 虚拟内存 (swap)
-
RES实际内存占用
-
可用内存 = free + buffers + cached
-
/proc/meminfo# cat /proc/meminfo MemTotal: 3881692 kB MemFree: 249248 kB MemAvailable: 1510048 kB Buffers: 92384 kB Cached: 1340716 kB 40+ more ...
JVM 基本内存划分
内存区域
-
堆:JVM 堆中的数据,是共享的,是占用内存最大的一块区域
-
虚拟机栈:Java 虚拟机栈,是基于线程的,用来服务字节码指令的运行
-
程序计数器:当前线程所执行的字节码的行号指示器
-
元空间:方法区就在这里,不是堆
-
本地内存:其他的内存占用空间
Java 内存管理基本概念
-
Java 内存
-
Metaspace默认无上限 -
原方法区在这里
-
JVM 分配的 Java 内存对象
-
通常使用
-Xmx -Xms控制大小 -
Java 堆内存
-
元空间(堆外)
-
-
操作系统剩余内存
内存划分
-
JVM 进程内存 = 堆内内存 + 堆外内存 -
堆外内存 = 元空间 + CodeCache + 本地内存 -
堆外内存和操作系统剩余内存是此消彼长的关系
-
可分配内存大小 = 物理内存 + SWAP -
32 位内存限制 4GB,目前 ZGC 支持 16 TB内存
配置参数设置
-
堆:
-Xmx -Xms -
元空间:
-XX:MaxMetaspaceSize -XX:MetaspaceSize -
栈:
-Xss -
直接内存:
-XX:MaxDirectMemorySize -
其它内存:无法控制
查看内存指令对比
-
jmap-
可以查看 堆内存 对象分布
-
可以导出堆内存快照线下分析
-
-
pmap-
查看 进程内存 映像信息
-
内存问题出现的分析
垃圾回收
-
自动垃圾回收:JVM 自动检测和释放不再使用的内存
-
Java 运行时 JVM 会有线程执行 GC,不需要程序员显示释放对象
-
GC 发生的实际由复杂的策略判断,自动触发,不受外部控制
-
不同的垃圾回收算法、甚至不同的 JVM 版本,回收策略都不一样
-
统计显示:OOM/ML 问题占比 5% 左右
-
平均处理时间 40 天左右
内存问题两种形式
-
内存溢出
OutOfMemoryError,简称OOM-
堆是最常见的情况
-
堆外内存排查困难
-
-
内存泄漏
Memory Leak,简称ML-
分配的内存没有得到释放
-
内存一直在增长,有 OOM 风险
-
GC时该回收的回收不掉
-
能够回收掉但很快又占满,产生压力
-
内存问题的影响
-
发生 OOM Error,应用停止(最严重)
-
频繁 GC,GC 时间长,GC 线程时间片占用高
-
服务卡顿,请求响应时间变长
-
排查困难
-
问题时间跨度大
-
问题解决耗费精力
-
现场保护意识不足
-
简单问题场景
-
物理内存不足
-
主机物理内存非常小
-
主机上应用进程非常多
-
-
给应用 JVM 分配的内存小
-
错误的引用方式,发生了内存泄漏。没有及时的切断与 GC roots 的关系
-
并发量大,计算需要内存大
-
没有控制取数范围(如分页)
-
加载了非常多的Jar包
-
对堆外内存无限制的使用
垃圾回收器介绍
-
CMS将在 Java 14 正式移除 -
G1主流应用的垃圾回收器 -
ZGC大容量(16TB),低延迟(10ms)的垃圾回收器 -
MaxGCPauseMillis预定目标,自动调整 -
G1HeapRegionSize小堆区大小 -
InitiatingHeapOccupancyPercent堆内存比例阈值,启动并发标记
可达性分析法
-
Reference Chain
-
GC 过程:找到活跃的对象,然后清理其他的
-
引用级别
-
强引用:属于最普通最强硬的一种存在,只有在和 GC Roots 断绝关系时,才会被消灭掉
-
软引用:只有在内存不足时,系统则会回收软引用对象
-
弱引用:当 JVM 进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象
-
虚引用:虚引用主要用来跟踪对象被垃圾回收的活动
-
对象何时提升(Promotion)
-
常规提升:对象够老
-
分配担保 Survivor 空间不够,老年代担保
-
大对象直接在老年代分配
-
动态对象年龄判定
-
-XX:MaxTenuringThreshold在 CMS 下默认为 6,G1 下默认为 15
排查内存问题
对比交通事故和内存故障
-
事故发生方 = 具体的服务
-
事故处理方 = 相关程序员
-
事故现场(拍照取证)= 问题发生快照
-
后续处理 = 措施改进
瞬时态和历史态
-
瞬时态 - 现场保存
-
是指当时发生的,快照类型的元素。
-
体积大
-
-
历史态 - 日志信息,监控
-
指按照频率抓取的
-
有固定监控项的资源变动图
-
业务日志
-
GC日志 (http://gceasy.io/)
-
排查工具示例
-
ss -antp > $DUMP_DIR/ss.dump 2>&1 -
netstat -s > $DUMP_DIR/netstat-s.dump 2>&1 -
top -Hp $PID -b -n 1 -c > $DUMP_DIR/top-$PID.dump 2>&1 -
sar -n DEV 1 2 > $DUMP_DIR/sar-traffic.dump 2>&1 -
lsof -p $PID > $DUMP_DIR/lsof-$PID.dump -
iostat -x > $DUMP_DIR/iostat.dump 2>&1 -
free -h > $DUMP_DIR/free.dump 2>&1 -
jstat -gcutil $PID > $DUMP_DIR/jstat-gcutil.dump 2>&1 -
jstack $PID > $DUMP_DIR/jstack.dump 2>&1 -
jmap -histo $PID > $DUMP_DIR/jmap-histo.dump 2>&1 -
jmap -dump:format=b,file=$DUMP_DIR/heap.bin $PID > /dev/null 2>&1
不同区域溢出示例
堆溢出
java -Xmx20m -Xmn4m -XX:+HeapDumpOnOutOfMemoryError - OOMTest [18.386s][info][gc] GC(10) Concurrent Mark 5.435ms [18.395s][info][gc] GC(12) Pause Full (Allocation Failure) 18M->18M(19M) 10.572ms [18.400s][info][gc] GC(13) Pause Full (Allocation Failure) 18M->18M(19M) 5.348ms Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at OldOOM.main(OldOOM.java:20)
元空间溢出
java -Xmx20m -Xmn4m -XX:+HeapDumpOnOutOfMemoryError -XX:MetaspaceSize=16M -XX:MaxMetaspaceSize=16M MetaspaceOOMTest 6.556s][info][gc] GC(30) Concurrent Cycle 46.668ms java.lang.OutOfMemoryError: Metaspace Dumping heap to /tmp/logs/java_pid36723.hprof ..
直接内存溢出
java -XX:MaxDirectMemorySize=10M -Xmx10M OffHeapOOMTest Exception in thread "Thread-2" java.lang.OutOfMemoryError: Direct buffer memory at java.nio.Bits.reserveMemory(Bits.java:694) at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123) at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311) at OffHeapOOMTest.oom(OffHeapOOMTest.java:27)...
栈溢出
java -Xss128K StackOverflowTest Exception in thread "main" java.lang.StackOverflowError at java.io.PrintStream.write(PrintStream.java:526) at java.io.PrintStream.print(PrintStream.java:597) at java.io.PrintStream.println(PrintStream.java:736) at StackOverflowTest.a(StackOverflowTest.java:5)
问题代码
泄漏代码示例
-
由于没有重写 Key 类的
hashCode和equals方法,造成了放入HashMap的所有对象,都无法被取出来 -
它们和外界失联了
-
如何修正:重写 Key 对象的
equals和hashCode方法
结果集失控示例
-
错误代码:
-
正确代码:
条件失控示例
-
fullname和other为空的时候 -
正确方式:使用 limit 语句,分页的思路
万能参数示例
-
错误代码:
-
减少使用map作为参数的频率
-
解决方式:拆分成专用的函数
-
正确代码:
一些预防措施
-
减少创建大对象的频率:比如 byte 数组的传递
-
不要缓存太多的堆内数据:使用 guava 的 weak 引用模式
-
查询的范围一定要可控:如分库分表中间件;ES 等有同样问题
-
用完的资源一定要 close 掉:可以使用新的 try-with-resources 语法
-
少用 intern:字符串太长,且无法复用,就会造成内存泄漏
-
合理的 Session 超时时间
-
少用第三方本地代码,使用Java方案替代
-
合理的池大小
-
XML(SAX/DOM)、JSON 解析要注意对象大小
案例一
现象
-
环境:
CentOS7,JDK1.8,SpringBoot -
G1 垃圾回收器
-
刚启动没什么问题,慢慢放量后,发生了 OOM
-
系统自动生成了
heapdump文件 -
临时解决方式:重启,但问题依然发现
信息收集
-
日志:GC 的日志信息:内存突增突降,变动迅速
-
堆栈:Thread Dump 文件:大部分阻塞在某个方法上
-
压测:使用
wrk进行压测,发现 20 个用户并发,内存溢出
wrk -t20 -c20 -d300s http://127.0.0.1:8084/api/test -t 使用的线程数 -c 开启的连接数量 -d 持续压测的时间
MAT 分析
-
MAT 工具是基于 eclipse 平台开发的,本身是一个 Java 程序
-
分析 Heap Dump 文件:发现内存创建了大量的报表对象
-
堆栈文件获取:
jmap -dump:format=b,file=heap.bin 37340 jhsdb jmap --binaryheap --pid 37340
解决
-
分析结果:
-
系统存在大数据量查询服务,并在内存做合并
-
当并发量达到一定程度,会有大量数据堆积到内存进行运算
-
-
解决方式:
-
重构查询服务,减少查询的字段
-
使用 SQL 查询代替内存拼接,避免对结果集的操作
-
举例:查找两个列表的交集
-
案例二
现象
-
环境:
CentOS7,JDK1.8,JBoss -
CMS 垃圾回收器
-
操作系统 CPU 资源耗尽
-
访问任何接口,响应都非常的慢
分析
-
找到使用 CPU 最高的线程
-
根据堆栈定位到是 GC 进程占用高 CPU
-
发现是 GC 线程占用大量资源
-
陷入僵局
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007ff9f8020000 nid=0x4f5e runnable "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007ff9f8021800 nid=0x4f5f runnable "GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007ff9f8023800 nid=0x4f60 runnable "GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007ff9f8025000 nid=0x4f61 runnable
进一步分析
-
发现每次 GC 的效果都特别好,但是非常频繁
-
了解到使用了堆内缓存,而且设置的容量比较大
-
缓存填充的速度特别快
-
结论:开了非常大的缓存,GC 之后迅速占满,造成 GC 频繁
-
类似问题:
-
Websocket心跳检测失效,造成链接不释放,无效包持续发送 -
数据库连接持续创建,依靠 GC 进行回收
-
案例三
现象
-
java进程异常退出
-
java进程直接消失
-
没有留下dump文件
-
GC日志正常
-
监控发现死亡时,堆内内存占用很少,堆内仍有大量剩余空间
分析
-
XX:+HeapDumpOnOutOfMemoryError不起作用 -
监控发现操作系统内存持续增加
-
可能:
-
被操作系统杀死
dmesg oom-killer -
System.exit() -
java com.cn.AA & -
kill -9
-
解决
-
发现:在
dmesg命令中发现确实被oom-kill -
解决:给JVM少分配一些内存,腾出空间给其他进程
kill -9 && kill -15
案例四
现象
-
Java 服务被
oom-kill -
操作系统内存
free区一直减少,并无其他进程抢占资源 -
堆内内存使用情况正常
-
使用
top命令,发现RES占用严重超出了-Xmx的设定
分析
-
大概率发生了堆外内存溢出
-
程序使用
unsafe类操作了堆外内存 -
pmap查看内存分布 -
gdb导出内存块 -
perf监控函数调用 -
gperftools分析内存分配函数
解决
-
发现:程序使用了 JNA 库,调用了 native 加密函数库,加密函数库存在内存管理 bug
-
修复:修正 native 函数库的 bug
堆内和堆外内存问题区别
-
堆内存问题
-
Java 进程内存持续增长
-
GC 显示 heap 区内存不足,GC 频繁
-
-
本地内存问题
-
GC 日志显示,heap 区有足够的空间
-
Java 进程内存一直在增长
-
总结
步骤
一、问题发现(最困难)
-
确保加入了日志和自动转储参数
-
确定物理内存足够:
free -
确定 Java 进程内存足够:
jmap -
确定主机环境,剩余内存大小
-
查看
GClog和其他日志 -
使用
jstack对线程进行摸底 -
对堆外内存进行排查
-
保留现场
二、采取措施
三 、重复观察
四、问题解决
SWAP的启用和观测