前京东陌陌高级架构师的直播笔记分享(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 类的
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的启用和观测
- Spring集成RabbitMQ-使用RabbitMQ更方便
- Nodejs学习笔记(三)--- 模块
- 使用JClouds在Java中获取和发布云服务器
- Silverlight单元测试框架
- Enterprise Library深入解析与灵活应用(2): 通过SqlDependency实现Cache和Database的同步
- 让你感觉不真实的13个伟大科学成就和发现
- 分析Silverlight跨域调用
- Spring集成RabbiMQ-Spring AMQP新特性
- Nodejs学习笔记(二)--- 事件模块
- 巧用FireFox来调试Silverlight
- Nodejs学习笔记(一)--- 简介及安装Node.js开发环境
- WCF后续之旅(7):通过WCF Extension实现和Enterprise Library Unity Container的集成
- 区块链技术(一):Truffle开发入门
- Nodejs学习笔记(一)——初识Nodejs
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释