当前位置: 首页 > news >正文

JVM-(11)JVM-定位OOM问题

JVM-定位OOM问题

OOM 即 OutOfMemoryError(内存溢出),是由于 JVM 的垃圾回收器 (Garbage Collector) 无法回收出足够的空间来满足对象分配的请求,并且堆内存已经无法再扩大容量导致的异常。


如不了解JVM知识,请查看如下链接:
JVM-(1)JVM入门
JVM-(2)Class File Format
JVM-(3)Class 加载过程
JVM-(5)JVM内存模型
JVM-(6)JVM GC
JVM-(7)堆内存逻辑分区
JVM-(8)JVM启动的常用命令以及参数
JVM-(9)JVM诊断的常用命令以及参数

导致OOM问题原因有哪些?

一、堆内存(Heap)相关 OOM (Java heap space, GC overhead limit exceeded)

这是最常见的一类 OOM,发生在对象实例分配的区域。

1. 内存泄漏 (Memory Leak) - 最主要原因

概念:对象已经不再被应用程序使用,但由于疏忽或错误,它们仍然被 GC Roots (不了解GC Roots请查看 JVM-(6)JVM GC)引用,导致垃圾回收器无法回收它们。

哪些情况会导致内存泄漏?

  • 静态集合类滥用:使用 static 修饰的 Map, List, Set 等,一旦将对象放入,其生命周期就与 JVM 一致,除非手动移除。
    public class MemoryLeak {public static Map<String, Object> staticMap = new HashMap<>(); // 危险!public void add(String key, Object value) {staticMap.put(key, value); // 只存放不移除,对象永远无法被回收}
    }
    
  • 未关闭的资源:数据库连接 (Connection)、文件流 (InputStream/OutputStream)、网络连接 (Socket) 等未在 finally 块或 try-with-resources 中显式关闭。这些对象不仅本身占内存,还可能附带巨大的底层资源。
  • 监听器和回调未注销:向全局管理器注册了监听器(如事件监听器),但在对象不用时没有注销,导致管理器依然持有其引用。
2. 内存需求预估不当
  • 堆大小设置不合理:应用本身需要 2G 内存才能平稳运行,但 JVM 堆最大只分配了 1G (-Xmx1g)。
  • 数据量激增
    • 流量陡增:正常系统突然遇到高并发请求,瞬间创建海量对象(如处理一次大促活动)。
    • 处理大文件/大数据集:一次性将一个大文件加载到内存或数据库查询结果全部加载到内存的 List 或 Map 中,而不是流式或分页处理。
    • 一次性任务:跑一个批处理job,需要处理的数据量远超平时。
3. 代码编写不当
  • 循环中创建大量对象:在循环体内频繁创建大对象,且无法快速回收。
  • 使用不当的数据结构:例如,使用 Vector 或 StringBuffer 时设置了过大的初始容量(capacity)。

二、非堆内存(Non-Heap)相关 OOM

1. Metaspace 溢出 (Metaspace / PermGen space)
  • 动态生成大量类:
    • 使用 CGLib、ASM、Javassist 等库进行动态代理、字节码增强,运行时生成了大量新类。
  • 反射:大量使用 Reflection 也可能增加 Metaspace 的负担。
  • 参数设置不当:-XX:MaxMetaspaceSize(或 JDK7 的 -XX:MaxPermSize)设置过小。
2. 直接内存溢出 (Direct buffer memory)
  • NIO 操作:显式调用 ByteBuffer.allocateDirect(size) 申请直接内存,但未妥善管理。
  • 使用 NIO 框架:如 Netty,其高性能的核心就是使用了直接内存池。如果在 Netty 的 ChannelHandler 中处理 ByteBuf 后没有调用 .release() 方法释放,就会造成直接内存泄漏。这是最常见的原因。
  • 参数设置不当:-XX:MaxDirectMemorySize 设置过小。
3. 线程栈溢出 (unable to create new native thread)
  • 创建过多线程:应用程序创建了成千上万个线程,超过了操作系统或 JVM 的限制。
  • 栈空间设置过大:每个线程都需要独立的栈空间(通过 -Xss 设置,如默认1M)。如果线程数多且 -Xss 值设得大,很容易耗尽用于线程栈的本地内存。
  • 系统限制:Linux 系统下通过 ulimit -u 命令查看的用户最大进程数限制。

三、特殊类型的 OOM

GC overhead limit exceeded
这是一种特殊的堆内存 OOM。它表示 JVM 花费了 98% 以上的时间进行垃圾回收,但每次回收只能释放不到 2% 的堆空间。这通常是内存泄漏的晚期症状,JVM 在绝望地做无用功。

如何定位OOM问题?

正常案例

1)设置 JVM 堆大小
本文使用文件下载的例子来描述oom问题是如何发生的,为方便快速得到实验结果,这里设置的堆内存大小为100M

-Xms100m -Xmx100m

