常见的溢出错误:
- java.lang.OutOfMemoryError: Java heap space
- java.lang.OutOfMemoryError: PermGen space
- java.lang.OutOfMemoryError: Requested array size exceeds VM limit
- java.lang.OutOfMemoryError: request bytes for . Out of swap space?
- java.lang.OutOfMemoryError: (Native method)
1.0 问题描述
线上有一个应用, 运行平均每天增加 100多MB的使用内存, 并且是降不下来的那种, 比如JVM配置是 3G堆内存!
但是运行一周后, 通过top命令查看到 RES使用达到了 5.2G, 并且只要持续运行就会不断地增加, 降不下来, 目前解决的结果是定时去重启!!!
2.0 排查
2.1 排查是否存在日志溢出错误
从结果上看是没有发生过溢出错误的!!!, 这个时候就开始怀疑是堆外内存出现泄露了
2.2 通过Jstat 命令查看垃圾回收情况
查看监控 Java 虚拟机的垃圾回收情况, 每隔 1000 毫秒(1 秒)收集一次数据,并连续收集 10 次 返回参数说明
- Eden 区使用率过高: EU 的数值在第1行1347584.0, 不断快速增加, 然后在第5行进行了一次新生代中的Eden 空间GC回收, 后续然后马上又大量增加,这表明 Eden 区的内存使用率非常高。可能是由于程序创建了大量的对象,导致 Eden 区很快被填满,频繁触发年轻代垃圾回收 (但总体GC回收正常)
2.3 jmap 排查前50个最大对象
从结果上看也没有发现上面特别大的异常!
2.4 通过java诊断工具-arthas进行分析
2.4.1查看当前系统的实时数据面板
堆内存使用正常, 堆外的(nonheap)也使用不多, 远远达不到5G多, 这个时候怀疑是 JNI 异常点了
2.5 分析dump文件
dump出文件分析下是否存在异常点
也分析不出什么异常点, 占用的内存量远远达不到5g多
2.6 JVM启动参数增加 -XX:NativeMemoryTracking=detail 分析
注意增加此参数会损耗JVM性能, 加上参数后得重启又是漫长的等待几天查看 目前内存使用量到达了3.6G
- Java Heap (reserved=3072MB, committed=3072MB) 保留总量:3 GB , 已提交3G; (堆内存使用量在预期限制内,因为它已完全保留和提交)
- Class (reserved=1079MB, committed=61MB) 保留总量:1 GB , 已提交61MB, ,
- Thread (reserved=57MB, committed=57MB) (thread #147)线程数量和使用也是正常
- Code (reserved=250MB, committed=38MB) 代码缓存也不大
- GC (reserved=176MB, committed=176MB) GC使用也是正常的
- Internal (reserved=287MB, committed=287MB) 指代 JVM 内部使用的内存,包含各种数据结构、缓存等
到目前为止还是看不出任何的异常!!! 感觉排查失败的样子!!!
2.6.1 设置比较基线
Properties
jcmd 2252351 VM.native_memory baseline
2.6.2 每天查看比较
Properties
jcmd 2089751 VM.native_memory summary.diff scale=MB
发现Internal 区域每天都会增加使用 100多MB, 直到一周后使用量到达了1G多, 已经确定问题出现在这里!!!
2.6.3 查看更为详细的内存
Properties
jcmd 2089751 VM.native_memory detail.diff scale=MB
这里就已经能确定是JNI引起的问题了, 但是怎么确定项目中哪里使用到了呢?
首先review代码看看哪里可能出现问题, 如果没有发现问题情况下, 再深入排查
2.7 通过arthas-profiler排查
通过原生函数名找Java调用栈 通过arthas的profiler命令,可以采样到原生函数的调用栈,如下
Properties
[arthas@2089751]$ profiler execute 'start,event=doPrivileged,alluser'
发现profiler其实可以直接使用指令地址,所以不转换为函数名称,也是OK的 使用的是这种方式
第一次执行发现失败了, 原因是没有控制非特权用户(没有CAP_SYS_ADMIN权限)使用perf events系统的行为
进行执行下
继续执行
分析
从结果上看邮件找到了泄露点的调用链, 后面根据代码进行优化!!!
扩展:
Arthas 是一个强大的 Java 诊断工具,其中的 profiler
命令可以帮助开发者生成应用热点的火焰图,进行性能分析。以下是 profiler
支持的事件及其详细说明: $ profiler start --event alloc
基本事件 (Basic events)
- cpu: 采样 CPU 使用情况。
- alloc: 采样内存分配情况。
- lock: 采样锁竞争情况。
- wall: 采样墙钟时间(Wall-clock time)。
- itimer: 使用内部定时器进行采样。
Java 方法调用 (Java method calls)
可以指定特定的 Java 方法进行采样,格式如下:
- ClassName.methodName: 例如
com.example.MyClass.myMethod
。
性能事件 (Perf events)
这些事件主要用于更低层次的性能分析,涵盖了硬件和软件性能计数器等:
- page-faults: 页面错误。
- context-switches: 上下文切换。
- cycles: CPU 周期。
- instructions: 指令数。
- cache-references: 缓存引用。
- cache-misses: 缓存未命中。
- branch-instructions: 分支指令。
- branch-misses: 分支未命中。
- bus-cycles: 总线周期。
- L1-dcache-load-misses: L1 数据缓存加载未命中。
- LLC-load-misses: 最后一级缓存加载未命中。
- dTLB-load-misses: 数据 TLB 加载未命中。
- rNNN: 特定的硬件事件。
- pmu/event-descriptor/: PMU 事件描述符。
- mem:breakpoint: 内存断点。
- trace:tracepoint: 跟踪点。
- kprobe:func: 内核探针函数。
- uprobe:path: 用户探针路径。
profiler execute命令可以用来执行一些复杂的操作,支持一次执行多个子命令,命令之间用逗号分隔
操作类参数
start
:开始分析resume
:开始或恢复分析,不重置已收集的数据stop
:停止分析dump
:转储收集的数据,不停止分析会话check
:检查指定的分析事件是否可用status
:打印分析状态(不活动/运行 X 秒)meminfo
:打印分析器内存统计信息list
:显示可用的分析事件列表version[=full]
:显示代理版本
事件类参数
event=EVENT
:要跟踪的事件(cpu, wall, cache-misses 等)alloc[=BYTES]
:以 BYTES 间隔分析分配live
:仅从活动对象构建分配概况lock[=DURATION]
:分析超过 DURATION 纳秒的竞争锁
输出类参数
collapsed
:转储折叠的堆栈(FlameGraph 脚本使用的格式)flamegraph
:生成 HTML 格式的火焰图tree
:生成 HTML 格式的调用树jfr
:以 Java Flight Recorder 格式转储事件jfrsync[=CONFIG]
:使用给定的配置启动 Java Flight Recording 和分析器traces[=N]
:转储前 N 个调用跟踪flat[=N]
:转储前 N 个方法(又名 flat profile)samples
:计算样本数(默认)total
:计算总值(时间、字节等)而不是样本
其他参数
chunksize=N
:JFR chunk 的近似大小(字节),默认为 100 MBchunktime=N
:JFR chunk 的持续时间(秒),默认为 1 小时timeout=TIME
:在 TIME 时自动停止分析器(绝对或相对时间)loop=TIME
:在循环中运行分析器(连续分析)interval=N
:采样间隔(纳秒),默认为 10,000,000(即 10 毫秒)jstackdepth=N
:最大 Java 堆栈深度,默认为 2048safemode=BITS
:禁用堆栈恢复技术,默认为 0(即全部启用)file=FILENAME
:转储的输出文件名log=FILENAME
:将警告和错误记录到给定的专用流loglevel=LEVEL
:日志级别:TRACE、DEBUG、INFO、WARN、ERROR 或 NONEserver=ADDRESS
:在 ADDRESS/PORT 启动不安全的 HTTP 服务器filter=FILTER
:线程过滤器threads
:分别分析不同的线程sched
:按调度策略对线程分组cstack=MODE
:如何在 Java 堆栈之外收集 C 堆栈帧- MODE 为 'fp'(帧指针)、'dwarf'、'lbr'(最后分支记录)或 'no'
allkernel
:仅包括内核模式事件alluser
:仅包括用户模式事件fdtransfer
:使用 fdtransfer 将 fds 传递给分析器simple
:简单的类名而不是 FQNdot
:带点的类名sig
:打印方法签名ann
:注释 Java 方法lib
:添加库名称前缀mcache
:jmethodID 缓存的最大时间,默认为 0(禁用)include=PATTERN
:包含包含 PATTERN 的堆栈跟踪exclude=PATTERN
:排除包含 PATTERN 的堆栈跟踪begin=FUNCTION
:在执行 FUNCTION 时开始分析end=FUNCTION
:在执行 FUNCTION 时结束分析title=TITLE
:火焰图标题minwidth=PCT
:火焰图最小帧宽度百分比reverse
:生成反向堆栈的火焰图/调用树
Properties
profiler execute 'start,event=cpu'
RN`:排除包含 PATTERN 的堆栈跟踪
begin=FUNCTION
:在执行 FUNCTION 时开始分析end=FUNCTION
:在执行 FUNCTION 时结束分析title=TITLE
:火焰图标题minwidth=PCT
:火焰图最小帧宽度百分比reverse
:生成反向堆栈的火焰图/调用树
Properties
profiler execute 'start,event=cpu'
1