1. 程式人生 > >java內存泄漏問題

java內存泄漏問題

從數據 輕松 .com 頁面 對象引用 定時 java tostring 數據

問題背景:
在本期的開發中,為了向客戶的API接口每天定時發送訂貨的數據(訂貨的數據量較大,大概有800多家門店,平均每家店鋪每天大約有100條左右的訂貨數據),數據總量大約是10萬左右。這這些數據我們需要每天都在某一個時間點一次性發送給對方。

開發設計概要:
在設計時我們我們采用了一個開源的分布式任務調度框架XXL-JOB,通過簡單明了WEB頁面來操作我們的數據發送任務。

程序概要:
1.查詢獲取發生訂貨的所有門店ID;
2.循環門店ID來獲取門店的訂貨信息,並發送

問題出現發生:
在開發過程中,我們都是在測試數據庫中進行的。門店數較少,訂貨數據也只有幾十條。順利開發出來後,把程序放在模擬真實環境的服務器上,第一天程序正常運行,在晚上下班前還登錄XXL-JOB的控制頁面查看了一遍一切正常,到此我以為這個項目也就算是完成了。瞬間感覺輕松了許多,回家的腳步都輕快了不少。
第二天早上來上班後,我想看看昨晚程序的運行是否穩定,準備打開調度控制頁面。然而,當我在瀏覽器中輸入URL回車後,頁面加載了2秒,沒有出來。頓時我心中一緊,果然最終頁面提示我“無法訪問此頁面”。
通過查看服務器的的Log,在早上7點半時程序報了內存溢出錯誤。同時這時也把我的服務程序給停掉了。

分析問題:
從報內存溢出的錯誤,我們能很明確的得到導致程序死掉的原因肯定是,我的程序在運行過程中占用了大量的JVM創建的堆內存,導致。
於是我百度了一下,導致內存溢出的原因可能有如下幾種:
1.內存中加載的數據量過於龐大,如一次從數據庫取出過多數據;
2.集合類中有對對象的引用,使用完後未清空,使得JVM不能回收;
3.代碼中存在死循環或循環產生過多重復的對象實體;
4.使用的第三方軟件中的BUG;
5.啟動參數內存值設定的過小;
這時首先想到的可能就是增加JVM的堆內存空間,但這並非根本原因。根本問題還是代碼出來問題。
//逐個門店按要貨時間進行發送訂貨數據