先看下正常的下载案例代码:
分批次从输入流中读取一定内容(1024 B)到内存,而不是一次性将整个文件加载到内存

@GetMapping("/download3/{filename}")public void downloadFile3(@PathVariable String filename, HttpServletResponse response) throws IOException {Path filePath = Paths.get(uploadDir).resolve(filename).normalize();// 读到流中InputStream inputStream = new FileInputStream(filePath.toFile());// 文件的存放路径response.reset();response.setContentType("application/octet-stream");response.addHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode(filename, "UTF-8"));ServletOutputStream outputStream = response.getOutputStream();byte[] b = new byte[1024];int len;//从输入流中读取一定数量的字节,并将其存储在缓冲区字节数组中,读到末尾返回-1while ((len = inputStream.read(b)) > 0) {outputStream.write(b, 0, len);}inputStream.close();}

2)准备测试数据
本案例中下载的单个文件大小为 1.67M

3)使用 JMeter 测试
使用JMeter 模拟100个线程同时去下载该 1.67 M的文件


4)使用 Java VisualVM 工具 观察 jvm 堆内存使用情况
Java VisualVM 工具是 java 自带 jvm 分析工具,可以在 java/bin 目录找到,

异常案例

1)异常案例代码

@GetMapping("/download2/{filename}")public void downloadFile2(@PathVariable String filename, HttpServletResponse response) throws IOException {Path filePath = Paths.get(uploadDir).resolve(filename).normalize();/*** 高危写法*/byte[] fileByte = Files.readAllBytes(filePath);// 设置response的Headerresponse.setCharacterEncoding("UTF-8");// 指定下载文件名(attachment-以下载方式保存到本地,inline-在线预览)response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");// 告知浏览器文件的大小response.addHeader("Content-Length", "" + fileByte.length);// 内容类型为通用类型,表示二进制数据流response.setContentType("application/octet-stream");OutputStream os = response.getOutputStream();os.write(fileByte);os.flush();}

2)测试
100个线程同时下载


3)使用 Java VisualVM 工具 观察 jvm 堆内存使用情况

如何定位

① 启动JVM时添加产生oom异常时生成HeapDump参数
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=D://test//dump//
② 使用 VisualVM 导入 HeapDump 文件

这里使用的是 VisualVM , java 自带的 Java VisualVM 也可以,操作方法类似,官网:http://visualvm.github.io/

③ 使用 VisualVM 分析 HeapDump

1)找到占用内存最大的对象
按 Size 倒序,这里占用内存最大的对象是 byte[]

2)根据该对象找到其对应的 GC Root,本处 byte[]就是 GC Root 对象

3)在 GC Root 上右键选择 Select in Threads,跳转到线程视图,可以找到使用该 GC Root 的代码块

可以看到 Files.readAllBytes 这段代码是在 DownloadController 类,downloadFile2 方法中调用的,假设这段代码不是自己写的,我们事先也不知道,但是当看到这段代码的时候就要提高警惕了,很明显它的意思是将文件全部加载到内存

http://www.dtcms.com/a/350966.html

相关文章:

  • 论文学习日志——忆阻器与神经网络——part1
  • Python大型数组计算完全指南:从基础到分布式系统实践
  • Zookeeper(分布式RPC调用和分布式文件储存)
  • [小练习]100行不到使用Java Socket网络编程实现定向聊天
  • 从双重检查锁定的设计意图、锁的作用、第一次检查提升性能的原理三个角度,详细拆解单例模式的逻辑
  • 基于SpringBoot的网上点餐系统
  • OpenTenBase核心技术解密:突破OLTP与OLAP边界的分布式数据库革新
  • Flask模块如何使用
  • 浅谈技术顾问的转型困境
  • Web前端开发基础
  • windows 下 使用C++ 集成 zenoh
  • ArcGIS学习-9 ArcGIS查询操作
  • vLLM轻松通
  • ArcGIS学习-10 空间连接和矢量分析
  • 百度IOS客户端岗位--面试真题分析
  • 5G 三卡图传终端:应急救援管理的 “可视化指挥核心”
  • PostgreSQL诊断系列(3/6):性能瓶颈定位——缓冲池、I/O与临时文件
  • 【Linux系统】线程概念
  • 【R代码分析】甲烷排放绘制代码-参考论文
  • 【云原生】CentOS安装Kubernetes+Jenkins
  • RAW API 的 UDP 总结1
  • Android Glide常见问题解决方案:从图片加载到内存优化
  • [在实践中学习] 中间件理论和方法--Redis
  • 【JavaEE】了解volatile和wait、notify(三)
  • 08-引入AI编程插件(TRAE)编写单元测试并检查覆盖率
  • Cloudflare 推出 GenAI 安全工具,守护企业数据
  • 科普 | 5G支持的WWC架构是个啥(1)?
  • android 改机系列之-虚拟摄像头-替换相机预览画面
  • excel导出,多列合并表头
  • 运行node18报错