前言:本以为(OutOfMemoryError)OOM问题会离我们很远,但在一次生产上线灰度的过程中就出现了Java.Lang.OutOfMemoryError:Java heap space异常,通过对线上日志的查看,最终定位到ArrayList#addAll方法中,出现这个问题的原因是:由于历史原因有个接口的响应时间经常超时,所以笔者对其进行了优化,之前使用的是ArrayList#add方法,笔者通过一系列修改后将add方法修改为了addAll方法,导致内存溢出。但具体是怎样产生的呢,下面对其详细分析。


ArrayList的内部原理

谈起ArrayList想必大家在日常中经常使用,用于存储一系列的元素。由于笔者在使用过程中出现了OOM异常,这里有必要对其内部原理进行简单的分析:

#1.ArrayList底层采用数组来存储数据,查找速度快,毕竟直接使用数组下标进行数据的查找。这里有一点特别重要其内部的数据存储结构为数组。

#2.数组:数组是一种线性表数据结构,它是一组连续的内存空间。注意:一组连续的内存空间,这就意味着在申请数组时如果不能满足连续的内存空间,哪怕是内存足够也会导致OOM问题。

#3.ArrayList的默认容量为10,超过10时,会进行扩容:int newCapacity = oldCapacity + (oldCapacity >> 1);相当于扩大为原来的1.5倍。其扩容函数如下:

  private void grow(int minCapacity) {
// overflow-conscious code
// 获得当前ArrayList的大小
int oldCapacity = elementData.length;
// 进行扩容,扩大为原来的1.5倍,那为什么不直接*1.5呢,因为位操作速度更快
int newCapacity = oldCapacity + (oldCapacity >> 1);
// minCapacity参数为扩容前确认的数组大小参数,将在下面进行分析
// 如果新容量比minCapacity小,说明容量不够,则使用minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
// 如果newCapacity大于最大ArrayList承受的最大值,则计算最大值
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
// 进行扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}

分析:上述扩容函数涉及到几个变量minCapacity、MAX_ARRAY_SIZE,下面将对其进行解释。

关于minCapacity变量通过ArrayList#addAll函数进行分析(add函数其实一样):

     public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
// 获取要插入集合的长度
int numNew = a.length;
// 确认容量大小,扩容也就是在该函数中进行操作
ensureCapacityInternal(size + numNew); // Increments modCount
// 将要插入的数据拷贝至数组尾部
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
     private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
} private void ensureExplicitCapacity(int minCapacity) {
modCount++; // overflow-conscious code
// 所需容量大于当前数组容量,则进行扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}

分析:

#1.ArrayList的扩容入口就是ensureCapacityInternal函数,其入参为当前ArrayList存储容量与要处理集合容量的和

#2.然后通过calculateCapacity函数进行容量确认:

    private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果当前数组为空,则从默认值(10)与minCapacity(当前ArrayList容量+要插入集合容量之和)中取最大值
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 否则直接返回minCapacity
return minCapacity;
}

#3.在ensureExplicitCapacity函数中进行具体扩容,也就是调用grow函数。

在grow函数中有一个变量需要注意一下MAX_ARRAY_SIZE:

注释已讲的非常清楚:尝试去分配最大容量的数组内存也许会造成OOM异常。

还有这里为什么要用Integer.MAX_VALUE-8呢,因为数组在虚拟机中存储时需要8字节来存储其自身的大小。

#4.ArrayList的扩容是通过Array.copyOf函数进行的:

   public static <T> T[] copyOf(T[] original, int newLength) {
// original需要被拷贝的原数据集合
// newLength新的数组长度
return (T[]) copyOf(original, newLength, original.getClass());
}
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
@SuppressWarnings("unchecked")
// 申请内存空间,如果这里没有连续的内存空间,则会抛出OOM异常
T[] copy = ((Object)newType == (Object)Object[].class)
? (T[]) new Object[newLength]
: (T[]) Array.newInstance(newType.getComponentType(), newLength);
// 将原数组拷贝到新空间中
System.arraycopy(original, 0, copy, 0,
Math.min(original.length, newLength));
return copy;
}

