JAVA性能優化權威指南 讀書筆記(三)

HotSpot VM的垃圾收集機制

  首先我們需要明確這個知識點,我們在java中所說的垃圾收集機制指的都是在java堆中的垃圾收集。Java虛擬機規範要求所有的JVM都能適當的回收閒置內存,垃圾收集器的運行方式和執行效率對於應用的性能和相應有着極大的影響。

分代垃圾收集

  在HotSpot VM中使用分代垃圾收集器,這個邏輯基於以下事實:

      大多數分配對象的存活時間都非常短

      存活時間久的對象很少引用存輝時間短的對象

  對於大多數應用而言,這個兩個特徵能夠比較明確的得到體現,所以依據這些邏輯,在HotSpot VM中堆中的內容被分爲三個部分:

      新生代:新創建的對象會被分配到這裏,對於整個堆而言一般比較小,垃圾收集平凡,並且其中的對象大部分被收集得很快(這裏指存活時間比較短)。

  老年代:在新生代中如果一個對象存活度過一定的週期之後,那麼HotSpot VM認爲這個對象將會比較長久的存在,那麼就會把對象提升到老年代中。老年代的佔比比較大,並且其中的垃圾收集次數較少,並且每次收集能夠銷燬的對象比較少(相對比例而言)

  永久代:這個雖然稱之爲代,但是事實上這個部分不與新老兩個代進行代際的轉移,HotSpot VM只是拿來存放元數據(類的數據結構、保留字符串等)

 

  收集方法,垃圾收集器通過卡表來進行查找老——新之間的引用,將老年代的數據以每512個字節劃分爲若干個快。每次老年代對象中引用新生代的字段發生變化的時候卡表的狀態便被設置爲髒,垃圾收集器只需要在髒卡中查找老——新引用的變化。

      HotSpot VM字節碼解釋器和JIT編譯器使用些屏障維護卡表,就是一段維護卡表狀態的代碼,每次在有引用的更新的時候就會執行這段代碼,這雖然添加了部分操作量但是極大的減少了垃圾收集器的工作,所以整體上這個還是對於性能有極大的提升的。

分代垃圾收集的辦法使得JVM可以通過不同的垃圾收集的策略和算法對於不同特徵的堆內容進行特異性的收集,這個對於性能的優化就給我們提供了更多的空間。

新生代

     新生代的區域一般來說被分爲三塊,其中一塊爲Eden還有兩塊爲Survivor,在這裏我們爲了方便解說就將Survivor分成S1S2分別對應兩塊Survivor

    在這裏我們提供一個簡單的垃圾收集的例子,假設一個垃圾收集就要開始,這個時候依據內存分配的情況EdenS1中存在對象,S2中空閒。

一般來說新分配的對象都會被生成在Eden中,當某次垃圾收集完成之後,在Eden依然存活的以及在S1中存活的對象會被轉移到之前空置的S2中去,將之前有對象的S1Eden清空。然後直到進行下一次收集,Eden中剩下以及S2中剩下的對象被轉移到空閒的S1中去......直到如果在轉移過程中發現某些對象存在的時間過程,譬如說已經倖存超過10次垃圾收集,那麼這些對象將會被提升到老年代中去。

詳細的情況我們可以參見下面的圖示:


----------------------------------------------------------------------------------------------------------------------------------------------


----------------------------------------------------------------------------------------------------------------------------------------------



----------------------------------------------------------------------------------------------------------------------------------------------


快速內存分配

  有一點可以清楚的看到,就是在之前的垃圾收集(新生代)之後,我們處於Eden中的空間總是空的,所以爲了提升內存分配的效率可以直接使用指針碰撞的方法進行內存的快速分配。

  在分配開始時只需要檢查topEden末端之間的空間是否符合需要,如果能夠容納,那麼就直接分配空間出去,並把top指針往後移動至分配出去的空間之末。

  注:在這裏由於java是一個允許多線程的語言,所以實際操作的時候是給予每個線程一個Eden中的緩衝區,並在這個給定的區域內進行指針碰撞,一般單個線程不會填滿整個給定的緩衝區

垃圾收集器

  Serial收集器:這款收集器在新生代中使用之前描述的方法進行處理,而在老年代的收集中使用壓縮標記清除的方式對於內存進行收集。這裏這兩種的收集方式都是會STOP-THE-WORLD的方式進行處理,這種方式顧名思義是停止了所有JVM內的線程進展,這使得垃圾收集器能夠高效以及完全的模式進行垃圾收集,但是隨之而來的問題是垃圾收集的過程中其他的服務進程全部會被停滯,在這個時間段之中會極大的影響響應的速度。(特別是部分響應時間有比較高要求的應用)

    Serial收集器對於大多數停頓時間要求不高和在客戶端運行的應用中使用比較廣泛,它只需幾百兆java堆就能有效管理許多應用,並在最差的情況下也能保持比較短暫的停頓。

