Java OutOfMemoryError 剖析指南-内存分配策略大揭秘


今天,我们来探讨一个 Java 程序员再熟悉不过的话题 —— OutOfMemoryError(OOM)。这种 RuntimeException 在我们的日常工作中屡见不鲜,但你真的了解其根源所在吗?本文将为您深入剖析 OOM 产生的原因,以及 JVM 内存管理和分配的相关机制,并通过实例代码完成形象演示。最后,我将留给大家几个思考题,探讨如何更好地避免和诊断 OOM 问题。那么,让我们开始今天的分享吧!


一、Java 内存模型

在深入探讨 OOM 之前,首先让我们快速回顾一下 Java 内存模型。我们知道,Java 运行时的内存空间主要被划分为两大区域:

  1. 堆内存(Heap):所有对象实例均在此分配内存。
  2. 方法区(Method Area):存储已被虚拟机加载的类信息、常量、静态变量等。

除此之外,每个线程还拥有一个私有的虚拟机栈(VM Stack),用于存储局部变量表等。我们今天的讨论将主要围绕堆内存展开。

想详细了解内存模型的同学,请前往查阅JVM 内存布局深度解析,你所不知道的一面


二、OOM 的常见类型


1、Java 堆空间不足导致的 OOM

堆内存用于存放新创建的对象实例,只要空间被耗尽,就会出现如下错误:

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

造成这种 OOM 的典型原因是对象实例创建过多,导致内存被消耗殆尽。

我们来看一个简单的例子:

List<Object> list = new ArrayList<>();
while(true) {
    list.add(new Object());
}

上述代码将无限创建 Object 对象,直至内存被耗尽。产生这类 OOM 的场景还包括大数据集计算、缓存持有过多对象等。


2、永久代(Perm Gen)空间不足

在 JDK 1.7 之前的 HotSpot 虚拟机,类元数据(如方法字段等)被存储在永久代(Perm Gen)中。当加载的类太多时,就会出现这种 OOM:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space

为了演示这种 OOM,我们可以不停生成有大量静态变量的类:

List<Class<?>> classCache = new ArrayList<>();
int i = 0;
try {
    while(true){
        ClassWriter cw = new ClassWriter(0);
        cw.visit(...)
        // 生成大量静态变量
        for (int j = 0; j < 100000; j++) {
            cw.visitField(...);
        }
        byte[] code = cw.toByteArray();
        classCache.add(new MyClassLoader().defineClass(null, code, 0, code.length));
        i++;
    }
} catch (Throwable e) {
    System.out.println(i);
}

上述代码将不断生成静态字段过多的匿名类,耗尽元空间。从 JDK 1.8 开始,类元数据被移至本地内存,因此该问题不复存在。


#### 3、直接内存(Direct Buffer)耗尽

直接内存(Direct Buffer)是为了加快 IO 操作而创建的堆外内存。当我们向 ByteBuffer 中写入数据时,如果数据量太大,直接内存也会被耗尽:

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory

我们可以通过下列代码触发该异常:

ByteBuffer.allocateDirect(104857600);

简单分析可知,在 32 位系统下,直接分配 100MB 直接内存是没有问题的,但 64 位系统下直接内存的大小受限于虚拟内存映射区域,因此较容易导致 OOM。


4、无限制创建线程导致的 OOM

Java 虚拟机栈也是内存区域的一部分,无限制创建线程最终会消耗该区域内存,抛出如下异常:

Exception in thread "main" java.lang.OutOfMemoryError: Unable to create new native thread

我们可以编写这样一段代码演示该问题:

for( ; ; ){
    new Thread().start();
}

这段代码将无限创建线程,必将导致虚拟机栈内存被耗尽。


5、内存映射文件访问异常

通过内存映射文件的方式对大文件进行随机访问时,JVM 会直接将文件映射到内存中,提高读写性能。但这种做法也会对内存造成很大压力:

Exception in thread "main" java.lang.OutOfMemoryError: Map failed

我们可以通过映射一个超大文件来复现该问题:

