記一次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.pswp.cn/news/392613.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/392613.shtml
英文地址,請注明出處:http://en.pswp.cn/news/392613.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

leetcode 222. 完全二叉樹的節點個數(dfs)

給出一個完全二叉樹&#xff0c;求出該樹的節點個數。說明&#xff1a;完全二叉樹的定義如下&#xff1a;在完全二叉樹中&#xff0c;除了最底層節點可能沒填滿外&#xff0c;其余每層節點數都達到最大值&#xff0c;并且最下面一層的節點都集中在該層最左邊的若干位置。若最底…

css 計算屬性的應用_如何使用一點CSS Grid魔術設計計算器應用

css 計算屬性的應用by Deepika Gunda由Deepika Gunda 如何使用一點CSS Grid魔術設計計算器應用 (How to use a little CSS Grid magic to design a calculator app) This article is a quick intro to CSS Grid. We will be making a calculator using it.本文是CSS Grid的快速…

vc調試大全

一、調試基礎 調試快捷鍵 F5&#xff1a; 開始調試 ShiftF5: 停止調試 F10&#xff1a; 調試到下一句&#xff0c;這里是單步跟蹤 F11&#xff1a; 調試到下一句&#xff0c;跟進函數內部 ShiftF11: 從當前函數中跳出 CtrlF10: 調試到光標所在位置 F9&#xff1a; …

Google-Guava-EventBus源碼解讀

Guava是Google開源的一個Java基礎類庫&#xff0c;它在Google內部被廣泛使用。Guava提供了很多功能模塊比如&#xff1a;集合、并發庫、緩存等&#xff0c;EventBus是其中的一個module&#xff0c;本篇結合EventBus源碼來談談它的設計與實現。 概要 首先&#xff0c;我們先來預…

leetcode 1370. 上升下降字符串

給你一個字符串 s &#xff0c;請你根據下面的算法重新構造字符串&#xff1a; 從 s 中選出 最小 的字符&#xff0c;將它 接在 結果字符串的后面。 從 s 剩余字符中選出 最小 的字符&#xff0c;且該字符比上一個添加的字符大&#xff0c;將它 接在 結果字符串后面。 重復步驟…

mysql 設置事物自動提交_mysql事務自動提交的問題

1&#xff1a;mysql的aut0commit配置默認是開啟的&#xff0c;也就是沒執行一條sql都會提交一次&#xff0c;就算顯示的開啟事務也會導致多條SQL不在一個事務中&#xff0c;如果需要相關的SQL在同一個事務中執行&#xff0c;那么必須將autocommit設置為OFF&#xff0c;再顯式開…

rest laravel_如何通過測試驅動開發來構建Laravel REST API

rest laravelby Kofo Okesola由Kofo Okesola 如何通過測試驅動開發來構建Laravel REST API (How to build a Laravel REST API with Test-Driven Development) There is a famous quote by James Grenning, one of the pioneers in TDD and Agile development methodologies:T…

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;是如何…

leetcode 164. 最大間距(桶排序)

給定一個無序的數組&#xff0c;找出數組在排序之后&#xff0c;相鄰元素之間最大的差值。 如果數組元素個數小于 2&#xff0c;則返回 0。 示例 1: 輸入: [3,6,9,1] 輸出: 3 解釋: 排序后的數組是 [1,3,6,9], 其中相鄰元素 (3,6) 和 (6,9) 之間都存在最大差值 3。 示例 2: …

批處理定時mysql備份數據庫_定時備份mysql數據庫的批處理

定時備份mysql數據庫的批處理代碼&#xff0c;保存為backup_mysql.bat&#xff0c;運行即可。復制代碼 代碼如下:echo offset txt1%date:~0,4%::當前年set txt2%date:~5,2%::當前月set txt3%date:~8,2%::當前日set txt4%time:~0,2%::當前小時set txt5%time:~3,2%::當前分鐘set …

算法訓練營 重編碼_您在編碼訓練營期間可能面臨的最大挑戰

算法訓練營 重編碼by Joanna Gaudyn喬安娜高登(Joanna Gaudyn) 您在編碼訓練營期間可能面臨的最大挑戰 (The biggest struggles you might face during a coding bootcamp) You think that during a coding bootcamp nothing can be more challenging than learning programmi…

1449 砝碼稱重(思維)

題目鏈接&#xff1a;https://www.51nod.com/onlineJudge/submitDetail.html#!judgeId259281 題解&#xff1a;這題有一個技巧&#xff0c;畢竟是w^0,w^1,w^2....這樣&#xff0c;必然會想到w進制&#xff0c;而且就只能用一次。 那么就簡單了&#xff0c;把m拆成w進制&#xf…

leetcode 454. 四數相加 II(哈希表)

給定四個包含整數的數組列表 A , B , C , D ,計算有多少個元組 (i, j, k, l) &#xff0c;使得 A[i] B[j] C[k] D[l] 0。 為了使問題簡單化&#xff0c;所有的 A, B, C, D 具有相同的長度 N&#xff0c;且 0 ≤ N ≤ 500 。所有整數的范圍在 -228 到 228 - 1 之間&#xf…

“換標”Intel的窮則思變

成語有云“窮則思變”&#xff0c;用這個詞來形容早先的Intel換標也最恰當不過。當然這里“窮”&#xff0c;不是說Intel很貧窮&#xff0c;而是說Intel在自己的產業到了盡頭。Intel推產品概念的水平是一流的&#xff0c;雖然某些概念事后被認為是錯誤的&#xff08;如&#xf…

mysql開發中遇到的坑_mysql優化過程中遇見的坑(mysql優化問題特別注意)

單條查詢最后添加 LIMIT 1&#xff0c;停止全表掃描。對于char(4) 或者vachar(4)&#xff0c;無論是中文還是英文都是存儲四個字符&#xff0c;注意是字符而不是字節。如果一個字段未int類型&#xff0c;此類型只有0、1兩個狀態&#xff0c;需要為此建立索引嗎&#xff1f;過度…

初級開發人員的缺點_在您作為初級開發人員的第一年獲得此建議

初級開發人員的缺點Are you a junior developer embarking on your software development career?您是從事軟件開發事業的初級開發人員嗎&#xff1f; Or a recent computer science graduate who has recently started a new job?還是最近剛開始從事新工作的計算機科學專業…

Spark日志分析

根據tomcat日志計算url訪問了情況&#xff0c;具體的url如下&#xff0c; 要求&#xff1a;區別統計GET和POST URL訪問量 結果為&#xff1a;訪問方式、URL、訪問量 輸入文件&#xff1a; 196.168.2.1 - - [03/Jul/2014:23:36:38 0800] "GET /course/detail/3.htm HTTP/1.…

進程、線程和協程的區別

首先&#xff0c;給出“進程、線程和協程”的特點&#xff1a; 進程&#xff1a;擁有自己獨立的堆和棧&#xff0c;既不共享堆&#xff0c;也不共享棧&#xff0c;進程由操作系統調度&#xff1b;線程&#xff1a;擁有自己獨立的棧和共享的堆&#xff0c;共享堆&#xff0c;不共…