Java記憶體管理再探究
以前寫過JVM及記憶體管理的文章,現在看來,當時對Java虛擬機器及其記憶體管理的方式還是認識不夠深。今天結合書本再次做個整理。
來源
“記憶體回收”機制很容易理解。Java語言在建立對象的時候回佔用記憶體,作為一種自我保護,避免記憶體泄露,Java提供了記憶體回收機制來回收不再使用的對象所佔用的記憶體空間。
記憶體配置方式
Java虛擬機器將自己分配的對象或者數組儲存在某種資料結構中,簡稱這種資料結構為“分配表”。同時JVM還能區分棧幀裡的局部變數指向堆裡的哪一個對象或者數組。Finally,JVM能夠追蹤到堆中對象和數組儲存的引用。
因為上述特性,JVM已經能夠判斷記憶體配置的對象在某個時刻是否依舊被對象或變數引用。在遇到不被引用的對象時,解譯器就可以回收這個對象的記憶體。
基本標記演算法
通常記憶體回收使用的方式叫做“標記演算法”。顧名思義,就是把不再佔用記憶體的對象標記出來,一一清除。具體過程如下:
迭代分配表,把所有的對象都標記為死亡。 從指向堆的局部變數開始,每次遇到對象,沿著對象的引用一直向下,每次遇到分配表中沒有的對象或數組,就標記為存活(這就是標記演算法)。一直向下,直到找出能從局部變數到達的所有引用為止。
再次執行第一步,迭代分配表。這次回收所有標記為死亡的對象佔用的記憶體,同時釋放記憶體,刪除這些死亡對象。
但是同時JVM面臨一個問題,就是在回收過程中,應用程式可能一直在執行,所以回收執行前後的對象狀態不一定一致,某個對象在回收前可能是活躍的,在回收執行之後也恰好不活躍了,這怎麼處理?
此時JVM也有最佳化機制。就是在執行回收時,應用程式進行短暫停頓(stop the world,STW),這個停頓不會影響到程式的正常執行。停頓之後,進行記憶體回收,然後繼續執行應用程式。
弱代假設(Weak Generational Hypothesis)
然而,在實際的運行環境中,對象的狀態不是均勻分布的。通常大部分對象在建立之後很早就不再被使用;同時對於舊的對象,也很少引用新的對象。因此在這裡,可以將堆分成存放舊對象和新對象的不同地區,也就是常說的老年代和新生代(這就是新生代和老年代的由來,說到底還是為了方便記憶體回收)。
複製回收演算法
此時就可以用複製演算法了,也叫篩選演算法。
來看下文軒網技術團隊的筆記:
由於新生代對象98%都是朝生夕死,故採用複製演算法回收效率最高,將新生代分為一塊Eden,二塊Survivor地區。
Eden地區用於新對象的記憶體配置。Eden記憶體配置採用bump-the-pointer技術,使用一個指標指向已指派記憶體的末尾,分配記憶體時,僅檢查剩餘記憶體是否滿足新對象分配。效率高。對於多線程記憶體配置採用Thread-Local Allocation Buffers TLABS,每個線程有自己的一塊空閑記憶體配置緩衝區。不需要任何鎖機制,只有當一個TLAB滿了以後才需要同步。
兩塊Survivor地區分為From和To,To地區用於下次新生代GC存活對象的存放地,From地區存放著至少活過一次新生代GC的對象。在一次新生代GC結束後,From變為To,To變為From。
上文說得很明顯,這種複製演算法的主要工作就是複製存活的對象,所以至始至終它處理的是活性對象,效率更高。新生代的Eden部分存放新對象,Survivor部分不停地變換From和To。此時如果存在好幾次變換後依舊活著的對象,直接移到老年代(老不死的對象…)。
此外還有持久代,主要存放類資訊什麼的(類定義,結構,欄位,方法(資料及代碼)以及常量在內的類相關資料),和記憶體回收關係不大。而且在Java 8中,持久代已經廢棄,取而代之的是元空間(metaspace),參見:Java 8的元空間 和Java 8: 從永久代(PermGen)到元空間(Metaspace)
到這裡Java的記憶體管理差不多總結完了,如果你還想瞭解其他的記憶體回收演算法,推薦閱讀這篇文字:幾種經典的記憶體回收演算法
FYI:
http://www.open-open.com/lib/view/open1413872607965.html
http://blog.jobbole.com/80499/
http://developers.winxuan.com/blog/434
http://my.oschina.net/hnuweiwei/blog/291367?p=1