RandomAccessFile raf = new RandomAccessFile("huge.txt", "rw");
FileChannel fc = raf.getChannel();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, new File("huge.txt").length());

以上场景都有可能导致 OOM 异常。作为开发人员,我们需要清楚地认识和了解 OOM 的各种形态,以便及时规避和诊断问题。


三、Java 内存回收与分配策略


OOM 的发生往往与虚拟机的内存分配与回收策略密切相关。了解这些机制,对于我们诊断和规避 OOM 至关重要。

1、 内存分配

对象在 Java 中主要通过两种方式分配内存:

  • TLAB 分配(Thread Local Allocation Buffer):为每个线程预先分配一块内存空间,在该空间内分配对象时无需加锁,可提高效率。

  • 指针碰撞(Bump The Pointer):在堆内存中维护一个指针,用于分配对象内存空间。如果内存空间不足,就会触发 GC。


2、 GC 算法

在堆内存中,主要执行以下几种 GC 算法:

  • 复制算法(Copying):将内存划分为两个相等的空间,每次只使用其中一个。GC 时,将正在使用中的对象复制到未使用的空间中。
  • 标记-清除(Mark-Sweep):标记出所有正在使用的对象,未被标记的空间就会被清理。
  • 标记-整理(Mark-Compact):先标记存活对象,再移动这些存活对象,最后将存活对象向一端压缩,完成内存整理。
    • 分代收集(Generational Collection):根据对象存活周期不同将内存划分为几块,如新生代和老年代等,每块采用不同的 GC 算法。

3、内存分配与回收小结

Java 内存分配时,首先会在 TLAB 中分配。当 TLAB 空间不足时,将会触发 GC,对新生代进行复制算法回收。如果经过 GC 后仍无法分配内存,则会触发 Full GC,扫描整个堆内存回收空间。如果 Full GC 后仍无法进行内存分配,就会出现 OOM。

需要详细了解Java 内存回收与分配策略的同学,请前往查阅Java虚拟机原理(上)-揭秘Java GC黑匣子-知其所以然,从此不再捆手捆脚.


四、手动触发 Full GC


既然 GC 不足往往会导致 OOM,那么我们是否可以手动触发 Full GC 来避免这种情况呢?答案是肯定的。我们可以调用 System.gc()强制触发 Full GC,但这种做法存在以下几个问题:

  1. 这只是"建议"而非命令,虚拟机并不一定会执行 Full GC。
  2. 执行 Full GC 是一个非常昂贵的操作,会导致应用暂停响应,产生较长的停顿时间。
  3. 手动触发 Full GC 可能会暴露虚拟机 Bug,因此不被官方推荐使用。

因此,在生产环境中,还是应该优先考虑合理的内存配置和代码优化手段,避免 OOM 异常的发生。


五、OOM 问题排查


出现 OOM 异常后,我们如何高效地定位问题根源呢?这里给出一些常见的排查思路:


1、内存映像分析(Memory Dump)

运行带 OOM 的应用时,通过 jmap 命令生成内存映像文件,再通过 Eclipse MAT 等工具加载分析。可定位内存中的大对象。

以下是使用jmap命令和Eclipse Memory Analyzer Tool (MAT) 进行内存映像分析的详细步骤:


第一步,准备环境

确保你的Java开发环境已经安装了jmap工具,它通常与JDK一起提供。同时,下载并安装Eclipse MAT。


第二步,运行Java应用

运行你的Java应用程序,确保它能够复现OOM问题。


第三步,使用jmap生成内存映像

当应用程序出现OOM问题时,使用jmap命令生成内存映像文件。命令格式如下:

jmap -dump:format=b,file=<filename.hprof> <pid>
  • format=b 表示生成的内存映像格式为二进制格式。
  • file=<filename.hprof> 指定生成的内存映像文件的名称。
  • <pid> 是Java进程的进程ID。

第四步,使用Eclipse MAT分析内存映像

打开Eclipse MAT,选择“File” > “Open Heap Dump”,然后加载你刚才生成的.hprof文件。


