若是對C++這門語言熟悉的人,再來看Java,就會發現這二者對垃圾(內存)回收的策略有很大的不一樣。html
C++:垃圾回收很重要,咱們必需要本身來回收!!!程序員
Java:垃圾回收很重要,咱們必須交給系統來幫咱們完成!!!算法
我想這也能看出這兩門語言設計者的心態吧,總之,Java和C++之間有一堵由內存動態分佈和垃圾回收技術所圍成的高牆,牆外面的人想進去,牆裏面的人想出來。服務器
本篇博客咱們就來詳細介紹Java的垃圾回收策略。編輯器
咱們知道Java是一門面向對象的語言,在一個系統運行中,會伴隨着不少對象的建立,而這些對象一旦建立了就佔據了必定的內存,在上一篇博客Java運行時內存結構中,咱們介紹過建立的對象是保存在堆中的,當對象使用完畢以後,不對其進行清理,那麼會一直佔據內存空間,很明顯內存空間是有限的,若是不回收這些無用的對象佔據的內存,那麼新建立的對象申請不了內存空間,系統就會拋出異常而沒法運行,因此必需要常常進行內存的回收,也就是垃圾收集。ide
文章開頭,咱們就說Java的垃圾回收是系統自動進行的,不須要咱們程序員手動處理,那麼咱們爲何還要了解垃圾回收呢,?函數
其實這也是一個程序員進階的過程,生產項目在運行過程當中,極可能會存在內存溢出、內存泄露等問題,出現了這些問題,咱們應該怎麼排查?以及在生產服務器有限的資源上如何更好的分配Java運行時內存區域,提升系統運行效率等,咱們必須知其然知其因此然。this
PS:本篇博客只是介紹Java垃圾回收機制,關於排查內存泄漏、溢出,運行時內存區域參數調優等會在後面進行介紹。spa
仍是結合上一篇博客Java運行時內存結構,咱們介紹了Java運行時的內存結構,其中程序計數器、虛擬機棧、本地方法棧這三個區域是線程私有的,隨線程而生,隨線程而滅,棧中的棧幀隨着方法的進入和退出而有條不紊的執行着入棧和出棧操做,這幾個區域的內存分配和回收都具有肯定性,在方法結束或線程結束時,內存也就跟着回收了,因此不須要咱們考慮。線程
那麼如今就剩下Java堆和方法區了,這兩塊區域在編譯期間咱們並不能徹底肯定建立多少個對象,有些是在運行時期建立的對象,因此Java內存回收機制主要是做用在這兩塊區域。
經過上面介紹了,咱們瞭解了爲何要進行垃圾回收以及回收哪部分的垃圾,那麼接下來咱們怎麼去區分哪些對象爲垃圾呢?
換句話來講,咱們如何判斷哪些對象還「活着」,哪些對象已經「死了」,那些「死了」的對象佔據的內存就是咱們要進行回收的。
這種算法是這樣的:給每個建立的對象增長一個引用計數器,每當有一個地方引用它時,這個計數器就加1;而當引用失效時,這個計數器就減1。當這個引用計數器值爲0時,也就是說這個對象沒有任何地方在使用它了,那麼這就是一個無效的對象,即可以進行垃圾回收了。
這種算法實現簡單,並且效率也很高。可是Java沒有采用該算法來進行垃圾回收,由於這種算法沒法解決對象之間的循環引用問題。
下面咱們就來構造一個循環引用的例子:
首先,有一個 Person 類,這個類有兩個自引用屬性,分別表示其父親,兒子。
1 package com.ys.algorithmproject.leetcode.demo.JVM; 2 3 /** 4 * Create by YSOcean 5 */ 6 public class Person { 7 8 private Byte[] _1MB = null; 9 10 public Person() { 11 /** 12 * 這個成員屬性的做用純粹就是佔據必定內存,以便在日誌中查看是否被回收 13 */ 14 _1MB = new Byte[1*1024*1024]; 15 } 16 17 18 19 private Person father; 20 private Person son; 21 22 public Person getFather() { 23 return father; 24 } 25 26 public void setFather(Person father) { 27 this.father = father; 28 } 29 30 public Person getSon() { 31 return son; 32 } 33 34 public void setSon(Person son) { 35 this.son = son; 36 } 37 }
接着,咱們經過Person類構造兩個對象,分別是父親,兒子,以下:
1 public static void main(String[] args) { 2 3 Person father = new Person(); 4 Person son = new Person(); 5 father.setSon(son); 6 son.setFather(father); 7 8 father = null; 9 son = null; 10 11 /** 12 * 調用此方法表示但願進行一次垃圾回收。可是它不能保證垃圾回收必定會進行, 13 * 並且具體何時進行是取決於具體的虛擬機的,不一樣的虛擬機有不一樣的對策。 14 */ 15 System.gc(); 16 }
首先,從第3-6行代碼,其運行時內存結構圖以下:
father對象和son對象,其引用計數第一個是棧內存指向,第二個就是其屬性互相引用對方,全部引用計數器都是2。
接着咱們看第8,9行代碼,分別將這兩個對象置爲null,也就是去掉了棧內存指向。
這時候其實這兩個對象只是本身互相引用了,沒有別的地方在引用它們,引用計數器爲1,那麼這兩個對象按照引用計數算法實現的虛擬機就不會回收,可想而知,這是咱們不能接受的。
因此Java虛擬機都沒有使用該算法來判斷對象是否存活,咱們能夠經過增長打印虛擬機參數來驗證。
咱們將上面的man函數,增長以下Java虛擬機參數,用來打印gc信息。
-verbose:gc
在IDEA編輯器中,添加方式以下:
運行結果以下:
咱們看到12201K->1088K(125952K)的輸出,表示垃圾收集GC前有12201K,回收後剩下1088K,堆的總量爲125952K,回收的內存爲12201K-1088K = 11113K。
換句話說,上面的例子Java虛擬機是有進行垃圾回收的,因此,這也間接佐證了Java虛擬機並非採用的引用計數法來判斷對象是不是垃圾。
PS:這些參數信息詳解也會在後面博客進行詳細介紹。
咱們這裏直接給出結論:在主流的商用程序中(Java,C#),都是使用根搜索算法(GC Roots Tracing)來斷定對象是否存活。
該算法思路:經過一系列名爲「GC Roots」 的對象做爲終點,當一個對象到GC Roots 之間沒法經過引用到達時,那麼該對象即可以進行回收了。
上圖Object1,Object2,Object3,Object4到GC Roots是可達的,因此不會被做爲垃圾回收。
上圖Object1,Object2,Object3這三個對象互相引用,可是到 GC Roots不可達,因此都會被垃圾回收掉。
那麼有哪些對象能夠做爲 GC Roots 呢?
在Java語言中,有以下4中對象能夠做爲 GC Roots:
1 1、虛擬機棧(棧幀中的本地變量表)中引用的對象 2 2、方法區中的靜態變量屬性引用的對象 3 3、方法區中常量引用的對象 4 四、本地方法棧中(JNI)(即通常說的Native方法)的引用的對象
垃圾回收涉及到大量的程序細節,並且各個平臺的虛擬機操做內存的方式也不同,可是他們進行垃圾回收的算法是通用的,因此這裏咱們也只介紹幾種通用算法。
算法實現:分爲標記-清除兩個階段,首先根據上面的根搜索算法標記出全部須要回收的對象,在標記完成後,而後在統一回收掉全部被標記的對象。
缺點:
一、效率低:標記和清除這兩個過程的效率都不高。
二、容易產生內存碎片:由於內存的申請一般不是連續的,那麼清除一些對象後,那麼就會產生大量不連續的內存碎片,而碎片太多時,當有個大對象須要分配內存時,便會形成沒有足夠的連續內存分配而提早觸發垃圾回收,甚至直接拋出OutOfMemoryExecption。
爲了解決標記-清除算法的兩個缺點,複製算法誕生了。
算法實現:將可用內存按容量劃分爲大小相等的兩塊區域,每次只使用其中一塊,當這一塊的內存用完了,就將還活着的對象複製到另外一塊區域上,而後再把已使用過的內存空間一次性清理掉。
優勢:每次都是隻對其中一塊內存進行回收,不用考慮內存碎片的問題,並且分配內存時,只須要移動堆頂指針,按順序進行分配便可,簡單高效。
缺點:將內存分爲兩塊,可是每次只能使用一塊,也就是說,機器的一半內存是閒置的,這資源浪費有點嚴重。而且若是對象存活率較高,每次都須要複製大量的對象,效率也會變得很低。
上面咱們說過複製算法會浪費一半的內存,而且對象存活率較高時,會有過多的複製操做,效率低下。
若是對象存活率很高,基本上不會進行垃圾回收時,標記-整理算法誕生了。
算法實現:首先標記出全部存活的對象,而後讓全部存活對象向一端進行移動,最後直接清理到端邊界之外的內存。
侷限性:只有對象存活率很高的狀況下,使用該算法纔會效率較高。
當前商業虛擬機都是採用此算法,可是其實這不是什麼新的算法,而是上面幾種算法的合集。
算法實現:根據對象的存活週期不一樣將內存分爲幾塊,而後不一樣的區域採用不一樣的回收算法。
一、對於存活週期較短,每次都有大批對象死亡,只有少許存活的區域,採用複製算法,由於只須要付出少許存活對象的複製成本便可完成收集;
二、對於存活週期較長,沒有額外空間進行分配擔保的區域,採用標記-整理算法,或者標記-清除算法。
好比,對於 HotSpot 虛擬機,它將堆空間分爲以下兩塊區域:
堆有新生代和老年代兩塊區域組成,而新生代區域又分爲三個部分,分別是 Eden,From Surivor,To Survivor ,比例是8:1:1。
新生代採用複製算法,每次使用一塊Eden區和一塊Survivor區,當進行垃圾回收時,將Eden和一塊Survivor區域的全部存活對象複製到另外一塊Survivor區域,而後清理到剛存放對象的區域,依次循環。
老年代採用標記-清除或者標記-整理算法,根據使用的垃圾回收器來進行判斷。
至於爲何要這樣,這是因爲內存分配的機制致使的,新生代存的基本上都是朝生夕死的對象,而老年代存放的都是存活率很高的對象。關於內存分配下篇博客咱們會詳細進行介紹。
理清了什麼是垃圾,怎麼回收垃圾,最後一點就是Java虛擬機什麼時候進行垃圾回收呢?
程序員能夠調用 System.gc()方法,手動回收,可是調用此方法表示但願進行一次垃圾回收。可是它不能保證垃圾回收必定會進行,並且具體何時進行是取決於具體的虛擬機的,不一樣的虛擬機有不一樣的對策。
其次虛擬機會自行根據當前內存大小,判斷什麼時候進行垃圾回收,好比前面所說的,新生代滿了,新產生的對象沒法分配內存時,便會觸發垃圾回收機制。
這裏須要說明的是宣告一個對象死亡,至少要經歷兩次標記,前面咱們說過,若是對象與GC Roots 不可達,那麼此對象會被第一次標記並進行一次篩選,篩選的條件是此對象是否有必要執行 finalize() 方法,當對象沒有覆蓋 finalize()方法,或者該方法已經執行了一次,那麼虛擬機都將視爲沒有必要執行finalize()方法。
若是這個對象有必要執行 finalize() 方法,那麼該對象將會被放置在一個有虛擬機自動創建、低優先級,名爲 F-Queue 隊列中,GC會對F-Queue進行第二次標記,若是對象在finalize() 方法中成功拯救了本身(好比從新與GC Roots創建鏈接),那麼第二次標記時,就會將該對象移除即將回收的集合,不然就會被回收。