注:標記壓縮方法,在回收老年代的對象的過程中,將存活的對象移到堆的頭部,最終收集完成之後所有空餘的位置留在尾部,這種方法就可以在之後的內存分配中使用前文所提到過的‘指針碰撞’進行快速的內存分配,在垃圾收集的時候雖然會產生額外的性能消耗,但是顯然設計者認爲這個設計是值得的。

  Parallel收集器:這款收集器是以吞吐量爲追求的,一般也被稱爲Throughput收集器,它的操作模式可以說和Serial基本一致,新生代使用Stop-the-world,老年代使用標記壓縮。但是它的特色就是使得Minor GC以及Full GC可以在同時進行,也就是將老年代和新生代的垃圾收集進行併發處理,這個提升了垃圾收集的整體效率,所以它能夠提供更好的吞吐量的體現。

注:吞吐量值得是在一定時間內對於任務的處理數量,所以我個人將這個視作性能在時間上的積分,而通過提高垃圾收集的時間效率能夠降低垃圾收集的時間,減少了應用不能提供服務的整體時間,在系統和硬件條件不變的情況下就能提供更多的吞吐量。

 

  Mostly-Concurrent收集器:這種類型的收集器就和之前的非常大的不同了,爲了使得系統不至於由於垃圾收集的原因造成較大的延遲波動,所以在耗時較高的老年代垃圾收集中使用了非Stop-the-world的模式,併發標記收集,在正式收集之前就在併發線程中標記好需要收集的內容,只在開始和最後收集執行的時候產生兩次簡短的停頓。由於在這裏收集的時候只需要依照之前的標記進行執行,不需要再進行判斷,所以收集的過程停頓時間被降到很低的程度。

  但是依然有問題會出現,這種方式的垃圾收集由於是併發進行的,同時代碼也在對於內存中的對象進行操作和執行,所以在併發標記階段將會有不少對象在之後又被改動過,這個時候收集器引入了重新標記的方法,在第一次標記完成之後再對於期間有過個改動的對象進行遍歷。爲了進一步提高效率重新標記的之前還引入了預清除階段,預清除階段會重新標記一些在第一次標記階段被改掉的對象,這將在重新標記階段提示效率,減少重新標記產生的停頓。在標記完成之後採用的內存收集的方式並不使用標記-壓縮的模式,這個特性使得內存的收集過程也可以和應用的線程是並行的,這進一步減少了停頓的時間。

總體來說CMS收集器的優勢在於停頓時間短,但是這個收集器對於性能會有更高的要求,同時由於併發的問題其實並不能被完全避免,垃圾收集的結果難免會漏掉部分需要收集的垃圾,而且由於不使用標記-壓縮的模式,導致在之後的內存分配中會有相比其他收集器的額外消耗。

  G1收集器:Garbage-First收集器是最新的收集器,在概念上顛覆了之前JVM中新生代和老年代的概念。該款收集器將java堆分成相同尺寸的區域,將一組區域標記分爲一個帶,當然在這裏的一組區域可能不連續。

  G1收集器的操作方式是將區域中存活的對象轉移到另一個區域中去,然後收集前者的區域,G1定期併發標記那些幾乎是空的區域,這就是G1的核心思想,使用最小的消耗優先解放區域。

應用對於垃圾收集器的影響

  內存分配:由於垃圾收集這個任務的性質,可以預見的是當我們的內存中東西過多或者說是溢出的時候我們不得不啓動垃圾收集這個過程,內存的大小以及應用中對於內存的使用方式將通過內存使用率極大的影響垃圾收集的頻率。

  存活數據多少:顯然垃圾收集器的使用時間是和存活對象的數量有關,越多的對象存活將會使得垃圾收集器需要遍歷更多的對象。

  老年代的引用更新:老年代中引用更新也會需要垃圾收集器額外的注意,所以這也會影響垃圾收集的任務量。

 

  注意,對象池的設計能夠在編碼的時候提供更多的便利以及種種的好處,但是對於垃圾收集而言,池化的對象顯然是Old-to-young引用更新的大戶,並且由於池化的原因,這些對象會增加老年代的壓力。綜上我們在設計應用的時候要考慮到池化對象的優缺點並儘量平衡的使用。



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章