一次OutOfMemoryError异常的分析

本文通过一个具体案例探讨了Java中出现OutOfMemoryError的原因。即使给JVM分配了足够的堆内存,但由于堆内存的分配机制,仍然可能无法成功分配大型对象。文章详细介绍了如何调整JVM参数以解决此类问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么我给JVM分配的堆已经足够大了,但在给一个数据结构分配内存的时候却抛出了OutOfMemoryError异常?这是我最近面临的一个问题。


看了下开发人员这段代码到底是干什么的并且再三确认了通过-Xmx参数给JVM设置的堆大小之后,看样子问题确实是有点诡异了。


半小时后我们终于知道是怎么回事并解决了这个疑案。不过这个问题的确在一开始并不是那么明显,所以我想如果我把底层的问题描述得清楚点的话,或者以后能替大家节省掉一天的排查问题的时间。


通常来说,想弄清楚一个问题的最好方式就是通过一个实例来进行说明。这里我创建一个小的测试用例:



package eu.plumbr.demo;
class ArraySize {
public static void main(String... args) {
int[] array = new int[1024*1024*1024];
}
}



代码很简单——它要做的就是分配一个1G大小的数组。现在你想下,Java的int类型需要4个字节,那如果用个6G大小的堆来运行这个程序应该就没问题了。毕竟10亿个整型数值也就占用4G内存而已。那为什么我执行这段代码的时候会出现下面的结果?


My Precious:bin me$ java –Xms6g –Xmx6g eu.plumbr.demo.ArraySize
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at eu.plumbr.demo.ArraySize.main(ArraySize.java:6)



在增加堆的大小之前(事实上,如果使用-Xmx7g来运行上述程序就没问题了),我们先来看下为什么出现的不是我们预期的结果。


首先,int类型在Java的确是占用4个字节。因此这并不是我们的JVM在晚上突然发疯了。而且我还可以很确定地告诉你,我的计算也没有问题——1024*1024*1024个整型的确是需要4,294,967,296字节,也就是4G内存。


想知道发生了什么,我们先使用–XX:+PrintGCDetails参数把GC的日志打开之后再运行一遍这个程序看看:



My Precious:bin me$ java –Xms6g -Xmx6g -XX:+PrintGCDetails eu.plumbr.demo.ArraySize

-- cut for brevity --

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at eu.plumbr.demo.ArraySize.main(ArraySize.java:6)

Heap
PSYoungGen total 1835008K, used 125829K [0x0000000780000000, 0x0000000800000000, 0x0000000800000000)
eden space 1572864K, 8% used [0x0000000780000000,0x0000000787ae15a8,0x00000007e0000000)
from space 262144K, 0% used [0x00000007e0000000,0x00000007e0000000,0x00000007f0000000)
to space 262144K, 0% used [0x00000007f0000000,0x00000007f0000000,0x0000000800000000)
ParOldGen total 4194304K, used 229K [0x0000000680000000, 0x0000000780000000, 0x0000000780000000)
object space 4194304K, 0% used [0x0000000680000000,0x0000000680039608,0x0000000780000000)
PSPermGen total 21504K, used 2589K [0x000000067ae00000, 0x000000067c300000, 0x0000000680000000)
object space 21504K, 12% used [0x000000067ae00000,0x000000067b087668,0x000000067c300000)



答案现在一目了然:尽管我们总的堆大小是足够的,但堆中没有一个单独的区域能容下4G的对象。我们这个6G的堆被划分成了四个不同的区域,它们的大小分别是:


- Eden区:1536M
- Survivor区(包括from区及to区):分别是256M
- 老生代:4096M


现在,请记住了,对象只能在一个独立的区中进行分配,而现在我们看到,这个程序是不可能了——没有任何一个区能有足够的空间来满足这个单次的4G内存的分配的。


因此——我们只能寄希望于增加堆的大小了吗?尽管我们已经多提供了差不多50%的内存——为一个4G大小的数据结构提供了一个6G的堆?先别着急——还有另一个解决方案。你可以设置内存中不同的区域的大小。但这并不是你想像的那么简单明了,你需要对启动配置做两点小的改动。在运行这段相同的程序时,得多增加两个额外的参数:



My Precious:bin me$ java -Xms6g -Xmx6g -XX:NewSize=5g -XX:SurvivorRatio=10 eu.plumbr.demo.ArraySize



这样程序就能完成它的使命也不会抛出OutOfMemoryError了。启动时加上-XX:+PrintGCDetails参数的话可以印证这一点:



Heap
PSYoungGen total 4806144K, used 4369080K [0x00000006c0000000, 0x0000000800000000, 0x0000000800000000)
eden space 4369408K, 99% used [0x00000006c0000000,0x00000007caaae228,0x00000007cab00000)
from space 436736K, 0% used [0x00000007e5580000,0x00000007e5580000,0x0000000800000000)
to space 436736K, 0% used [0x00000007cab00000,0x00000007cab00000,0x00000007e5580000)
ParOldGen total 1048576K, used 0K [0x0000000680000000, 0x00000006c0000000, 0x00000006c0000000)
object space 1048576K, 0% used [0x0000000680000000,0x0000000680000000,0x00000006c0000000)
PSPermGen total 21504K, used 2563K [0x000000067ae00000, 0x000000067c300000, 0x0000000680000000)
object space 21504K, 11% used [0x000000067ae00000,0x000000067b080c90,0x000000067c300000)



我们可以看到,各个区的大小的确是我们想要的:


- 新生代的总大小是5G,确如-XX:NewSize=5g所指定的那样
- Eden区是Survior区的10倍,正如我们通过-XX:SurvivorRatio=10参数指定的那样


注意,在这个例子中,两个参数都是必须的。如果你只指定了-XX:NewSize=5g的话,新生代划分成eden和survivor区的结果就没有一个能大于4G的区域了。


希望这篇文章将来能省掉你一天的调试时间。或者让你避免提供不必要的资源(分配过大的Java堆)。


译注:仔细观察了下他的日志可以发现,他分配6G内存的时候,新生代占了大概2G,老生代刚刚好是4G的大小,并且程序退出的时候老生代中还占用了200多K的大小,这样正好存放不下这个数组。作者这个解决方法也有点奇怪,还有一个方案就是将新生代的大小稍微调小一点,比如-XX:MaxNewSize=1800M,这样的话这个数组就可以直接在老生代中进行分配了:



ParOldGen total 4448256K, used 4194304K [0x0000000680000000, 0x000000078f800000, 0x000000078f800000)
object space 4448256K, 94% used [0x0000000680000000,0x0000000780000010,0x000000078f800000)



原创文章转载请注明出处:[url=http://it.deepinmind.com]http://it.deepinmind.com[/url]

[url=http://en.deepinmind.com/blog/2014/05/18/outofmemoryerror-on-overprovisioned-heap.html]英文原文链接[/url]
### Kettle 运行时 OutOfMemoryError 的解决方案 当 Kettle 遇到 `OutOfMemoryError` 问题时,通常可以通过调整 JVM 参数、优化数据流设计以及改进任务配置来解决问题。以下是具体的解决方法: #### 1. **增加 JVM 堆内存** Kettle 默认分配的堆内存较小(约 512MB 至 1GB),这可能导致在处理大规模数据集时发生内存溢出错误。通过修改启动参数可以显著提升可用内存。 - 编辑 kettle.bat 或 spoon.sh 文件中的 `-Xmx` 参数以增大最大堆内存大小。例如: ```bash export JAVA_OPTS="-Xms2g -Xmx8g" ``` 上述命令将初始堆内存设置为 2GB (`-Xms`) 并允许扩展至最高 8GB (`-Xmx`) [^1]。 #### 2. **启用分页机制** 如果作业涉及大量记录读取而未采用分页策略,则可能一次性加载过多的数据进入内存,从而引发异常。建议针对数据库查询部分实施分批提取方式减少单次加载量。 对于 SQL 查询步骤可加入 LIMIT 和 OFFSET 子句实现手动分页;或者利用 PDI 自带的支持增量抽取功能完成自动化分页逻辑构建 [^1]。 #### 3. **优化转换过程** 某些特定类型的转换可能会消耗额外资源,特别是涉及到排序(Sort),连接(Merge Join) 及去重复(Distinct Rows) 等操作节点时候更甚。因此有必要重新审视这些地方是否存在性能瓶颈并加以改善: - 替代算法选择:考虑用 Hash 类型替代标准 Merge Joins 如果适用的话; - 数据分区技术应用:把大数据分成若干个小块分别计算后再汇总结果; - 推荐使用轻量化组件代替重量级控件 (如 Replace Fields With Constants vs Calculator Step). #### 4. **监控与日志分析** 持续跟踪应用程序的表现情况有助于及时发现问题所在,并据此作出相应调整措施。开启详细的执行计划输出可以帮助定位哪些阶段最耗时/耗费最多资源。 另外还可以借助外部工具如 VisualVM 来深入剖析整个程序运行期间的各项指标变化趋势图谱以便更好地理解内部工作机制原理[^2]^. #### 5. **Driver端内存管理** 有时即使已经设置了足够的JVM选项仍会出现OOM现象, 此刻应该检查是否是在Driver端产生了巨大的临时对象而导致此状况的发生. 对于此种情形可通过下面两种办法之一缓解压力: - 尽量让那些大型结构转移到Executor侧进行处理; - 提高Driver自身的heap size限制.[^3] --- ```python import os # Example of setting environment variable for increasing memory allocation in Unix-like systems. os.environ['JAVA_OPTS'] = "-Xms2g -Xmx8g" print(f"Java Options set as {os.getenv('JAVA_OPTS')}") ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值