第五步,分析内存使用情况

MAT提供了多种分析工具来帮助诊断内存问题:

  • Histogram: 显示所有对象的列表,按类名、实例数量和占用内存排序。

  • Dominator Tree: 显示对象的支配树,帮助找出占用内存最多的对象。

  • Leak Suspects: 通过算法分析可能的内存泄漏。

  • References: 查看对象的引用情况,找出为什么对象没有被垃圾收集器回收。


第六步,诊断内存泄漏
  • 在Histogram视图中,查找占用内存最多的对象。

  • 使用Dominator Tree来确定哪些对象占用了大量内存。

  • 在Leak Suspects视图中,MAT会提供可能的内存泄漏信息。

  • 使用References视图来跟踪对象的引用链,这有助于确定为什么对象没有被回收。


第七步,优化代码

根据MAT的分析结果,你可以开始优化你的代码:

  • 修复内存泄漏,比如确保及时释放不再使用的对象。

  • 优化数据结构,减少内存使用。

  • 调整JVM参数,比如增加堆大小或调整垃圾收集器。


第八步,测试和验证

在进行代码优化后,重新运行应用程序并监控其性能,确保OOM问题已经得到解决。


第九步,文档和记录

记录你的分析过程和解决方案,这将有助于未来的性能调优和问题排查。

通过以上步骤,你可以使用jmap和Eclipse MAT来定位和解决Java应用程序中的OOM问题。这是一个持续的过程,需要不断地监控、分析和优化应用程序的内存使用情况。


2、堆转储快照分析(Heap Dump)

通过 jcmd 命令获取运行中的堆转储快照文件,使用 Eclipse MAT 分析,查找内存中的大对象或泄漏对象。

  • 打印 GC 日志:通过 GC 日志分析 GC 发生的频率、时间等,定位 Full GC 发生的场景。
  • 内存占用与 GC 监控:结合多种监控手段,如 VisualVM、JConsole、GCViewer 等,实时监控应用内存和 GC 情况。
  • 代码审计:彻底审视代码质量,查找潜在的内存泄漏问题,如静态变量引用变量导致无法回收、实例变量生命周期过长等。

总之,我们通过多管齐下的分析方式,最终还是要定位到导致 OOM 的具体代码环节,并给出相应的优化方案和修复思路。这并非一蹴而就的过程,需要开发人员对 JVM 内存管理机制有一定的了解和经验。


六、优化思路

为了避免 OOM 的发生,我们可以考虑以下几个角度来改进:

  • 根据应用内存需求,给虚拟机设置合理的内存参数,如 -Xms -Xmx 等。
  • 检查代码是否存在内存泄漏,及时关闭无用资源,如数据库连接、IO 流等。
  • 控制对象创建的数量和生命周期,及时标记对象为 null,主动请求 GC。
  • 优化 GC 频率和对象存活时间,如分代收集、使用较新的 GC 算法等。
  • 使用内存映射文件/直接内存时,规划好使用量,及时释放。
  • 对于大对象或者不可预期大小对象的创建,考虑使用本地内存.

显然,这需要根据具体的应用场景和内存使用模型来具体分析。由此可见,作为 Java 开发人员,我们不仅需要掌握编码技能,更要对虚拟机的内存管理机制有深入的理解。


七、思考

通过本文的分析,相信您已经对 OOM 以及 Java 虚拟机内存管理机制有了更加深入的认识。不过,我也希望能通过以下几个问题,引导大家进一步思考:

  1. 除了堆内存之外,虚拟机栈和元空间是否也会出现 OOM?如果会,导致它们 OOM 的原因有哪些?
  2. 网上有种说法"64位系统中不会出现 OOM",这种观点是否正确?
  3. 类加载器是否可能导致 OOM 发生?

期待您在评论区与我分享自己的见解!另外,如果您对 Java 虚拟机及其性能优化有进一步的兴趣,也欢迎继续关注我的博客,我们后续将一起探讨更多精彩的内容!


  • 17
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

w风雨无阻w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值