版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
今天在使用spark任务计算千万级数据时,出现了OOM:
java.lang.OutOfMemoryError: GC overhead limit exceeded
原先提交任务的参数配置:
--master yarn-cluster --driver-memory 6g --num-executors 8 --executor-memory 18g --executor-cores 4
调大driver-memory后,问题得到了解决:
--master yarn-cluster --driver-memory 20g --num-executors 8 --executor-memory 18g --executor-cores 4
问题虽然得到了解决,但仍有些不太明白的地方:
- driver不做任何计算和存储,只是下发任务与yarn资源管理器和task交互,一般只需要给4-6g即可。为什么调大了driver-memory的内存,问题得到了解决?
- 关于JVM的OOM是如何产生的?
关于第一个问题,可以参考此文章的相关论述。
关于第二个问题,我们需要了解JVM的内部机制:
这个异常表示您的Java程序在运行的时候, 98%的时间都在执行GC回收, 但是每次回收只回收不到2%的空间!
换句话说,其实这个异常往往是抛出java.lang.OutOfMemoryError: Java heap space异常的前兆! 因为Java程序每次都GC回收只能回收一点点内存空间,而你的程序却仍然在不停的产生新的对象实例, 这无疑导致了两种可能结果:
- 不停的进行GC
- 直接超出的堆内存大小
这个问题还有一些细节需要我们去掌握,我们先从下面的例子来看吧
public static void main(String args[]) throws Exception {
Map map = System.getProperties();
Random r = new Random();
while (true) {
map.put(r.nextInt(), "value");
}
}
以上代码说明: 这段代码不停的往map中加入新的key-value,导致map大小不断变大! 当到达堆内存顶点的时候,GC发生, 但是清理完毕后,JVM发现清理前后的堆内存大小改变很小,不到2%; 这时候程序继续运行,继续往map中加数据!GC又发生了!又只清理不到2%! 如此不停的循环, 最后JVM得出了一个判断! 你的Java程序在占用CPU进行运算的时间里,98%的时间都特么的在垃圾回收,而每次GC居然只能回收堆内存的2%空间, 这肯定是代码存在问题,于是抛出了这个异常. 如果这个时候,你断定不是自己的代码问题, 使用JVM参数-XX:-UseGCOverheadLimit来关闭这种检查! 然后你就会发现你的程序抛出了堆溢出异常! 为什么呢? 因为堆内存不断的被占满,最终导致最后一次加入新的int的时候, 堆内存空间直接不足了!
解决方法:
- JVM参数
JVM给出一个参数避免这个错误:-XX:-UseGCOverheadLimit。
但是,这个参数并不是解决了内存不足的问题,只是将错误发生时间延后,并且替换成java.lang.OutOfMemoryError: Java heap space。 - 堆内存
还有一个偷懒的方法是:增大堆内存。既然堆内存少了,那就增加堆内存即可。
但是,这个方法也不是万能的。因为程序里可能有内存泄露。这个时候即使再增大堆内存,也会有用完的时候。
所以前两个方法都只是治标不治本而已。 - 终极方法
其实还是有一个终极方法的,而且是治标治本的方法,就是找到占用内存大的地方,把代码优化了,就不会出现这个问题了。
怎么找到需要优化的代码呢?就是通过heap dump生产jvm快照,通过分析快照找到占用内存大的对象,从而找到代码位置。
通过设置-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=heapdump参数来生产快照,然后通过VisualVM或者MAT等工具分析快照内容进行定位。通过这个参数是将发生OOM时的堆内存所有信息写入快照文件,也就是说,如果此时堆内存中有敏感信息的话,那就可能造成信息泄漏了。