前京东陌陌高级架构师的直播笔记分享(Java 内存问题排查和解决:内存概览,内存问题出现的原因,问题代码,案例分析)

时间:2022-07-24
本文章向大家介绍前京东陌陌高级架构师的直播笔记分享(Java 内存问题排查和解决:内存概览,内存问题出现的原因,问题代码,案例分析),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

上一周我有幸观看了高级架构师李国讲师的直播,内容是关于 Java 内存问题排查和解决。

下面是我做的笔记,在这里分享一下。

直播背景

直播讲师

李国,曾任京东、陌陌高级架构师。负责过京东金融调用链系统 SGM,以及数据库中间件 CDS 的开发工作。曾负责陌陌基础社交业务线的整体架构工作,对高并发下的 JVM 调优有丰富的经验。

主题
  • 了解 JVM 和操作系统的内存管理基本概念
  • 了解内存溢出和内存泄漏的原因和症状
  • 根据实例诊断/发现/解决内存问题

内存

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 >
  • sar -n DEV 1 2 > $DUMP_DIR/sar-traffic.dump 2>&1
  • lsof -p PID >
  • iostat -x > $DUMP_DIR/iostat.dump 2>&1
  • free -h > $DUMP_DIR/free.dump 2>&1
  • jstat -gcutil PID >
  • jstack PID >
  • jmap -histo PID >
  • 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 类的 hashCodeequals 方法,造成了放入 HashMap 的所有对象,都无法被取出来
  • 它们和外界失联了
  • 如何修正:重写 Key 对象的 equalshashCode 方法
结果集失控示例
  • 错误代码:
  • 正确代码:
条件失控示例
  • fullnameother 为空的时候
  • 正确方式:使用 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 的日志信息:内存突增突降,变动迅速
  1. 堆栈:Thread Dump 文件:大部分阻塞在某个方法上
  2. 压测:使用 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 不起作用
  • 监控发现操作系统内存持续增加
  • 可能:
    1. 被操作系统杀死 dmesg oom-killer
    2. System.exit()
    3. java com.cn.AA &
    4. 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 进程内存一直在增长

总结

步骤

一、问题发现(最困难)

  1. 确保加入了日志和自动转储参数
  2. 确定物理内存足够:free
  3. 确定 Java 进程内存足够:jmap
  4. 确定主机环境,剩余内存大小
  5. 查看 GClog 和其他日志
  6. 使用 jstack 对线程进行摸底
  7. 对堆外内存进行排查
  8. 保留现场

二、采取措施

三 、重复观察

四、问题解决

SWAP的启用和观测