Java内存溢出(OutOfMemoryError)
前言
在正式开始讲解关于OutOfMemoryError错误之前先来了解下,我在遇到这个异常的背景。
对数据充满敬畏之心
我需要对hive中的数据进行批量操作处理,对于没有了解过hive的同学来说,有点茫然了。于是按照常规思路开始通过JDBC连接Hive读取数据 -> 处理数据 -> 写入数据。
貌似没有什么不妥的,于是乎噼里啪啦代码写好了 -> 调试 -> 验证 -> 上线批量处理数据。上线处理数据的过程出问题了,为什么半天没有处理完一条数据?
被坑了,Hive可不是MySQL能够在ms内返回结果给你,这下傻了,处理一条数据需要几分钟到十几分钟不等,简直不能忍啊。
不能忍了能怎么办,能怎么办优化解决方案呗。而是借助Hive分布式MR的能力将输入数据先处理一遍按照读取的顺序给整理好存放到中间表 -> 批量操作中间表 -> 数据写回。
开始接近了本篇要讲解的主题了,进行批量操作数据而导致OutOfMemoryError。
为什么会出现OutOfMemoryError
相信有一定编程经验的开发人员都会遇到这个错误,其实出现这个错误大家肯定想到的原因:是不是程序写的有问题产生了大量垃圾对象没法被JVM回收掉,亦或者是程序的正常逻辑确实需要用到比JVM提供的堆区内存大。
本人在遇到这个错误的时候也是这么怀疑过,于是首先去检查了下自己的代码,因为逻辑代码比较少仔细分析后发现程序写的没问题,不应该出现无法被JVM回收的内存垃圾。那怎么验证自己的代码没有出现内存垃圾?
通过 jmap -histo:live <pid>
可以分析出所有存活的对象所占用的内存大小。

发现在跳出批量处理数据的逻辑后,所有相关的内存都被回收了,所以确认没有内存垃圾。
那就只能是处理的数据超过了JVM堆区内存上限,按照这个猜测往下分析。
首先来观察下机器内存的变化情况jstat -gc <pid> 5000 100
后面两个参数一个是间隔多久统计一次,总共统计多少次。不了解可以自行搜索资料了解这个命令的输出参数。

确实内存会在一段时间后大量释放,然后随着运行又将整个堆区给占满了。到这里可以确定是由于批量处理数据太多而使线程所拥有的堆区撑爆了。
本来分析应该到这里为止了,但是得知道是什么将堆区给占满了吧?
什么将内存给占满了
首先通过Java对象内存存储结构这篇文章了解Java一个对象在内存中分配的字节数为多少。
通过jmap -histo:live <pid>
查看在内存满和不满的时候其中存活的对象。

主要暴增对象如上图框出来的地方。
TestObject定义如下:
public class TestObject { private String a; private String b; private String c; private String d; private Integer e; private Integer f; }
从TestObject的定义和上图存活对象的对比就可以判断出java.lang.String、java.lang.Integer、[C暴增的原因了。
TestObject size = 194664000/4866600 = 40
String size = 514238640/21426610 = 24
符合Java对象内存一篇文中分析的字节大小。
JVM内存波动
JVM内存管理很多前辈都已经讲的非常清楚了,根据理论我们来实际窥探下JVM是如何针对内存进行管理的,同时如何进行内存回收。

根据JVM对内存的使用策略我们可以看到程序不断使用内存的过程中堆内存容量在各个部分的波动情况,在新生代/老生代内存达到一定百分比的同时GC回收的回收情况。
触发条件:
老生代:1043574/1329152 = 0.78 (-XX:CMSInitiatingOccupancyFraction 参数控制容量达到多少进行FGC)
新生代:在Eden区满后触发
总结
针对OutOfMemoryError优化手段无非两种:
- 加大堆区内存。
- 优化自己的程序,使其在运行过程中占用内存尽可能的少。
针对OutOfMemoryError异常的具体优化措施。