標記-清除(mark and Sweep)是最經典的垃圾回收算法
碎片整理
內存模型就像一個衣柜有很多隔斷,假如每個隔斷內部都是滿的,如果執行清理,那么最后可能內存會很亂,一三五層隔斷有東西,二四六沒東西,或者更亂。
大家可以想象一下,假如要寫入一個很大的文件,需要一個連續的內存地址來存儲。如果內存碎片很多,可能在尋找地址的時候就會浪費很多時間。
所以在JVM每次執行清理的時候會執行內存清理整理,以減少內存碎片。
分代假設
對象越多則收集垃圾的消耗時間越長,所以研究人員發現,程序中的大多可回收的垃圾歸為兩類:
- 大部分很快就不使用或立即無用
- 存活時間很長的
如果統一去處理這些垃圾,可能就會消耗更長的時間處理存活較長的垃圾,所以這些觀測形成了弱代假設。基于這一假設,VM的內存分為,年輕代(Young)和老年代(Old/Tenured)
拆分這兩個可清理的單獨區域,允許采取不同的算法從而來大幅提高GC的性能。
但是這種方法不是沒有問題,例如,不同分代中的對象可能會互相引用,這樣在收集某個分代可能就是GC root。
沒有完美的垃圾回收算法,只有好的算法,之所以這樣說是因為,GC算法專門針對“要么死得快,要么活的長”這類特征的對像來優化,JVM對于那些不長不短的對象就很<font color=red>awkword(尷尬)</font>
內存池(Memory Pools)
Java內存模型是很抽象的,很多定義也不同,網上資料各異沒有明確官方定義,但是小編感覺,只要理解是對的,有助你Java開發,那么怎么理解怎么記,就算是錯的,也會增加你對Java垃圾回收的理解,這里用一個圖來把這個抽象的東西,表述一下。
新生代(Eden,伊甸園)
Eden是內存中的一個區域,用來分配新創建的對象。通常會與多個線程同時創建多個對象,所以Eden區被劃分為多一個線程本地分配緩沖區,通過分離,避免與其他線程同步操作。
如果線程緩沖區沒有足夠內存了,就會在Eden共享區分配,如果共享區也沒有了內存,就會觸發年輕代的GC來釋放內存。如果GC之后Eden區依然沒有足夠的內存,則對象就分配到年老代,Old
當Eden去進行垃圾收集的時候,GC將從root可達的對象過一遍,并標記存活對象,標記完成后,Eden中所有存活的對象,被復制到Eden的右邊的存活區(Survivor spaces),這個時候Eden區可能就是空的,然后分配新對象,這種方法就是標記-復制,是復制而不是移動。
存活區(Survivor Spaces)
Eden 區的旁邊是兩個存活區, 稱為 from 空間和 to 空間。需要著重強調的的是, 任意時刻總有一個存活區是空的(empty)。
空的這個存活區在下一次年輕代GC是存放對象,年輕代中所有的存活對象(包括Edenq區和非空的那個 “from” 存活區)都會被復制到 ”to“ 存活區。GC過程完成后, ”to“ 區有對象,而 ‘from’ 區里沒有對象。兩者的角色進行正好切換 。
存活的對象會在倆個區多次復制,當存活區對象在經歷多次GC后依然,存活那么這個時候就可以升級為老年代Old。為了確定一個對象是否“足夠老”, 可以被提升(Promotion)到老年代,GC模塊跟蹤記錄每個存活區對象存活的次數。每次分代GC完成后,存活對象的年齡就會增長。當年齡超過提升閾值(tenuring threshold), 就會被提升到老年代區域。
具體的提升閾值由JVM動態調整,但也可以用參數-XX:+MaxTenuringThreshold
來指定上限。如果設置 -XX:+MaxTenuringThreshold=0
, 則GC時存活對象不在存活區之間復制,直接提升到老年代。
現代 JVM 中這個閾值默認設置為15個 GC周期。這也是HotSpot中的最大值。
如果存活區空間不夠存放年輕代中的存活對象,提升(Promotion)也可能更早地進行。
老年代(Old Generation)
老年代的對象是經歷了篩選進來的,老年代內存空間通常會更大,一般是垃圾的概率會相對小。
老年代的GC發生頻率比年輕代小很多,因為大部分都是存活的,所以使用的算法也不是標記和復制,對于因為是垃圾的改變會相對小,所以如果說是清理垃圾,更不說是,優化內存。但是都會經歷下面幾步:
- 通過標志位(marked bit),標記所有通過 GC roots 可達的對象.
- 刪除所有不可達對象
- 整理老年代空間中的內容,方法是將所有的存活對象復制,從老年代空間開始的地方,依次存放。
永久代(PermGen)
在Java 8 之前有一個特殊的空間,稱為“永久代”(Permanent Generation)。這是存儲元數據(metadata)的地方,比如 class 信息等。此外,這個區域中也保存有其他的數據和信息, 包括 內部化的字符串(internalized strings)等等。實際上這給Java開發者造成了很多麻煩,因為很難去計算這塊區域到底需要占用多少內存空間。預測失敗導致的結果就是產生 java.lang.OutOfMemoryError: Permgen space 這種形式的錯誤。除非 ·OutOfMemoryError· 確實是內存泄漏導致的,否則就只能增加 permgen 的大小,例如下面的示例,就是設置 permgen 最大空間為 256 MB:
java -XX:MaxPermSize=256m com.mycompany.MyApplication
元數據區(Metaspace)
既然估算元數據所需空間那么復雜, Java 8直接刪除了永久代(Permanent Generation),改用 Metaspace。從此以后, Java 中很多雜七雜八的東西都放置到普通的堆內存里。
當然,像類定義(class definitions)之類的信息會被加載到 Metaspace 中。元數據區位于本地內存(native memory),不再影響到普通的Java對象。默認情況下, Metaspace的大小只受限于 Java進程可用的本地內存。這樣程序就不再因為多加載了幾個類/JAR包就導致 java.lang.OutOfMemoryError: Permgen space. 。注意, 這種不受限制的空間也不是沒有代價的 —— 如果 Metaspace 失控, 則可能會導致很嚴重的內存交換(swapping), 或者導致本地內存分配失敗。
如果需要避免這種最壞情況,那么可以通過下面這樣的方式來限制 Metaspace 的大小, 如 256 MB:
java -XX:MaxMetaspaceSize=256m com.mycompany.MyApplication
Minor GC vs Major GC vs Full GC
以下內容原文寫的很清楚,是概念上內容,采用原文描述
垃圾收集事件(Garbage Collection events)通常分為: 小型GC(Minor GC) - 大型GC(Major GC) - 和完全GC(Full GC) 。本節介紹這些事件及其區別。然后你會發現這些區別也不是特別清晰。
最重要的是,應用程序是否滿足 服務級別協議(Service Level Agreement, SLA), 并通過監控程序查看響應延遲和吞吐量。也只有那時候才能看到GC事件相關的結果。重要的是這些事件是否停止整個程序,以及持續多長時間。
雖然 Minor, Major 和 Full GC 這些術語被廣泛應用, 但并沒有標準的定義, 我們還是來深入了解一下具體的細節吧。
小型GC(Minor GC)
年輕代內存的垃圾收集事件稱為小型GC。這個定義既清晰又得到廣泛共識。對于小型GC事件,有一些有趣的事情你應該了解一下:
- 當JVM無法為新對象分配內存空間時總會觸發 Minor GC,比如 Eden 區占滿時。所以(新對象)分配頻率越高, Minor GC 的頻率就越高。
- Minor GC 事件實際上忽略了老年代。從老年代指向年輕代的引用都被認為是GC Root。而從年輕代指向老年代的引用在標記階段全部被忽略。
- Minor GC 每次都會引起全線停頓(stop-the-world ), 暫停所有的應用線程。對大多數程序而言,暫停時長基本上是可以忽略不計的, 因為 Eden 區的對象基本上都是垃圾, 也不怎么復制到存活區/老年代。如果情況不是這樣, 大部分新創建的對象不能被垃圾回收清理掉, 則 Minor GC的停頓就會持續更長的時間。
所以 Minor GC 的定義很簡單 —— Minor GC 清理的就是年輕代。
Major GC vs Full GC
Minor GC 清理的是年輕代空間(Young space),相應的,其他定義也很簡單:
- Major GC(大型GC) 清理的是老年代空間(Old space)。
- Full GC(完全GC)清理的是整個堆, 包括年輕代和老年代空間。
悲劇的情況出現了,很多Major GC是由于Minor GC觸發的,所以很多情況下這和Full GC就沒有多大區別了,這個就很<font color=red>awkword(尷尬)</font>
這也讓我們認識到,不應該去操心是叫 Major GC 呢還是叫 Full GC, 我們應該關注的是: 某次GC事件 是否停止所有線程,或者是與其他線程并發執行。
文章參考自附錄地址,附帶小編自己的學習經歷和理解重新翻譯,希望大家共進步