Java 虛擬機器學習筆記(3)——垃圾回收機制
一. 為什麼需要“垃圾”回收
1.什麼是“垃圾”?
此處講的“垃圾”分為兩種: 廢棄常量和無用的類。
- 廢棄常量,主要是判斷當前系統中有沒有物件引用這個常量;
- 無用類則比較嚴格,需要滿足下面三個條件:
(1)該類的所有例項都已經被回收,即堆中不存在該類任何勢力;
(2)載入該類的ClassLoader已經被回收;
(3)對類對應的java.lang.Class物件沒有在任何地方被引用,無法再任何地方通過反射訪問該類的方法;
滿足了上面三個條件也僅僅是“可以”進行回收了,還要根據HotSpot的一些配置引數綜合考慮。
2.這些垃圾都在哪?
java記憶體模型,其中,程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的,因此這幾個區域的記憶體分配和回收都具備確定性,故這幾個區域就不需要過多考慮回收的問題,因為方法結束或者執行緒結束時,記憶體自然就跟著回收了。
對於java堆和方法區則不一樣,java堆是存放例項物件的地方,我們只有在程式執行期間才能知道會建立哪些物件,這部分記憶體的分配和回收是動態的,因此,垃圾收集器所關注的就是這一部分。
3.為什麼要回收這些“垃圾”?
因為這些“垃圾”都是要佔用實體記憶體的,而實體記憶體是有限的,如果不清理掉則記憶體被佔滿,程式無法繼續執行。
二. 如何確定“垃圾”?
- 明白了什麼是“垃圾”,那麼在程式執行中我們需要找出垃圾,然後對其回收,那麼怎麼找出這些“垃圾”?
一般我們有兩種方法 : 引用計數演算法 和 可達性分析演算法
引用計數演算法
演算法過程如下:
【給物件新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的】。
引用計數演算法實現簡單,判定效率也很高,大部分情況下是一個不錯的演算法。但有一個比較重要的缺點:很難解決物件之間相互迴圈引用的問題。比如:j假設變數objA、objB為某個類的物件例項,objA中持有一個指向objB的成員,此時objB的引用計數為1;在objB中持有一個指向objA的成員,此時objA的引用計數值也為1;此時,即使把objA、objB都置為null,此時兩個物件都不能被回收,因為這兩個物件雖然為null了,但是它們的引用計數值都還為1 。可達性分析演算法
目前主流的虛擬機器,如java預設虛擬機器HotSpot就是用的這種方式。演算法基本思路為:
【通過一系列的稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時(或者說從GC Roots到這個物件不可達),則證明此物件是不可用的】。
可作為GC Roots的物件包括:
1)虛擬機器棧(棧幀中的本地變量表)中引用的物件;
2)方法區中類靜態static屬性引用的物件;
3)方法區中常量final引用的物件;
4)本地方法棧中JNI(即一般說的Native方法)引用的物件;
需要注意的是,即使在可達性分析演算法中不可達的物件,也並非是“非死不可”的,要真正宣告一個物件死亡,至少要經歷兩次標記過程:如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize()方法。當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過(也就是說物件的finalize()方法只能被呼叫一次),虛擬機器將這兩種情況都視為“沒有必要執行”。
- 如果這個物件被判定為有必要執行finalize()方法,那麼這個物件將會放置在一個叫做F-Queue的佇列中,並在稍後由一個由虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它(即去執行物件的finalize()方法,這裡所謂的“執行”是值虛擬機器會觸發這個方法,但並不承若會等待它執行結束,主要是為了防止物件的finalize方法執行緩慢或發生死迴圈,導致其他物件不能被執行的,從而引起記憶體回收系統崩潰)。
- finalize()方法是物件逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功拯救自己——只需要重新與引用鏈上的任何一個物件建立關聯即可,譬如把自己(this)賦值給某個類變數或者物件的成員變數,那在第二次標記時它將被移除出“即將回收”的集合;如果物件這時候還沒有逃脫,那基本上它就真的被回收了。
- 因此對於不可達物件判定真正死亡的過程小結如下:(1)GC進行第一次標記並進行一次篩選(篩選那些覆蓋了finalize方法並且finalize方法是第一次呼叫的物件);–> (2)另一個低優先順序的執行緒去呼叫那些被篩選出來的物件的finalize方法;–> (3)GC進行第二次標記,如果在前一步中那些篩選出來的物件沒有在finalize拯救自己,此時,那些未被篩選到的和這些這些篩選到的但是沒有拯救自己的物件都將會回收。
三.怎麼回收?
垃圾回收有專門的回收演算法,基本有以下幾種。
1. 標記-清除
- 是最基礎的一種收集演算法。分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。標記過程就是上面可達性分析演算法中所講的二次標記過程。標記-清除演算法的執行過程如下圖所示:
回收前狀態:
回收後狀態:
缺點:
(1)效率問題:標記和清除的兩個過程效率都不高;
(2)空間問題:標記清除後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前出發另一次垃圾收集動作;
2.複製演算法
- 為了解決上面演算法的效率問題,複製演算法出現。它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體使用完了,就將還存活的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。
複製演算法的優點:
(1)每次都是對整個半區進行記憶體回收,實現簡單、執行也高效;
(2)在那塊使用記憶體上進行記憶體分配時,不用考慮記憶體碎片的問題,只要移動堆頂指標,按順序分配記憶體即可;
缺點:
將記憶體縮小為原來的一半,代價較高。
複製演算法的執行過程如下:
回收前的狀態:
回收後狀態
- 按照新生代的特點,新生代中的物件98%是“朝生夕死”的,因此,可以改進上面的複製演算法,目前商業虛擬機器正是用這種改進的收集演算法來回收新生代。
- 改進的收集演算法:
根據新生代的特點,我們並不需要按照1:1的比例來劃分記憶體空間,而是將記憶體劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。當回收時,將Eden和Survivor中還存活的物件一次性地複製到另外一塊Survivor空間,最後清理掉Eden和剛才用過的Survivor空間,清理完成後,剛剛被清理的Eden和另一塊在回收時放入存活物件的Survivor空間作為使用記憶體,剛被清理的Survivor作為保留空間,以便後面用來回收之用。
這種改進的收集演算法也有一個問題,就是在回收時,那塊空的Survivor空間能否放得下Eden和使用的Survivor空間中還存活的物件,如果Survivor空間不夠存放上一次新生代收集下來的存活物件,此時就需要向老年代“借”記憶體,那些剩餘未放下的物件就通過分配擔保機制進入老年代。
3.標記-整理演算法
- 複製演算法如果在物件存活率較高時,就需要進行較多次的複製操作,效率也會變低。而對於老年代中的物件,一般存活率都較高,因此需要選用其他收集演算法:標記 - 整理演算法。標記過程仍然與“標記-清除”演算法中一樣,但是在標記完成後並不直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。演算法示意圖如下:
回收前狀態;
回收後的狀態
4.分代收集演算法
- 當前商業虛擬機器都採用這個“分代收集”演算法(Generation Collection),它根據物件存活週期的不同將記憶體劃分為幾塊,一般是把java堆分為新生代和老年代,根據各個年代的特點選用不同的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,因此可以選用“複製演算法”,此時只需要付出少量存活物件的複製成本即可;對於老年代,因為物件存活率較高、也沒有額外空間為期分配擔保,就必須使用“標記-清除”或“標記-整理”演算法來進行回收。
- 是最基礎的一種收集演算法。分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。標記過程就是上面可達性分析演算法中所講的二次標記過程。標記-清除演算法的執行過程如下圖所示:
四.Java垃圾回收器
如果說上面介紹的收集演算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實現,按照上面的介紹,目前垃圾收集器基本都採用分代收集,因此一個垃圾收集器中一般都存在多種垃圾回收演算法。不同的虛擬機器提供的垃圾收集器也有很大差異,如下是HotSpot虛擬機器基於JDK1.7版本所包含的所有垃圾收集器:
HotSpot中共有7中不同的垃圾收集器,如果兩個收集器之間存在連線,說明它們之間可以搭配使用,其中,Serial、ParNew、Parallel Scavenge屬於新生代收集器,CMS、Serial Old、Parallel Old屬於老年代收集器,G1是最新的一種收集器,在新生代和老年代中都可使用。1.Serial(序列)收集器
- 最基本、發展歷史最悠久的一種收集器。看名字就知道,這個收集器是一個單執行緒的收集器,只使用一個CPU或一條收集執行緒去完成垃圾收集工作,最重要的是,在它進行垃圾收集的時候,必須暫停其他所有的工作執行緒,知道它收集結束。雖然有這個缺點,但是依然是虛擬機器執行在Client模式下的預設新生代收集器。優點是:簡單而高效,沒有執行緒互動的開銷。執行過程如圖:
新生代採用的是“複製演算法”,老年代採用的是“標記-整理”演算法。
2.ParNew收集器
- ParNew收集器其實就是Serial收集器的多執行緒版本,除了使用多條執行緒進行垃圾收集之外,其他行為和Serial收集器一樣。ParNew是許多執行在Server模式下的虛擬機器中首選的新生代收集器,其中有一個與效能無關的重要原因,除了Serial收集器外,目前只有ParNew能與老年代的CMS收集器配合使用。ParNew是一種並行的收集器。在垃圾回收中,並行是指:多條垃圾收集執行緒並行工作,使用者執行緒處於等待狀態;併發是指:使用者執行緒和垃圾收集執行緒同時執行(不一定並行,可能交替執行)。
3.Parallel Scavenge收集器
- Parallel Scavenge收集器使用的是複製演算法,也是一個並行的多執行緒收集器。和ParNew相似,但是Parallel Scavenge的關注點不同,CMS收集器的關注點是儘可能地縮短垃圾收集時使用者執行緒的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量,吞吐量 = 執行使用者程式碼時間 / (執行使用者程式碼時間 + 垃圾收集時間)。
- 最基本、發展歷史最悠久的一種收集器。看名字就知道,這個收集器是一個單執行緒的收集器,只使用一個CPU或一條收集執行緒去完成垃圾收集工作,最重要的是,在它進行垃圾收集的時候,必須暫停其他所有的工作執行緒,知道它收集結束。雖然有這個缺點,但是依然是虛擬機器執行在Client模式下的預設新生代收集器。優點是:簡單而高效,沒有執行緒互動的開銷。執行過程如圖:
上面三種都是新生代收集器,下面介紹老年代收集器。
4.Serial Old收集器
- Serial Old收集器是新生代Serial收集器的老年代版本,同樣是一個單執行緒收集器,使用“標記-整理”演算法,Serial Old的主要意義也是在於給Client模式下的虛擬機器使用。
5.Parallel Old收集器
Parallel Old是新生代收集器Prarllel Scavenge的老年代版本,使用多執行緒和“標記-整理”演算法。執行流程如下:
6.CMS收集器
- CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。對於網際網路站或者B/S系統的這種注重響應速度的服務端來說,CMS是很好的選擇。從名字Mark Sweep可以看出,CMS是基於“標記-清除”演算法實現的,分為四個步驟:
(1)初始標記(CMS initial mark):僅僅標記一GC Roots能直接關聯到的物件,這個步驟需要“stop the world”;
(2)併發標記(CMS concurrent mark):就是GC Roots進行可達性分析階段,可併發執行;
(3)重新標記(CMS remark):修正併發標記期間發生變動的那一部分物件,這個步驟需要“stop the world”;
(4)併發清除(CMS concurrent sweep):執行清除階段。
執行過程如下:
可以看到,初始標記和重新標記階段都是並行的,需要暫停使用者執行緒(過程比較短);在併發標記和併發清除階段是併發的,可以和使用者執行緒一起工作。
CMS的優點:併發收集、低停頓。
CMS的缺點:
(1)對CPU資源非常敏感,面向併發設計程式的通病,雖然不至於導致使用者執行緒停頓,但是會降低吞吐率;
(2)無法清理“浮動垃圾”,由於CMS併發清理階段使用者執行緒還在執行著,伴隨程式執行自然就還會有新的垃圾不斷出現,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次的GC;
(3)會產生大量空間碎片,因為CMS是基於“標記-清除”演算法,這種演算法的最大缺點就是會產生大量空間碎片,給分配大物件帶來麻煩,不得不提前觸發Full GC。為了解決這個問題,CMS提供了一個“-XX:+UseCMSCompaceAtFullCollection”的開關引數(預設開啟),用於在CMS收集器頂不住要進行Full GC時開啟記憶體碎片的合併整理過程。
7.G1收集器
G1收集器是最新的一款收集器,JDK1.7才釋出,是一種面向服務端應用的垃圾收集器,有如下特點:
(1)並行與併發:G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間;
(2)分代收集:分代概念在G1中依然得以保留。雖然G1可以不需其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間、熬過多次GC的舊物件以獲取更好的收集效果;
(3)空間整合:與CMS的“標記-清理”演算法不同,G1從整體看來是基於“標記-整理”演算法實現的收集器,從區域性(兩個Region之間)上看是基於“複製”演算法實現,無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片,收集後能提供規整的可用記憶體;
(4)可預測的停頓時間;使用G1收集器時,Java堆的記憶體佈局與就與其他收集器有很大差別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。
G1的收集過程分為以下幾個步驟:
(1)初始標記(Initial Marking)
(2)併發標記(Concurrent Marking)
(3)最終標記(Final Marking)
(4)篩選回收(Live Data Counting and Evacuation)
前幾個步驟和CMS有很多相似之處。執行示意圖如下:
相關推薦
Java 虛擬機器學習筆記(3)——垃圾回收機制
一. 為什麼需要“垃圾”回收 1.什麼是“垃圾”? 此處講的“垃圾”分為兩種: 廢棄常量和無用的類。 廢棄常量,主要是判斷當前系統中有沒有物件引用這個常量; 無用類則比較嚴格,需要滿足下面三個條件: (1)該類的
深入理解java虛擬機器學習筆記(一)
Java記憶體區域模型 Java虛擬機器在執行Java程式的過程中,會把它所管理的記憶體區域劃分為若干個不同的資料區域,這些區域一般被稱為執行時資料區(Runtime Data Area),也就是我們常說的JVM記憶體。 執行時資料區通常包括以下這幾個部分: 程式計數器(Program Counte
深入理解JAVA虛擬機器學習筆記(一)JVM記憶體模型
一、JVM記憶體模型概述 JVM記憶體模型其實也挺簡單的,這裡先提2個知識點: 1、組成:java堆,java棧(即虛擬機器棧),本地方法棧,方法區和程式計數器。 2、是否共享:其中方法區和堆區是執行緒共享的,虛擬機器棧,本地方法棧和程式計數器是執行緒私有的,也稱執行緒
Java虛擬機器學習筆記(一):記憶體區域與HotSpot虛擬機器物件探祕
執行時資料區域 Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。這些區域都有各自的用途,以及建立和銷燬的時間,有的區域隨著虛擬機器程序的啟動而存在,有些區域則依賴使用者執行緒的啟動和結束而建立和銷燬。根據《Java虛擬機
Java虛擬機器詳解(三)------垃圾回收
如果對C++這門語言熟悉的人,再來看Java,就會發現這兩者對垃圾(記憶體)回收的策略有很大的不同。 C++:垃圾回收很重要,我們必須要自己來回收!!! Java:垃圾回收很重要,我們必須交給系統來幫我們完成!!! 我想這也能看出這兩門語言設計者的心態吧,總之,Java和C++之間有一堵
Java虛擬機器學習筆記(位元組碼執行引擎)
執行時棧幀結構 1.區域性變量表 null JIT編譯器優化 2.運算元棧 LIFO 3.動態連結 | 4.方法返回地址 | 棧幀資訊 5.附加資訊 | —————————————————————————————————— 方法呼叫 1.解析呼叫 符號引用 靜態、私有
機器學習筆記(3):線性代數回顧
目錄 1)Matrices and vectors 2)Addition and scalar multiplication 3)Matrix-vector multiplication 4)Matrix-matrix multiplication 5)Matrix multip
機器學習筆記(3)——使用聚類分析演算法對文字分類(分類數k未知)
聚類分析是一種無監督機器學習(訓練樣本的標記資訊是未知的)演算法,它的目標是將相似的物件歸到同一個簇中,將不相似的物件歸到不同的簇中。如果要使用聚類分析演算法對一堆文字分類,關鍵要解決這幾個問題: 如何衡量兩個物件是否相似 演算法的效能怎麼度量 如何確定分類的個數或聚類
機器學習筆記(3)——K近鄰法
K-nearest neighbor(KNN) k近鄰法一種基本的分類與迴歸方法,原理和實現都比較直觀。其輸入為樣本的特徵向量,輸出為樣本的類別,可以進行多類別分類。k近鄰法是通過統計與未知樣本最近點的訓練樣本的類別來投票決定未知樣本的類別,不具有顯式的學習過
深入理解java虛擬機器閱讀筆記(一)java記憶體區域
1.1 概述 對於java來說,虛擬機器是採用的自動管理記憶體機制,不需要手動去寫delete/free程式碼,但是常在河邊走哪有不溼鞋,程式不可避免會遇到記憶體溢位或洩漏的問題,因此知道記憶體區域分佈情況對於記憶體管理是很有必要的。 1.2 執行時資料區域 java虛擬機器在執
深入理解java虛擬機器閱讀筆記(二)物件是否存活與垃圾收集演算法
1.1 判斷物件是否存活 1.1.1 引用計數演算法:給每個物件新增一個引用計數器,當一個地方引用此物件時,該計數器值+1;當引用失效時,該計數器值-1;當此物件沒有被引用時,該計數器的值為0。雖然此演算法實現簡單,效率高,但是很難解決兩個物件之間相互迴圈引用的問題。 1.1.2&
Java虛擬機器記憶體管理(二)--垃圾收集器及記憶體分配策略
概述 Java記憶體執行時區域的各個部分,其中程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅;棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。每一個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的(儘管在執行期會由JIT編
Java虛擬機器詳解(四)------垃圾收集器
上一篇部落格我們介紹了Java虛擬機器垃圾回收,介紹了幾種常用的垃圾回收演算法,包括標記-清除,標記整理,複製等,這些演算法我們可以看做是記憶體回收的理論方法,那麼在Java虛擬機器中,由誰來具體實現這些方法呢? 沒錯,就是本篇部落格介紹的內容——垃圾收集器。 1、垃圾收集
Java暑期學習筆記(3)
ring out 顯示 字節數 順序 作用 提示 string轉換 gbk # 2018.7.11 # * 1.匿名內部類(只針對重寫一個方法時候使用,不能向下轉型,因為沒有子類類名) * new Inter(){ public
《深入理解java虛擬機器》讀書筆記(三)---- 垃圾回收演算法及垃圾收集器介紹
一、垃圾回收演算法 1、標記--清除演算法 標記--清除(Mark-Sweep)演算法,分為標記和清除兩個階段,首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件,這是最基礎的收集演算法,後續很多演算法都是基於這種思想進行設計的。 標記--清除演算法主要的不足有兩點:一個
Java核心技術 卷I 基礎知識 學習筆記(3)
參考:Java核心技術 卷I 基礎知識 類之間最常見的關係有:依賴、聚合、繼承 依賴即“use-a”關係,是一種最明顯的,最常見的關係。如果一個類的方法操作另一個類的物件,就說一個類依賴於另一個類。應該儘可能地將相互依賴的類減至最少。 聚合即“has-a”關係,是一種具體且
HBase學習筆記(3)—— hbase java API
1 hbase依賴zookeeper 儲存Hmaster的地址和backup-master地址 管理HregionServer 做增刪改查表的節點 管理HregionServer中的表分配 儲存表-ROOT-的地址 hbase預設的根表,檢索表。 HRe
Kafka 學習筆記(3)——kafka java API
1 新建maven 工程 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi=
jvm學習筆記(3)——java物件的記憶體分配和物件的回收(GC)
引言: 之前的文章已經提過,java物件例項是存放在堆上的,至於是在伊甸區、存活區還是老年區,這些都是從物件回收(GC)角度來進行的邏輯劃分。所以我們先說物件的回收(GC),然後再依據GC的策略來說明新的物件具體在哪個區生成。 GC(Garbage C
深入理解Java虛擬機器學習筆記3-執行緒安全和鎖優化
併發處理是壓榨計算機運算能力最有力的工具。 1.執行緒安全 當多個執行緒訪問一個物件時,如果不用考慮這些執行緒執行時環境下排程和交替執行,也不需要進行額外的同步,或者在呼叫方進行任何其他的協調操作,呼叫這個物件的行為都可以獲取正確的結果,那麼這個物件是執行緒安全的。 2