分析:

关键在上述代码第8行中,申请新的内存空间,由于是数组,需要连续的内存空间,如果当前无连续的内存空间,哪怕内存足够也会抛出OOM异常。

通过对ArrayList的源码分析,就可以得出出现OOM原因的关键点了。这里贴上当时灰度环境JVM的堆内存走势图:

从以上JVM监控图可以清楚的看到堆内存从0直接飙到了2G,在2G后出现了OOM异常,并且此时JVM进行了垃圾回收,幸好没有把当前节点拖崩,万幸!!!

在同样的数据量下为什么用add未抛OOM异常,而用addAll确抛了OOM异常呢

在同样数据量的情况下,之前的代码使用了ArrayList#add方法未出现问题,而使用ArrayList#addAll方法却抛出了OOM异常呢,通过源码进行比较:

ArrayList#add:

ArrayList#addAll

通过对源码进行比较可知,ArrayList#add方法每次容量确定:size+1,而ArrayList#addAll每次是size+numNew(要插入的容量)。在ArrayList#add方法插入数据进行扩容时,每次都是扩容器为其1.5倍,并且扩容并不是那么频繁,需要达到临界点,而ArrayList#addAll不确定,需要依赖numNew大小。

在使用ArrayList#addAll方法时,如果插入集合的过大,而且该方法处于循环中,就会导致扩容非常的频繁,在JVM来不及进行垃圾回收的情况下,就会导致OOM异常。

最终的解决方法:在初始化ArrayList的时候,尽量知道所需存储元素的容量或者避免其频繁扩容,就有很大的机会避免OOM异常,笔者的解决方法就是如此。在通过其他途径得知了每次的ArrayList大小,最终解决了这个问题,由于是公司代码,这里就不贴具体代码了,其实在灰度时也把我吓了一跳。

总结

本文来源于笔者在生产环境中遇到的问题(线上数据量太大,在QA环境中并未出现该问题),通过对ArrayList源码的分析,最终找到问题出现的核心点,通过及时的修改,再次上线后该问题得到解决,因此特别记录下该问题,并以此为戒。

#1.在使用ArrayList的时候,尽量对其进行容量大小的初始化,避免其频繁扩容,造成OOM异常,线上出现该问题真的很恐怖。

#2.出现问题也不要过于惊慌,及时发现问题,并解决,也许你会有不小的收获。

#3.本次问题幸好出现在灰度环境,并未全量,这是不幸中的万幸,下次一定注意、注意、注意!!!


by Shawn Chen,2019.07.14日,下午。

