以前對標記和清除垃圾收集的介紹主要是理論性的。在實踐中,須要進行大量的調整,以適應真實的場景和需求。舉個簡單的例子,爲了安全地繼續分配對象,JVM還須要作如下事情。java
每次清理垃圾時,JVM必須確保填充了不可訪問對象的區域能夠從新使用。這可能會致使內存碎片,這與磁盤碎片相似,會致使兩個問題:算法
爲了不這些問題,JVM正須要確保碎片不會失控。所以,在垃圾收集期間也會發生「內存碎片整理」過程,而不只僅是標記和清理。這個過程當中須要將全部可訪問的對象彼此從新定位,消除(或減小)碎片。下面是一個例子:安全
前面說過,執行垃圾收集時須要徹底中止應用程序。很明顯,對象越多,收集全部垃圾所需的時間就越長。可是若是咱們有可能使用更小的內存區域呢?有研究人員調查了這種可能性,發現應用程序中的大多數分配能夠分爲兩類:bash
這些觀察結果造成了一個弱分代假設。根據這個假設,虛擬機中的內存被分爲年輕代( Young Generation)和老年代(Old Generation)。老年代有時也被稱爲永久代。性能
擁有這樣獨立的、單獨的可清理區域容許使用許多不一樣的算法,這些算法在提升GC性能方面取得了長足的進步。優化
這並非說這種方法沒有問題。首先,來自不一樣代的對象實際上可能彼此有引用,這些引用在收集代時也被視爲「事實上的」GC根。spa
但最重要的是,分代假設可能並不適用於某些應用。因爲GC算法是針對「早逝」或「可能永遠存在」的對象優化的,因此JVM對那些「中等」預期壽命的對象的回收表現很是糟糕。線程
稍微瞭解JVM的對堆中內存池的劃分應該很熟悉。不太常見的是垃圾收集如何在不一樣的內存池中執行其職責。在不一樣的GC算法中,一些實現細節可能會有所不一樣,可是本章中的概念其實是相同的。code
Eden區一般是內存中建立對象時分配對象的區域。因爲一般有多個線程同時建立許多對象,Eden區中進一步劃分爲一個或多個線程本地分配緩衝區(簡稱TLAB),這些TLAB緩衝區因爲是線程獨享的,所以容許JVM在TLAB中直接爲一個線程分配對象,避免了與其餘線程進行昂貴的同步操做。cdn
當在TLAB中分配對象失敗時(一般是由於那裏沒有足夠的空間),分配將轉移到共享的Eden區。若是沒有足夠的空間,就會觸發年輕代中的垃圾收集過程來釋放更多的空間。若是垃圾收集也沒有在Eden中產生足夠的空閒內存,那麼對象將在老年代中分配。
當Eden區進行垃圾回收時,GC Roots遍歷全部可訪問的對象,並將它們標記爲活動對象。
以前提到到對象能夠可能存在跨代的連接,因此一種直接的方法是檢查其餘區對Eden區的全部引用。不幸的是,這樣作將首先破壞分代的意義。JVM有一個訣竅:卡片標記。本質上,JVM只是標記了Eden中「髒」對象的粗略位置,這些對象可能與老年代對象有連接。
標記階段完成後,Eden區中的全部活動對象都被複制到一個Survivor區。整個Eden區如今是空的,能夠重用它來分配更多的對象。這種方法稱爲「標記和複製」:活動對象被標記,而後複製(而不是移動)到倖存者空間。
緊挨着Eden區的是兩個Survivor區,分別稱爲from和to。須要注意的是,兩個Survivor區中的一個老是空的。
當進行新生代收集的時候,空的緊挨着Eden區的是兩個Survivor區將開始有數據。新生代的全部活動對象(包括Eden空間和Survivor區中非空的「from」空間)都被複制到Survivor區中「to」空間。在這個過程完成以後,「to」區如今包含對象,而「from」不包含對象。此時他們的角色互換了。
在兩個Survivor區之間複製活動對象的過程會重複幾回,直到某些對象被認爲已經成熟而且「足夠老」爲止。請記住,根據分代假設,已經存在一段時間的對象預計將繼續使用很長時間。
這樣的「終身」對象就能夠推廣到老年代。當這種狀況發生時,對象不會從一個Survivor空間移動到另外一個Survivor空間,而是移動到老年代空間,它們將駐留在老年代空間中,直到沒法訪問爲止。
爲了肯定對象是否「足夠老」到能夠考慮將其傳播到老年代空間,GC跟蹤特定對象存活的集合數量。在每一代對象完成一次GC以後,仍然存活的對象的年齡就會增長。當年齡超過必定的閾值時,對象將被提高到老年代空間。
老年代內存空間的實現要複雜得多。老年代一般要大得多,而且被佔用的對象不太多是垃圾。
老一代的GC比年輕一代發生的頻率要低。並且,因爲大多數對象都被認爲是老一代的活對象,因此沒有標記和複製發生。相反只是移動對象以最小化碎片。清理老年代空間的算法一般創建在不一樣的基礎上。原則上,所採起的步驟以下:
在Java 8以前,有一個特殊的空間稱爲「永久代」,存放的是類信息之類的元數據。此外,一些額外的東西,如內部化字符串,保存在永久代中。永久代實際上給Java開發人員帶來了不少麻煩,由於很難預測這些數據到底須要多少空間。這些失敗預測的結果以 java.lang.OutOfMemoryError: Permgen space的形式出現。除非這種OutOfMemoryError錯誤的緣由是實際的內存泄漏,不然解決這個問題的方法就是簡單地增長與的permgen大小,以下:
java -XX:MaxPermSize=256m com.mycompany.MyApplication
複製代碼
因爲預測永久代數據的需求是一項複雜且不方便的工做,因此在Java8中刪除了永久代,代之以元數據空間。此時,大多數雜項都被移到了常規Java堆中。
然而,類定義如今被加載到稱爲Metaspace中。它位於本機內存中,不會干擾常規堆對象。默認狀況下,元數據空間大小僅受Java進程可用的本機內存數量的限制。這將使開發人員避免嚮應用程序中再添加一個類就會致使 java.lang.OutOfMemoryError: Permgen space錯誤的問題。
請注意,擁有這樣看似無限的空間並非沒有代價的——讓元空間不受控制地增加,可能會引入大量swap操做並致使本機分配失敗。
若是仍然但願在這種狀況下保護程序,能夠限制元數據空間的增加,好比限制元空間大小爲256 MB:
java -XX:MaxMetaspaceSize=256m com.mycompany.MyApplication
複製代碼