什麼是垃圾回收?java
垃圾回收是追蹤全部正在被使用的對象,並標註剩餘的爲garbage。這裏咱們先從JVM的GC是如何實現的提及。程序員
手動內存管理算法
在開始介紹垃圾回收以前,咱們先複習一下手動內存管理。它是指你須要明確的爲你的數據手動分配須要的空閒內存,可是若是用完後忘了free 掉這些內存,則以後也沒法再次使用這部份內存。也就是說,這部份內存是屬於被申明但未被繼續使用。這種狀況稱爲一個memory leak(內存泄漏)編程
下面是一個C語言寫的一個例子,使用手動管理內存:緩存
int send_request(){ size_t n = read_size(); int *elements = malloc(4 * sizeof(int)); if(read_elements(n, elements) < n){ // elements not freed return -1; } free(elements) return 0; }
忘記free memory 多是一件至關常見的事情。Memory Leak在過去也是一個較爲常見的問題,並且僅能經過修改代碼才能徹底解決此問題。因此,一個更好的方法是:自動回收未被用的內存,減小人自己可能犯錯的可能性。這種自動的機制就是垃圾回收(簡稱GC)安全
智能指針app
自動垃圾回收的第一種方法基於reference counting(引用計數)。對每一個object,簡單的計算它被引用了多少次,若是次數爲0,則這個object能夠被安全的回收。一個很著名的例子是C++的shared pointers:編程語言
int send_request() { size_t n = read_size(); shared_ptr<vector<int>> elements = make_shared<vector<int>>(); if(read_elements(n, elements) < n) { return -1; } return 0; }
shared_ptr 用於追蹤object被引用的數量。這個值會在object被傳遞時增長,在離開域時減小。一旦這個引用數量值到達0,shared_ptr 會自動刪除它底層的vector。然而,這個例子在實際使用中並不廣泛,不過做爲一個展現的例子足夠了。優化
自動內存管理spa
在上面的C++ 代碼中,咱們已經明確的說明了何時咱們須要考慮好內存管理。若是咱們讓全部的object都使用這種自動回收內存的方式的話,確定會方便開發人員作開發,由於他們不須要再去手動釋放一些objects。Runtime會自動獲取到哪些內存不會再被使用,並釋放這些內存。換句話說,它會自動收集這部分垃圾。
第一個垃圾回收器在1959年建立,用於Lisp語言,而且從那時候開始,垃圾回收的技術纔有進展。
引用計數法
上面介紹的C++ 的shared pointers 能夠被應用到全部的objects。不少語言例如Perl,Python或PHP都使用了這種方法。下面的圖片很好的展現了這個方法:
綠色的小云表示它們指向的objects仍然在被程序員使用。從技術層面來講,這些多是正在執行的方法中的局部變量,或是靜態變量等等。它可能在不一樣的編程語言中有不一樣的場景,這裏咱們不作進一步探討。
藍色的小環表明內存裏當前活躍的objects,上面的數字表示它的引用計數。最後,灰色的小環表示沒有被任何當前在使用的object(也就是之別被綠色的小云引用的)引用的objects。也就是說,灰色的小環就是須要被垃圾回收器清理的垃圾。
這個方法看起來好像很不錯,可是它有一個很大的問題,即:若是是存在一個獨立的有向迴環的話,則這些object永遠不會被回收,例如:
紅色的圓環實際上是須要被收集的垃圾,可是因爲相互引用,引用計數不爲1,因此不會被回收。因此這個方法仍舊會形成memory leak。
也有一些方法用於克服這個問題,例如使用一個特殊的 ‘weak‘ references 或應用一個單獨的算法用於收集這些迴環。像以前提到過的語言 – Perl,Python以及PHP,它們都會以某種方式處理這種迴環並回收垃圾。固然,這部分超出了在此討論的範圍,咱們仍會以討論JVM採用的方法爲主。
標記並清除
首先,JVM對於如何跟蹤一個object會有更具體的信息,因此相對於以前模糊定義的綠色的小云,咱們如今能夠更清晰的定義那些被稱爲Garbage Collection Roots(垃圾回收根)的一系列對象:
在JVM中跟蹤全部可達的(當前活躍的)對象,並確保那些uon-reachable對象申明的內存被再次重複使用的方法,稱爲Mark and Sweep(標記並清除)算法。它包含兩個步驟:
在JVM中的不一樣的GC 算法,例如Parallel Scavenge,Parallel Mark+Copy 或CMS,都實現了上面兩個階段,可是會存在一些細微的差異。可是從概念層次上,整個過程基本與上面兩個步驟相似。
這個方法中最重要的是:解決了迴環致使內存泄漏的問題。
可是這個方法的一個不太好的點是:在collect發生時,應用的線程須要被暫時stopped(中止),由於若是狀態是一直在變化的,則引用計數便不會特別準確。當全部應用被暫時stopped,以便讓JVM能夠徹底管理這種內部活動時,這個場景被稱爲Stop The World pause。固然,STWP 發生的緣由可能會有不少種,可是GC是其中最多見的一種。
Java 裏的垃圾回收
以前對於Mark and Sweep 的垃圾回收的描述是一個最理論的介紹。在實際狀況下,爲了適應real-world的場景及需求,對此可能須要作大量的調整。做爲一個簡單的例子,下面看一下在咱們安全的持續分配對象時,JVM所須要作的各種記錄與操做。
碎片與緊縮
當 Sweeping 發生時,JVM須要確保的是unreachable 對象所佔據的空間能夠被再次使用。這個(最終必定)會產生內存碎片(相似於磁盤碎片),這樣會致使兩個問題:
爲了不這些問題,JVM會去確保碎片問題不會失控。因此,除了作Marking and Sweeping,在垃圾回收時,也會有一個「memory defrag「的工做。這個進程從新分配全部reachable 對象,將它們相鄰排列,清除掉(或是減小)內存碎片。下面是一個示意圖:
世代假說
正如以前提到過的,在作垃圾回收時,會牽涉到徹底中止應用。同時,能夠明顯確認的是:對象越多,回收垃圾的耗時越長。那咱們是否能夠只對某些小的內存區域作操做?在研究人員對此作進一步研究後,能夠發現:在應用內部,大部份內存分配發生在如下兩種場景:
這個發現促成了 Weak Generational Hypothesis。根據這個假設,VM 裏的內存被分紅兩部分,分別稱爲Young Generation和Old Generation,後者有時也被稱爲 Tenured(終身的)。
這種分離的、獨立的可清理區域,使得大量不一樣的算法能夠對GC作不少performance上的提高。固然,這並非說,這種方式徹底沒有問題。例如,不一樣generations的對象可能事實上也是有相互的引用,這樣在作垃圾回收時,它們也會被認爲是GC roots。
須要着重注意的是,世代假說可能實際上並不適用一些應用。由於GC的算法是對「die young(早逝)」或「有可能一直存在」的對象作優化,但JVM的行爲對於(被預期爲)「中期」長度生命的對象是不夠優化的。
內存池
下面是在堆內存裏對內存池的劃分,可能你們對此已經比較熟悉了。而對於GC如何在不一樣的內存池中作回收,可能比較陌生。須要注意的是,不一樣的GC算法可能在實現的層面稍有差異,可是從概念層面上,基本是一致的。
Eden(伊甸園)
在對象被建立時,會從Eden這個內存區域裏分配內存。因爲通常會有多個線程用於同時建立大量對象,Eden空間會被進一步劃分爲一個或多個 Thread Local Allocation Buffer(TLAB)(線程本地分配緩衝區)。這些緩存容許JVM在一個線程在它對應的TLAB中直接分配可以分配的最多的對象,避免了與其餘線程同步的消耗。
當在一個TLAB中沒法完成分配動做時(通常來講是因爲裏面沒有足夠的空間),分配的動做會移動到一個共享的Eden空間。若是那裏也沒有足夠的空間,則在Young Generation裏的一個垃圾回收進程會被觸發並釋放出更多的空間。若是垃圾回收也沒法在Eden裏釋放足夠的空間,則對象會被分配到Old Generation。
當Eden 正在被回收時,GC會從GC roots遍歷全部可達的對象,並將它們標註爲存活(alive)。咱們以前提到過,對象可能存在跨generation的引用,因此一個直接的方法是:檢查全部從其餘generation指向Eden的引用。可是這個可能會直接影響了以前咱們提到的世代假說(本來已將它們分爲兩部分,如今這兩部分卻有了聯繫)。
JVM裏對此作了一個優化,叫作:card-marking(卡片標記)。簡單的說,就是對於那些有被Old Generation 引用的、存在於Eden中的「髒」對象,JVM僅僅是對它作一個大體的、模糊的位置標記。
在標記階段完成後,全部在Eden中存活的對象會被複制到其中一個Survivor 空間。整個Eden如今會被認爲是空的,而且它的空間能夠被從新用於分配其餘更多的對象。這個方法稱爲「Mark and Copy」(標記並複製):活躍的對象被標記,而後被複制到(而不是移動)一個survivor 空間。
Survivor Space(倖存者空間)
鄰接Eden空間的是兩個Survivor空間,被稱爲from和to。須要注意的是,這兩個Survivor空間中的其中一個必定是一直是空的。
空的Survivor 空間會在Young Generation被回收後開始往裏面放入內容。全部從整個Young Generation(包括Eden 空間以及non-empty的「from」Survivor空間)存活的對象會被複制到「to」Survivor空間。在這個過程完成後,「to」Survivor如今會存放對象,而「from」Survivor空間沒有對象,它們的角色也會在這時作轉換。
這個在兩個Survivor空間中複製存活對象的過程會重複屢次,直到一些對象被認爲經歷的時間足夠久並「old enough」。須要注意的是,根據世代假說,存活時間較長的對象被預期是會繼續被長時間使用的。
這些長時間存活的對象所以能夠被「提高」到Old Generation。當這個過程發生時,對象並非從一個survivor空間移動到另外一survivor空間,而是被移動到了Old Generation空間。這些對象會在Old Generation空間里長久存在,直到它們unreachable。
爲了判斷一個對象是不是「old enough」並被移動到Old 空間,GC會跟蹤對象在回收後仍然存活的次數。在每一個對象的generation在GC中完成後,這些依舊存活的對象的年紀會增長。當它們的年紀超過了一個特定的「年紀閾值」後,會被移動到Old 空間。
而實際的「年紀閾值」是JVM動態調整的,可是能夠經過指定 -XX:+MaxTenuringThreshold 設置一個上限值。若將此參數設置爲0,則會致使移動到Old 空間當即生效(也就是說,不會在Survivor空間之間作複製)。默認狀況下,這個閾值在主流的JVM中是15輪GC。這也是HotSpot中的最大值。
Promotion(從young 空間移動到old空間)也能夠在對象經歷的GC輪數達到閾值前發生,若是在Young Generation中的Survivor空間不足以存下全部存活的對象的話。
Old Generation(老生代)
Old Generation內存空間的實現更爲複雜。Old Generation的空間通常會比Young Generation大得多,而且存放了那些更少可能被回收的對象。
在Old Generation中發生的GC頻率要少於Young Generation。而且,因爲大部分對象被認爲是在Old Generation中應該存活的,因此在這裏不會有Mark and Copy(標記並複製)的過程發生。取而代之的是,對象會被四處移動,以最小化內存碎片。Old 空間的清理算法通常基於不一樣的基礎。基本上,會經歷如下幾個步驟:
從上面的步驟能夠看到,在Old Generation的GC會將對象緊湊的排列,以免過分的內存碎片。
PermGen(永生代)
在Java 8 之前,會存在一個特殊的空間名爲「Permanent Generation」(永久代)。這個地方會存放一些metadata(例如classes)。同時,一些額外的東西,例如Internalized strings(常量字符串)也會存在PermGen。
可是它在過去經常會對Java 開發者產生大量的問題,由於這個區域到底一共須要多少空間是很難被預測的。而預測失敗的結果每每會致使 java.lang.OutOfMemoryError:Permgen space 的報錯。
除非真正致使這個OutOfMemory報錯的緣由是一個內存泄漏,不然修復這個問題的方法通常是增長PermGen的空間分配,例以下面的選項指定了最高容許的PermGen內存空間爲256MB:
java -XX:MaxPermSize=256m com.company.MyApplication
Metaspace(源空間)
預測metadata所需的空間是一個複雜且不方便的工做,因此Permanent Generation在Java 8 被移除了,並由Metaspace所取代。 今後,大部分雜七雜八的內容被移動到了常規的Java heap中。
可是,類的定義(class definitions),如今被加載到了名爲Metaspace的地方。它存在於本地內存而且不干擾常規堆中的對象。默認狀況下,Metaspace的空間僅僅由Java進程所擁有的本地可用內存所限制。這種方式解決了在新增長一個或多個類到應用時返回 java.lang.OutOfMemoryError:Permgen space 的問題。
須要注意的是,擁有這種看似無限制的內存空間並非沒有開銷的,若是讓Metaspace無控制的增加的話,則會引入大量的swapping操做並甚至可能觸發本地內存分配報錯。
考慮到仍舊須要對此場景作控制,咱們能夠限制Metaspace的增加,例如,限制它的大小爲256MB:
java -XX:MaxMetaspaceSize=256m com.company.MyApplication
References:
https://plumbr.io/java-garbage-collection-handbook