List<Map<String, Object>> allOrderedStore=allOrderedStore(sysDate);
for (int i = 0; i < allOrderedStore.size(); i++) { String store_id=allOrderedStore.get(i).get("store_id").toString(); String receive_date=allOrderedStore.get(i).get("receive_date").toString(); //所有門店訂貨信息 List<Map<String, Object>> storeOrderDetails = jdbcSourceDAO.getOrderItemList(sysDate,store_id,receive_date); storeOrder(store_id,receive_date,storeOrderDetails); }

回顧我的代碼,我是按照門店來取數據進行逐個發送的,如此我就循環創建了很多List對象,用來存放門店的訂貨數據,或許是我太錯頻繁的創建,同時又沒有及時清空,而導致GC回收機制沒有及時回收,導致內存溢出呢?
垃圾回收機制:
在生活中,所謂垃圾,是指我們不需要的、不在使用的物品,這些物品擠占了我們的所需要的空間,
而讓我們有丟棄它,來騰出更多空間的想法。
這段對垃圾的理解中有兩個非常重要的基準,一個是物品,一個是空間。
1.堆內存:
引申到java中物品對應就是“對象”,而空間就是“java堆內存”(JVM啟動時創建的,主要用來維護運行時的數據);
此處再豐富一點,棧內存:用來存放一些基本類型的變量和對象的引用變量。
2.垃圾:
在java內存中的垃圾是指,不再存活的對象。而判斷對象是否存活主要有兩種方式:
a.引用計數:
為每一個創建的對象分配一個引用計數器,當該對象被引用時則計數器加1,去掉引用時計數器減1,如此反復。當計數器值為0時,則判定該對象“死亡”。但這種方案存在一個嚴重的問題——無法監測“循環應用”,當兩個對象互相應用時,即使它沒有被外界任何對象引用,它倆的引用計數也永遠不能為零,如此它兩就會永遠存活。

b.可達性分析:
把所有的引用對象想象成一棵樹,從樹的根節點GC Roots 出發,持續遍歷找出所有連接(引用)的樹枝對象,這些對象則是“可達”的對象,是存活的,而那些不能被遍歷找到的則是不“可達”的,是死亡的。
參考下圖,object5,object6 和 object7 便是不可達對象,視為“死亡狀態”,應該被垃圾回收器回收。

技術分享圖片

GC Roots:
首先,我們從字面上去看,GC Roots 這個根用的是“roots”是一個復數,我們不難猜測根對象是有多個的,而非唯一的,這也就說引用對象“樹”有多棵。主要的根對象有四種:
● 虛擬機棧(幀棧中的本地變量表)中引用的對象。
● 方法區中靜態屬性引用的對象。
● 方法區中常量引用的對象。
● 本地方法棧中 JNI 引用的對象。

3.垃圾回收:
通過上面的簡單介紹,已經知道了那些對象是垃圾了,現在我再來找找工具來打掃垃圾。
如下圖:
圖1
綠色塊表示可用的空間,灰色表示存活對象占用的空間,黑色表示垃圾對象占用的空間

技術分享圖片

垃圾回收的3種算法:
a.標記清理:
第一步,所謂“標記”就是利用可達性遍歷堆內存,把“存活”對象和“垃圾”對象進行標記,得到的結果如圖1;
第二步,既然“垃圾”已經標記好了,那我們再遍歷一遍,把所有“垃圾”對象所占的空間直接 清空 即可。
結果如下:
圖2

技術分享圖片

這是標記清理,簡單方便,但容易產生內存碎片。
b.標記整理:
為了彌補標記清理算法容易產生內存碎片,我們在對垃圾對象進行清理前做一次整理。把所有的存活對象移動到一起,這樣清理垃圾後就不會產生內存碎片了。
結果如下:

技術分享圖片

標記清理與標記整理沒有對存活對象做太多操作,所以這兩種方法適合對存活對象多,垃圾對象少的場景。
c.復制清空
這種方式簡單粗暴,直接把內存分為兩部分,一段時間內只允許在其中一塊內存上進行分配,當這塊內存被分配完後,則執行垃圾回收,把所有 存活 對象全部復制到另一塊內存上,當前內存則直接全部清空。
結果如下:

技術分享圖片

開始時只使用上面部分的內存,直到內存使用完畢,才進行垃圾回收,把所有存活對象搬到下半部分,並把上半部分進行清空。
這種做法不容易產生碎片,也簡單粗暴;但是,它意味著你在一段時間內只能使用一部分的內存,超過這部分內存的話就意味著堆內存裏頻繁的 復制清空。
因為這種算法需要把存活的對象從工作區復制到存儲區,這種方案適合 存活對象少,垃圾多 的情況。

知道了有三種算法可以進行垃圾回收,那麽java是選擇的那種算法進行垃圾回收呢?
因為上面的三種算法都有自己的適用場景,所以java也應該分場景來選擇回收算法。
但標記清理容易產生內存碎片,因為這個缺陷所以一般情況下應該不會選擇,那麽就只剩下了標記整理和復制清理了。
標記整理適合存活對象多的場景,復制清理適合存活對象少的場景。那麽java程序在何時存活對象多,在何時存活對象少,又有那些對象是永遠存活著的呢?

回歸我們的程序,一般程序中都會有大量的局部變量,和相對較少的全局變量。因此在程序開始運行時一定會產生很多新的存活對象,但是其中大部分的存活時間都不會太長,沒多久就會變為不可達的對象,快速死去,而另一些對象則會存活沈澱下來,存活相對長的時間,而另一些對象,比如靜態文件,這些對象的特點是不需要垃圾回收的。

根據這些特性,前輩們將java的堆內存空間做了一個簡易的劃分。
新生代:剛剛創建的對象,被統一的放在一個區域。(存活對象少,垃圾多) 適合復制清空算法
老年代:存活一段時間的對象,被統一放置的區域。(存活對象多,垃圾少) 適合標記整理算法
元空間:永久存在的對象統一放置的區域。

標記整理較好理解,但復制清空算法,由於涉及到內存空間的劃分,所以相對理解的費勁點。如下是對復制清空算法的深入探究。
在對內存空間進行劃分時最容易讓人想到的就是對半分,我們把新生代的內存空間按1:1來進行如下圖的劃分

技術分享圖片

每次只使用一半的內存,當這一半滿了後,就進行垃圾回收,把存活的對象直接復制到另一半內存,並清空當前一半的內存。
這種分法的缺陷是相當於只有一半的可用內存,對於新生代而言,新對象持續不斷地被創建,很可能很快就占滿了Eden內存而內存被占滿後就必須進行垃圾回收了,顯然持續不斷地進行垃圾回收工作,反而影響到了正常程序的運行,得不償失。

針對原始形態圖示意中的Eden內存太少,而survivor內存空間又有富余,那麽我們就來增加Eden的比例吧,讓Eden空間占9成。如下圖:

技術分享圖片

最開始在 9 的內存區使用,當 9 快要滿時,執行復制回收,把 9 內仍然存活的對象復制到 1 區,並清空 9 區。
這樣看起來是比上面的方法好了,但是它存在比較嚴重的問題。
當我們把 9 區存活對象復制到 1 區時,由於內存空間比例相差比較大,所以很有可能 1 區放不下從9區過來的存活對象,此時就不得不把對象移到 老年區 。而這就意味著,可能會有一部分 並不老 的 9 區對象由於 1 區放不下了而被放到了 老年區 ,可想而知,這破壞了 老年區 的規則。或者說,一定程度上的 老年區 並不一定全是 老年對象。
那應該如何才能把真正比較 老 的對象挪到 老年區 呢?

既然如此,那麽我們在分一個區域(SurvivorB)出來存放可能會被放入老年區的存活對象,如果一個對象在該區域存活了一定的次數(通常是15次),則我們認為這是一個正在的老年對象,此時我們便把它放入老年區。
分區圖如下:

技術分享圖片


此時再回到前面的問題,由於我是在短時間內創建了大量的占用大內存空間的對象,且此方法的棧幀長時間也不能被回收(局部變量表中沒有復用的操作);故此時我只需要在對象創建使用後手動清空,再調用GC.代碼如下:
storeOrderDetails.clear();
System.gc();

通過此方法,暫時解決了我的內存泄漏問題。

java內存泄漏問題