標籤:手動 輸入 src 引用計數器 code 原因 計數 去掉 span
問題背景:
在本期的開發中,為了向客戶的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);}
回顧My Code,我是按照門店來取資料進行逐個發送的,如此我就迴圈建立了很多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記憶體流失問題