记一次ArrayList产生的线上OOM问题

前言:本以为(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倍。其扩容函数如下:

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

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

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

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

分析:

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

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

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

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

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

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

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

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

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

分析:

关键在上述代码第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日,下午。

转载于:https://www.cnblogs.com/developer_chan/p/11184924.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/392613.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Google-Guava-EventBus源码解读

Guava是Google开源的一个Java基础类库&#xff0c;它在Google内部被广泛使用。Guava提供了很多功能模块比如&#xff1a;集合、并发库、缓存等&#xff0c;EventBus是其中的一个module&#xff0c;本篇结合EventBus源码来谈谈它的设计与实现。 概要 首先&#xff0c;我们先来预…

python之numpy

numpy是一个多维的数组对象&#xff0c;类似python的列表&#xff0c;但是数组对象的每个元素之间由空格隔开。 一、数组的创建 1.通过numpy的array(参数)&#xff0c;参数可以是列表、元组、数组、生成器等 由arr2和arr3看出&#xff0c;对于多维数组来说&#xff0c;如果最里…

git 上传

转载于:https://www.cnblogs.com/benbentu/p/6543154.html

Liferay 部署war包时候的deployDirectory 细节分析

引入&#xff1a; 在上文中&#xff0c;我们从宏观上讲解了Liferay部署war包的动作是如何触发监听器并且完成部署过程的&#xff0c;但是其中最核心的一块deployDirectory我们没讲&#xff0c;它的作用是当有了临时目录并且已经把war包的内容展开到该目录之后&#xff0c;是如何…

使用brew安装软件

brew 又叫Homebrew&#xff0c;是Mac OSX上的软件包管理工具&#xff0c;能在Mac中方便的安装软件或者卸载软件&#xff0c; 只需要一个命令&#xff0c; 非常方便 brew类似ubuntu系统下的apt-get的功能 阅读目录 安装brew 使用brew安装软件 使用brew卸载软件 使用brew查询软…

mysql 绕过select报错_MySQL注射绕过技巧(三)

在测试一次注入的时候发现过滤了逗号 所以找到这个思路第一次遇到的时候是看key哥挖洞 遇到后就想记录下来正文过滤了逗号 利用join来逐步查询select*from(select 1)a join (select 2)b join (select 3)c;例如下图逐步查询user()user() basediruser() basedir version()也可以…

Citrix、Microsoft、VMware虚拟桌面之网页接口登录对比

软件环境 Citrix Xendesktop 5.6 Microsoft Windows Server 2008 R2 Hyper-v VMware View client 4.6 首先看citrix的&#xff0c;很早之前Citrix就推出了网页的虚拟桌面和应用程序&#xff0c;默认是单点登录获取桌面 下面是微软的&#xff0c;和citrix很类似&#xff0c; 据我…

recyclerview 加载fragment_恢复 RecyclerView 的滚动位置

您可能在开发过程中遇到过这种情况&#xff0c;在 Activity/Fragment 被重新创建后&#xff0c;RecyclerView 丢失了它之前保有的滚动位置信息。通常这种情况发生的原因是由于异步加载 Adapter 数据&#xff0c;且数据在 RecyclerView 需要进行布局的时候尚未加载完成&#xff…

4.6.2 软件测试的步骤

系统测试是可有可无的。因为系统测试是和环境结合在一起。系统测试应该是在系统设计或者是需求分析阶段的前一步来完成的。 单元测试它的测试计划是在详细设计阶段完成。所以说单元测试的计划是在详细设计阶段来完成的。 模块接口的测试它保证了测试模块的数据流可以正确地流入…

栈,递归

栈的基本操作注意&#xff1a;是从后往前连接的 1 #include <stdio.h>2 #include <Windows.h>3 typedef struct sStack4 {5 int num;6 struct sStack* pnext;7 }Stack;8 void push(Stack **pStack,int num);9 int pop(Stack **pStack); 10 BOOL isEmpty(St…

mysql集群多管理节点_项目进阶 之 集群环境搭建(三)多管理节点MySQL集群

多管理节点MySQL的配置很easy&#xff0c;仅须要改动之前的博文中提高的三种节点的三个地方。1)改动管理节点配置打开管理节点C:\mysql\bin下的config.ini文件&#xff0c;将当中ndb_mgmd的相关配置改动为例如以下内容&#xff1a;[ndb_mgmd]# Management process options:# Ho…

APK伪加密

一、伪加密技术原理 我们知道android apk本质上是zip格式的压缩包&#xff0c;我们将android应用程序的后缀.apk改为.zip就可以用解压软件轻松的将android应用程序解压缩。在日常生活或者工作中&#xff0c;我们通常为了保护我们自己的文件在进行压缩式都会进行加密处理。这样的…

乱花渐欲迷人眼-杜绝设计的视噪

视噪&#xff0c;又称视觉噪音。我们每天接受来自外界的大量信息&#xff0c;这些信息有将近70&#xff05;是通过视觉感知获得的。视噪会干扰我们对信息的判断&#xff0c;影响到产品的易用性和可用性&#xff0c;与用户体验的好坏息息相关。(克劳德香农图演示了噪音如何影响信…

超详细windows安装mongo数据库、注册为服务并添加环境变量

1.官网下载zip安装包 官网地址https://www.mongodb.com/download-center/community?jmpnav&#xff0c;现在windows系统一般都是64位的&#xff0c;选好版本、系统和包类型之后点击download&#xff0c;mongodb-win32-x86_64-2008plus-ssl-4.0.10.zip。 2.解压zip包&#xff0…

.netcore mysql_.netcore基于mysql的codefirst

.netcore基于mysql的codefirst此文仅是对于netcore基于mysql的简单的codefirst实现的简单记录。示例为客服系统消息模板的增删改查实现第一步、创建实体项目&#xff0c;并在其中建立对应的实体类&#xff0c;以及数据库访问类须引入Pomelo.EntityFrameworkCore.MySql和Microso…

android 涨潮动画加载_Android附带涨潮动画效果的曲线报表绘制

写在前面本文属于部分原创&#xff0c;实现安卓平台正弦曲线类报表绘制功能介绍&#xff0c;基于网络已有的曲线报表绘制类(LineGraphicView)自己添加了涨潮的渐变动画算法最终效果图废话少说&#xff0c;直接上源码一、自定义View LineGraphicView&#xff0c;本类注释不算多&…

Oracle Study之--Oracle等待事件(5)

Db file single write这个等待事件通常只发生在一种情况下&#xff0c;就是Oracle 更新数据文件头信息时&#xff08;比如发生Checkpoint&#xff09;。当这个等待事件很明显时&#xff0c;需要考虑是不是数据库中的数据文件数量太大&#xff0c;导致Oracle 需要花较长的时间来…

Java多线程-工具篇-BlockingQueue

Java多线程-工具篇-BlockingQueue 转载 http://www.cnblogs.com/jackyuj/archive/2010/11/24/1886553.html 这也是我们在多线程环境下&#xff0c;为什么需要BlockingQueue的原因。作为BlockingQueue的使用者&#xff0c;我们再也不需要关心什么时候需要阻塞线程&#xff0c;什…

怎么连接 mysql_怎样连接连接数据库

这个博客是为了说明怎么连接数据库第一步&#xff1a;肯定是要下载数据库&#xff0c;本人用的SqlServer2008&#xff0c;是从别人的U盘中拷来的。第二步&#xff1a;数据库的登录方式设置为混合登录&#xff0c;步骤如下&#xff1a;1.打开数据库这是数据库界面&#xff0c;要…

webstorm环境安装配置(less+autoprefixer)

node安装&#xff1a; 参考地址&#xff1a;http://www.runoob.com/nodejs/nodejs-install-setup.html 1.下载node安装包并完成安装 2.在开始菜单打开node 3.查看是否安装完成&#xff08;npm是node自带安装的&#xff09; 命令&#xff1a;node -v npm -v less安装&#xff1a…