上一節是把大概的流程給過了一遍,但是還有很多地方沒有說到,後續的慢慢會涉及到,敬請期待! 這次我們說說垃圾收集器,又名gc,顧名思義,就是收集垃圾的容器,那什麼是垃圾呢?在我們這裡指的就是堆中那些沒人要的對象。 1.垃圾收集器的由來 為什麼要有垃圾收集器啊?不知道有沒有想過這個問題,你說我運行一個程 ...
上一節是把大概的流程給過了一遍,但是還有很多地方沒有說到,後續的慢慢會涉及到,敬請期待!
這次我們說說垃圾收集器,又名gc,顧名思義,就是收集垃圾的容器,那什麼是垃圾呢?在我們這裡指的就是堆中那些沒人要的對象。
1.垃圾收集器的由來
為什麼要有垃圾收集器啊?不知道有沒有想過這個問題,你說我運行一個程式要什麼垃圾收集器啊?
隨意看一下下麵兩行代碼:
User user = new User("root","123456") user = new User("lisi","123123")
簡單畫一下記憶體圖,可以看到user這個局部變數本來是指向root這個對象,現在改為指向lisi這個對象,那麼此時這個root對象沒有人用,假如類似root這樣的對象非常多的話,那麼jvm性能就會越來越低,直至最後創建個對象可能都要十幾秒,而且堆記憶體總有一天會裝滿就會報記憶體溢出異常;
所以我們就要想辦法把類似root這種對象給清理掉,這樣才能保證jvm高效的運行;
假如虛擬機沒有提供gc你覺得會怎麼樣?其實也行,只不過你每次需要你用代碼手動釋放不需要的對象,關於這點有好處有壞處,好處就是有利於我們對堆記憶體的控制,壞處就是我們在一些比較複雜的程式之中由於手動釋放記憶體難免會出錯,但是這中錯誤還不怎麼明顯,可能要你去慢慢調試好久才能看到!
所以java就把這種工作自己處理了,讓一個gc線程一直在後臺運行,隨時準備清理不需要用的對象,雖然相當程度上會對jvm性能造成一些影響,但是由於gc太好用了,我們不用再人為的去關心垃圾對象的釋放,簡化了我們編寫程式的難度,所以這種影響程度完全可以接受!
這裡順便一提兩個基本概念,記憶體泄漏和記憶體溢出:
記憶體溢出(Memory Overflow)比較好理解,就是我們保存對象需要的空間太大了,但是申請記憶體比較小,於是裝不下,於是就會報記憶體溢出異常,比如說你申請了一個integer,但給它存了long才能存下的數,那就是記憶體溢出;專業點的說法就是:你要求分配的記憶體超出了系統能給你的,系統不能滿足需求,於是產生溢出。
記憶體泄漏(Memory Leak)指的就是我們new出來的對象保存在堆中但是沒有釋放,於是堆中記憶體會越來越少,會導致系統運行速度減慢,嚴重情況會使程式卡死;專業點的說法就是:你用malloc或new申請了一塊記憶體,但是沒有通過free或delete將記憶體釋放,導致這塊記憶體一直處於占用狀態。
對於我們jvm來說,通常情況下我們不用擔心記憶體泄漏,因為有一個強大的gc在我們程式的背後默默地為我們清理,但是也會有特殊情況,比如當被分配的對象可達但已無用(未對作廢數據記憶體單元的賦值null)即會引起,至於這個可達是什麼意思,後面會慢慢說到;
相對而言記憶體溢出我們比較常見,還有gc只會對堆記憶體進行回收,所以靜態變數是不會回收的;
再順便提一下另外兩個小概念,非守護線程(也叫用戶線程)和守護線程,看下麵這個醜陋的程式運行會有幾個線程啊?
public class User{ public static void main(String[] args){ System.out.println("我是java小新人"); } }
兩個線程,一個是執行main方法的線程,後臺還有gc執行gc的線程,在這裡,用戶線程就是執行main方法的那個線程,執行gc的線程就是守護線程,默默地守護者jvm,假如jvm是雅典娜,那麼守護線程就是黃金聖鬥士;
當用戶線程停止之後整個程式直接停止,守護線程也會終止;但是黃金聖鬥士掛了雅典娜還是可以好好活著的繼續愉快的玩耍的;
2.堆記憶體結構
哎,記憶體中的結構如果真的要通過源代碼去看,簡直讓人崩潰,除了專業搞這方面的不然真的很難懂,本來我想自己畫一下草圖了,發現太醜陋了,於是去順手借了一張圖:
途中可以很清楚的看到,整塊堆記憶體分為年輕人聚集的地方和老年人聚集的地方,年輕人比較少趨勢占用1/3空間(新生代),老年人比較多就占用2/3的空間(老年代),然而啊,年輕人又要分分類,分別是Eden區占新生代8/10,From Survivor區占新生代1/10,To Survivor區占新生代1/10,emmm。。。我特意查了一下百度翻譯,Eden---->樂園,Survivor----->幸存者;哦~~~我感覺我仿佛明白了命名人的意圖!
那麼新生代和老年代到底是乾什麼的呢?我們創建的對象是放在哪裡啊?
新生代:java對象申請記憶體和存放對象的地方,而且存放的對象都是那種死的比較快的對象,很多時候創建沒多久就清理掉了,那些活的時間比較長的對象都被移動到了老年代。
老年代:存大對象比如長字元串、數組由於需要大量連續的記憶體空間,可以直接進入老年代;還有長期存活的對象也會進入老年代,具體是多長時間呢,其實預設就是經過15 對新生代的清理(Minor Gc)還能活著的對象。
而垃圾收集器對這兩塊記憶體有兩種行為,一種是對新生代的清理,叫做Minor Gc,另外一種是對老年代的清理被叫做Major Gc。
順便提一點:很多博客中都把Major GC和Full GC說成是一種,其實還是有區別的,因為很多java虛擬機的實現不一樣,所以就有各種各樣的名稱,比如Minor Gc又叫做Young GC,Major GC也可以叫做Old GC,但是Full GC卻有點不同,Full GC 是清理整個堆空間 —— 包括年輕代、老年代和永久代(也叫做方法區)。因此 Full GC 可以說是 Minor GC 和 Major GC 的結合。當然在我們這裡,為了好理解我們也就把Full GC當作Major GC就可以了。
3.篩選清理對象
GC要工作的話,必須首先知道哪些對象要被清理,你想一下,在新生代和老年代有這麼多對象,怎麼篩選會又快又省事呢?可以有以下兩種方法
1.引用計數演算法,相當於給你創建的對象偷偷的添加一個計數器,每引用一次這個對象,計數器就加一,引用失效就減一,當這個計數器為0的時候,說明這個對象沒有變數引用了,於是我們就可以說這個對象可以被清理了
2.根搜索演算法(jvm用的就是這個),這個怎麼理解呢?你可以想象現在有一個數組,這個數組裡麵包含了一些東西的引用,我們將這個數組叫做”GC Root“,然後我們根據這個數組中的引用去找到對應的對象,看看這個對象中又引用了哪些對象,一直往下找,這樣就形成了很多線路,在這個線路上的對象就叫做”可達對象“,不在這個線路上的對象就是不可達對象,而不可達對象也就是我們要清理的對象;
其中可以作為GC Root的對象:
(1).類中的靜態變數,當它持有一個指向一個對象的引用時,它就作為root
(2).活動著的線程,可以作為root
(3).一個Java方法的參數或者該方法中的局部變數,這兩種對象可以作為root
(4).JNI方法中的局部變數或者參數,這兩種對象可以作為root
(5).其它。
關於這個根搜索演算法專業一點的說法就是:通過一系列的名為“GC Root”的對象作為起始點,從這些節點開始向下搜索,搜索所有走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Root沒有任何引用鏈相連時(用圖論來說就是GC Root到這個對象不可達時),證明該對象是可以被回收的。
4.進行垃圾回收
前面已經篩選出了我們要清理的對象,但是怎麼清理比較快呢?難道要一個一個對象慢慢刪除嘛?就好像你要清理手機中的垃圾,你會一個應用一個應用去慢慢清理數據嗎?當然不可能,這也太浪費時間了!我們當然是用手機管家或者360管家先把要清理的東西給收集起來放在一起,然後我們一清理就是全部,一個字,爽!
ok,在這裡也一樣,我們要想辦法把所有的要清理的對象給放在一起清理,有什麼辦法呢?
1.標記-----清除演算法:這種方法分為兩步,先標記然後清除,其實就是需要回收的對象標記一下,然後就是把有標記的對象全部清理即可;這種方式比較適合對象比較少的記憶體,假如對象太多標記都要好半天,更別說清除了,而且用這種方法清除的記憶體空間會東一塊西一塊,下次再創建一個大的對象可能會出問題1
2.複製演算法:按記憶體容量將記憶體劃分為等大小的兩塊。每次只使用其中一塊,當這一塊記憶體滿後將尚存活的對象複製到另一塊上去,把已經使用的那塊記憶體直接全部清理掉;這種方法最大的缺陷就是耗記憶體啊,只能用總記憶體的一半,而且如果對象很多複製都要花很多時間。
3.標記----整理演算法:結合以上兩種方法優缺點進行改良的一種方法,標記和第一種方法一樣把要清理的對象做好標記,然後把所有標記的對象移動到本記憶體的一個小角落,最後集中力量對那個小角落進行消滅
4.分代收集演算法:這是集中了上面三種方法的優點所實現的一種最好的方法,是目前大部分JVM所採用的方法,這種演算法的核心思想是根據對象存活的時間不同將記憶體劃分為不同的域,一般情況下將GC堆劃分為新生代和老年代;新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,少數對象存活,因此可以使用複製演算法;老年代的特點是每次垃圾回收時只有少量對象需要被回收,可以選用”標記--清除方法“”或者標記--整理演算法“
所以目前大部分JVM的GC都是使用分代收集演算法。
5.執行GC的步驟
前面說了這麼多無非是介紹堆的內部結構,然後怎麼找到要被清理的對象,然後為了提高效率怎麼清理最快!
現在我們就大概說說GC的清理步驟(詳細版):
1.我們創建對象的時候會進行一個判斷,極少數很大的對象直接放進老年代中,除此之外所有新創建的對象都放進新生代的Eden區中;
2.此時新生代中只有Eden區中有對象,兩個Survivor區中是空的;當我們創建了很多對象,使得Eden區快滿的時候第一次GC發生(就是執行了一次Minior GC),Eden區和”From“區(此時“From”區是空的)存活的對象將會被移動到Surviver區的“To”區,並且為每個對象設置一個計數器記錄年齡,初始值為1;每進行一次GC,會給那些存活的對象設置一個年齡+1 的操作,預設是當年齡達到15歲,下次GC就會直接把這種”老油條“丟到老年代中。
3.Minior GC之後,會進行一個比較厲害的操作,就是將”To“區和”From“換個名字,沒錯,就是換個名字,然後進行下一次Minior GC。
4.由於又創建了很多對象使得Eden區要滿了,於是又一次Minior GC,Eden區還存活的對象會直接移動到Surviver區的“To”區,此時”From“區(這裡就是交換名字之前的”To“區)中的對象有兩個地方可以去,要麼年齡滿15歲了去老年代,要麼就移動到”To“區
5.此時我們看一下,只有”To“區的對象是活著的,Eden區都是垃圾對象可以直接全部清理,”From“區是空的;不管怎樣,在進行下一次Minior GC之前保證名為”To“的Survivor區域是空的就ok了
6.當老年代中快要裝滿之後,就會進行一次Major GC,這個清理事件很慢,至少比Minior GC慢十幾倍,甚至更多,所以我們儘量要少執行Major GC
註意:如果在移動過程中”To“ 區被填滿了,剩餘的對象會被直接移動到老年代中。還有在每次Minior GC之前會先進性判斷,只要老年代裡面的連續空間大於新生代對象總大小或者歷次晉升的平均大小進行Minor GC,否則進行Major GC。
簡化版:
(1)Eden 區活著的對象 + From Survivor 存儲的對象被覆制到 To Survivor ;
(2)清空 Eden 和 From Survivor ;
(3)顛倒 From Survivor 和 To Survivor 的邏輯關係: From 變 To , To 變 From 。
(4)老年代的Major GC執行時間很長,儘量少執行
只有在Eden空間快滿的時候才會觸發 Minor GC 。而 Eden 空間占新生代的絕大部分,所以 Minor GC 的頻率得以降低。當然,使用兩個 Survivor 這種方式我們也付出了一定的代價,如 10% 的空間浪費、複製對象的開銷等。
6.知識點補充
通過查看了很多大佬的博客看到的很多有關的東西還是挺有趣的,於是簡單做個小筆記:
6.1.新創建的對象是在堆中的新生代的Eden區,由於堆中記憶體是所有線程共用,所以在堆中分配記憶體需要加鎖。而Sun JDK為提升效率,會為每個新建的線程在Eden上分配一塊獨立的空間由該線程獨享,這塊空間稱為TLAB(Thread Local Allocation Buffer)。在TLAB上分配記憶體不需要加鎖,因此JVM在給線程中的對象分配記憶體時會儘量在TLAB上分配。如果對象過大或TLAB用完,則仍然在堆上Eden區或者老年代進行分配。如果Eden區記憶體也用完了,則會進行一次Minor GC(young GC)。
6.2.很多人認為方法區(或者HotSpot虛擬機中的永久代)是沒有垃圾收集的,Java虛擬機規範中確實說過可以不要求虛擬機在方法區實現垃圾收集,而且在方法區進行垃圾收集的“性價比”一般比較低:在堆中,尤其是在新生代中,常規應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。
6.3對象調用.finalize方法被調用後,對象一定會被回收嗎?
在經過可達性分析後,到GC Roots不可達的對象可以被回收(但並不是一定會被回收,至少要經過兩次標記),此時對象被第一次標記,併進行一次判斷,如果該對象沒有調用過或者沒有重寫finalize()方法,那麼在第二次標記後可以被回收了;否則,該對象會進入一個FQueue中,稍後由JVM建立的一個Finalizer線程中去執行回收,此時若對象中finalize中“自救”,即和引用鏈上的任意一個對象建立引用關係,到GC Roots又可達了,在第二次標記時它會被移除“即將回收”的集合;如果finalize中沒有逃脫,那就面臨被回收。因此finalize方法被調用後,對象不一定會被回收。
6.4.如果在Survivor空間中相同年齡所有對象大小總和大於Survivor空間的一半,年齡大於或者等於該年齡的對象直接進入老年代。不需要等到15歲。
總結
這篇說的就是java虛擬機怎麼去收集對記憶體的垃圾,首先是要通過可達性分析判斷哪些對象是可達的,哪些是不可達的,那些不可達的對象就是我們要處理的對象!這些不可達對象可能在新生代和老年代都有,在新生代用複製演算法去處理垃圾,老年代用標記整理演算法處理垃圾,這種處理方式也可以叫做分代收集演算法!而且還簡單說了一下Minor GC和Major GC的觸發方式!
基本的東西就這麼多,假如要深入的話可以深入很多,比如我們可以控制新生代的大小,還有很多種垃圾處理器的實現產品等等,都是可以去慢慢瞭解的。