本文在我的技術博客不一樣步發佈,詳情可用力戳
亦可掃描屏幕右側二維碼關注我的公衆號,公衆號內有我的聯繫方式,等你來撩...html
相關連接(注:文章講解JVM以Hotspot虛擬機爲例,jdk版本爲1.8)
一、 你必須瞭解的java內存管理機制-運行時數據區
二、 你必須瞭解的java內存管理機制-內存分配
三、 你必須瞭解的java內存管理機制-垃圾標記java
前面花了兩篇文章對JVM的內存管理機制作了較多的介紹,經過第一篇文章先了解了JVM的運行時數據區,而後在第二篇文章中經過一個建立對象的實例介紹了JVM的內存分配的相關內容!那麼,萬衆矚目的JVM垃圾回收是時候登場了!JVM垃圾回收這塊的內容相對較多、較複雜。可是,想要作好JVM的性能調優,這塊的內容又必須瞭解和掌握!安全
經過上篇文章咱們知道,JVM建立對象時會經過某種方式從內存中劃分一塊區域進行分配。那麼當咱們服務器源源不斷的接收請求的時候,就會頻繁的須要進行內存分配的操做,可是咱們服務器的內存確是很是有限的呢!因此對再也不使用的內存進行回收再利用就成了JVM肩負的重任了! 那麼,擺在JVM面前的問題來了,怎麼判斷哪些內存再也不使用了?怎麼合理、高效的進行回收操做?既然要回收,那第一步就是要找到須要回收的對象!服務器
實現思路:給對象添加一個引用計數器,每當有一個地方引用它,計數器加1。當引用失效,計數器值減1。任什麼時候刻計數器值爲0,則認爲對象是再也不被使用的。舉個小栗子,咱們有一個People的類,People類有id和bestFriend的屬性。咱們用People類來造兩個小人:併發
People p1 = new People(); People p2 = new People();
經過上篇文章的知識咱們知道,當方法執行的時候,方法的局部變量表和堆的關係應該是以下圖的(注意堆中對象頭中紅色括號內的數字,就是引用計數器,這裏只是舉慄,實際實現可能會有差別):jvm
造出來的p1和p2兩我的,我想讓他們互爲最好的朋友,因而代碼以下:ide
People p1 = new People(); People p2 = new People(); p1.setBestFriend(p2); p2.setBestFriend(p1);
對應的引用關係圖應該以下(注意引用計數器值的變化):性能
而後咱們再作一些處理,去除變量和堆中對象的引用關係。this
People p1 = new People(); People p2 = new People(); p1.setBestFriend(p2); p2.setBestFriend(p1); p1 = null; p2 = null;
這時候引用關係圖就變成以下了,因爲p1和p2對象還相互引用着,因此引用計數器的值還爲1。線程
優勢:實現簡單,效率高。
缺點:很難解決對象之間的相互循環引用。且開銷較大,頻繁的引用變化會帶來大量的額外運算。在談實現思路的時候有這樣一句話「任什麼時候刻計數器值爲0,則認爲對象是再也不被使用的」。可是經過上面的例子咱們能夠看到,雖然對象已經再也不使用了,但計數器的值仍然是1,因此這兩個對象不會被標記爲垃圾。
現狀:主流的JVM都沒有選用引用計數法來管理內存。
實現思路:經過GC Roots的對象做爲起始點,從這些節點向下搜索,搜索走過的路徑成爲引用鏈,當一個對象到GC Root沒有任何引用鏈相連時,則證實對象是不可用的。以下圖,紅色的幾個對象因爲沒有跟GC Root沒有任何引用鏈相連,因此會進行標記。
優勢:能夠很好的解決對象相互循環引用的問題。
缺點:實現比較複雜;須要分析大量數據,消耗大量時間;
現狀:主流的JVM(如HotSpot)都選用可達性分析來管理內存。
經過可達性分析能夠對須要回收的對象進行標記,是否標記的對象必定會被回收呢?並非呢!要真正宣告一個對象的死亡,至少要經歷兩次的標記過程!
在可達性分析後發現到GC Roots沒有任何引用鏈相連時,被第一次標記。而且判斷此對象是否必要執行finalize()方法!若是對象沒有覆蓋finalize()方法或者finalize()已經被JVM調用過,則這個對象就會認爲是垃圾,能夠回收。對於覆蓋了finalize()方法,且finalize()方法沒有被JVM調用過期,對象會被放入一個成爲F-Queue的隊列中,等待着被觸發調用對象的finalize()方法。
執行完第一次的標記後,GC將對F-Queue隊列中的對象進行第二次小規模標記。也就是執行對象的finalize()方法!若是對象在其finalize()方法中從新與引用鏈上任何一個對象創建關聯,第二次標記時會將其移出"即將回收"的集合。若是對象沒有,也能夠認爲對象已死,能夠回收了。
finalize()方法是被第一次標記對象的逃脫死亡的最後一次機會。在jvm中,一個對象的finalize()方法只會被系統調用一次,通過finalize()方法逃脫死亡的對象,第二次不會再調用。因爲該方法是在對象進行回收的時候調用,因此能夠在該方法中實現資源關閉的操做。可是,因爲該方法執行的時間是不肯定的,甚至,在java程序不正常退出的狀況下該方法都不必定會執行!因此在正常狀況下,儘可能避免使用!若是須要"釋放資源",能夠定義顯式的終止方法,並在"try-catch-finally"的finally{}塊中保證及時調用,如File相關類的close()方法。下面咱們看一個在finalize中逃脫死亡的栗子吧:
public class GCDemo { public static GCDemo gcDemo = null; public static void main(String[] args) throws InterruptedException { gcDemo = new GCDemo(); System.out.println("------------對象剛建立------------"); if (gcDemo != null) { System.out.println("我還活得好好的!"); } else { System.out.println("我死了!"); } gcDemo = null; System.gc(); System.out.println("------------對象第一次被回收後------------"); Thread.sleep(500);// 因爲finalize方法的調用時間不肯定(F-Queue線程調用),因此休眠一下子確保方法完成調用 if (gcDemo != null) { System.out.println("我還活得好好的!"); } else { System.out.println("我死了!"); } gcDemo = null; System.gc(); System.out.println("------------對象第二次被回收後------------"); Thread.sleep(500); if (gcDemo != null) { System.out.println("我還活得好好的!"); } else { System.out.println("我死了!"); } // 後面不管多少次GC都不會再執行對象的finalize方法 } @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("execute method finalize()"); gcDemo = this; } }
執行結果以下,具體就很少說啦,不明白的就本身動手去試試吧!
經過上面可達性分析咱們瞭解了有哪些GC Root,瞭解了經過這些GC Root去搜尋並標記對象是生存仍是死亡的思路。可是具體的實現就是那張圖顯示的那麼簡單嗎?固然不是,由於咱們的堆是分代收集的,那GC Root鏈接的對象可能在新生代,也可能在老年代,新生代的對象可能會引用老年代的對象,老年代的對象也可能引用新生代。若是直接經過GC Root去搜尋,則每次都會遍歷整個堆,那分代收集就無法實現了呢!而且,枚舉整個根節點的時候是須要線程停頓的(保證一致性,不能出現正在枚舉 GC Roots,而程序還在跑的狀況,這會致使 GC Roots 不斷變化,產生數據不一致致使統計不許確的狀況),而枚舉根節點又比較耗時,這在大併發高訪問量狀況下,分分鐘就會致使系統癱瘓!啥意思呢,下面一張圖感覺一下:
若是是進行根節點枚舉,咱們先要全棧掃描,找到變量表中存放爲reference類型的變量,而後找到堆中對應的對象,最後遍歷對象的數據(如屬性等),找到對象數據中存放爲指向其餘reference的對象……這樣的開銷無疑是很是大的!
爲解決上述問題,HotSpot 採用了一種 「準確式GC」 的技術,該技術主要功能就是讓虛擬機能夠準確的知道內存中某個位置的數據類型是什麼,好比某個內存位置究竟是一個整型的變量,仍是對某個對象的reference,這樣在進行 GC Roots枚舉時,只須要枚舉reference類型的便可。那怎麼讓虛擬機準確的知道哪些位置存在的是reference類型數據呢?OopMap+RememberedSet!
OopMap記錄了棧上本地變量到堆上對象的引用關係,在GC發生時,線程會運行到最近的一個安全點停下來,而後更新本身的OopMap,記下棧上哪些位置表明着引用。枚舉根節點時,遞歸遍歷每一個棧幀的OopMap,經過棧中記錄的被引用對象的內存地址,便可找到這些對象( GC Roots )。這樣,OopMap就避免了全棧掃描,加快枚舉根節點的速度。
OopMap解決了枚舉根節點耗時的問題,可是分代收集的問題依然存在!這時候就須要另外一利器了- RememberedSet。對於位於不一樣年代對象之間的引用關係,會在引用關係發生時,在新生代邊上專門開闢一塊空間記錄下來,這就是RememberedSet!因此「新生代的 GC Roots 」 + 「 RememberedSet存儲的內容」,纔是新生代收集時真正的GC Roots(G1 收集器也使用了 RememberedSet 這種技術)。
HotSpot在OopMap的幫助下能夠快速且準確的完成GC Roots枚舉,可是在運行過程當中,很是多的指令都會致使引用關係變化,若是爲這些指令都生成對應的OopMap,須要的空間成本過高。因此只在特定的位置記錄OopMap引用關係,這些位置稱爲安全點(Safepoint)。如何在GC發生時讓全部線程(不包括JNI線程)運行到其所在最近的安全點上再停頓下來?這裏有兩種方案:
一、搶先式中斷:不須要線程的執行代碼去主動配合,當發生GC時,先強制中斷全部線程,而後若是發現某些線程未處於安全點,那麼將其喚醒,直至其到達安全點再次將其中斷。這樣一直等待全部線程都在安全點後開始GC。
二、主動式中斷:不強制中斷線程,只是簡單地設置一箇中斷標記,各個線程在執行時主動輪詢這個標記,一旦發現標記被改變(出現中斷標記)時,就將本身中斷掛起。目前全部商用虛擬機所有采用主動式中斷。
安全點既不能太少,以致於 GC 過程等待程序到達安全點的時間過長,也不能太多,以致於 GC 過程帶來的成本太高。安全點的選定基本上是以程序「是否具備讓程序長時間執行的特徵」爲標準進行選定的,例如方法調用、循環跳轉、異常跳轉等,因此具備這些功能的指令纔會產生安全點(在主動式中斷中,輪詢標誌的地方和安全點是重合的,因此線程在遇到這些指令時都會去輪詢中斷標誌!)。
使用安全點彷佛已經完美解決如何進入GC的問題了,可是GC發生的時候,某個線程正在睡覺(sleep),沒法響應JVM的中斷請求,這時候線程一旦醒來就會繼續執行了,這會致使引用關係發生變化呢!因此須要安全區域的思路來解決這個問題。線程執行進入安全區域,首先標識本身已經進入安全區域。線程被喚醒離開安全區域時,其須要檢查系統是否已經完成根節點枚舉(或整個GC)。若是已經完成,就繼續執行,不然必須等待,直到收到能夠安全離開Safe Region的信號通知!