记一次ArrayList产生的线上OOM问题的更多相关文章

  1. 记一次log4j日志导致线上OOM问题案例

    最近一个服务突然出现 OutOfMemoryError,两台服务因为这个原因挂掉了,一直在full gc.还因为这个问题我们小组吃了一个线上故障.很是纳闷,一直运行的好好的,怎么突然就不行了呢... ...

  2. 一次线上OOM故障排查经过

    转贴:http://my.oschina.net/flashsword/blog/205266 本文是一次线上OOM故障排查的经过,内容比较基础但是真实,主要是记录一下,没有OOM排查经验的同学也可以 ...

  3. 【转】又一次线上 OOM 排查经过

    又一次线上OOM排查经过 最近线上一个服务又出现了频繁Full GC的情况,导致提供的业务经常超时.问题出现非常不稳定,经过两周的时候,终于又捕捉到了一次Full GC,于是联系运维做Heap Dum ...

  4. 火山引擎MARS-APM Plus x 飞书 |降低线上OOM,提高App性能稳定性

    通过使用火山引擎MARS-APM Plus的memory graph功能,飞书研发团队有效分析定位问题线上case多达30例,线上OOM率降低到了0.8‰,降幅达到60%.大幅提升了用户体验,为飞书的 ...

  5. 记一次令人窒息的线上fullgc调优

    今天第二篇采坑了... ... 现场因为处理太急促没有保留,而且是一旁协助,没有收集到所有信息实在是有些遗憾...只能靠记忆回想一些细节 情况是一台服务器一启动就开始full gc,短短1分钟可以有几 ...

  6. 记Booking.com iOS开发岗位线上笔试

    今晚参加了Booking的iOS职位线上笔试,结束后方能简单归纳一下. 关于测试内容: Booking采用了HackerRank作为测试平台,测试总时长为75分钟,总计4道题. 测试之前我很紧张,因为 ...

  7. 记一次线上 OOM 和性能优化

    大家好,我是鸭血粉丝(大家会亲切的喊我 「阿粉」),是一位喜欢吃鸭血粉丝的程序员,回想起之前线上出现 OOM 的场景,毕竟当时是第一次遇到这么 紧脏 的大事,要好好记录下来. 1 事情回顾 在某次周五 ...

  8. 记一次asp.net core 线上崩溃解决总结

    1.首先要先准备好环境,安装lldb 工具 要安装3.9版本的,因为每个版本对应dnc版本不一样,3.9的支持2.2 版本,然后确定分析的机器里dnc 版本和线上的生产环境是否一致,自己安装比较费劲, ...

  9. 记一次线上OOM问题分析与解决

    一.问题情况 最近用户反映系统响应越来越慢,而且不是偶发性的慢.根据后台日志,可以看到系统已经有oom现象. 根据jdk自带的jconsole工具,可以监视到系统处于堵塞时期.cup占满,活动线程数持 ...

随机推荐

  1. JVM锁优化以及区别

    偏向所锁,轻量级锁都是乐观锁,重量级锁是悲观锁. 首先简单说下先偏向锁.轻量级锁.重量级锁三者各自的应用场景: 偏向锁:只有一个线程进入临界区: 轻量级锁:多个线程交替进入临界区: 重量级锁:多个线程 ...

  2. orangepi香橙派安装VNC Viewer远程桌面

    用ssh连接实在没有图形界面操作的好,虽然命令会快,但是很多命令都记不住. 第一步: sudo apt-get install xfce4 第二步: sudo apt-get install vnc4 ...

  3. 支付宝手机网站支付(基于Java实现支付宝手机网站支付)

    支付宝支付核心需要的参数是(APPID,PRIVATE_KEY,ALIPAY_PUBLIC_KEY) APPID:创建应用后就有的APPID. PRIVATE_KEY:应用私钥 ALIPAY_PUBL ...

  4. Linux 之 压缩解压缩

    Linux中常见的压缩格式 .zip            .gz             .bz2           .tar.gz      tar.bz2 zip zip格式的压缩文件和win ...

  5. oracle concepts学习

    祭图一张!!!

  6. Django:ContentType组件

    一.项目背景 二.版本一 三.版本二 三.终极版(使用ContentType) 一.项目背景 luffy项目,有课程有学位课(不同的课程字段不一样),价格策略 问题:1.如何设计表结构,来表示这种规则 ...

  7. 《3+1团队》第七次作业:团队项目设计完善&编码

    项目 内容 这个作业属于哪个课程 任课教师博客主页链接 这个作业的要求在哪里 作业链接地址 团队名称 3+1团队 团队博客地址 https://home.cnblogs.com/u/3-1group ...

  8. NodeJS 开发博客(五) 使用express脚手架

    1 安装脚手架 npm i express-generator -g 2 使用 express 命令 生成 项目 express-test   express express-test 3. npm ...

  9. dt7.0百度熊掌当天主动推送方法

    因自己没事新做了一个网站,申请了一个熊掌号,所以做了这个主动推送接口,希望能收录快些,在此分享下关于DT7.0主动当天推送功能 上代码: <?php /* 百度当天主动推送熊掌功能 作者:68喜 ...

  10. Mac卸载Python

    推荐使用 Homebrew 来安装第三方工具 自己安装的python散落在电脑各处,删除起来比较麻烦 今天在此记录一下删除的过程(以Python3.6为例) 删除Python 3.6 framewor ...