深刻理解Java虛擬機閱讀心得(三)

Java中提倡的自動內存管理最終能夠歸結爲自動化的解決兩個問題:java

  1. 給對象分配內存
  2. 回收分配給對象的內存

 

先說說回收這一方面的兩個主要知識點算法

一。垃圾收集算法數組

1.標記-清理算法安全

  首先標記出全部須要回收的對象,而後在標記完成後統一回收全部被標記的對象(適用老年代)網絡

  兩個缺陷:(1)效率問題,標記和清除兩個過程效率都不高多線程

       (2)空間問題,標記清除算法會產生大量不連續的內存空間碎片,致使沒法分配較大對象併發

2.複製算法函數

  將可用內存按容量劃分爲等大小的兩塊,每次只使用其中的一塊。清理時將還存活着的對象複製到另外一塊中,而後把已使用過的內存空間一次清理掉。這樣每次都是對整個半區進行回收。商業虛擬機中經常使用複製算收集新生代對象,但這種算法的代價內存使用率太低,爲此對算法進行改進。spa

  將內存空間劃分爲8:1:1的三個內存區間(Hotspot虛擬機中默認比例),其中佔8比率的是Eden區,另外兩個是Survior區,每次使用Eden區和其中一個Survior區;清理時,將Eden區和Survior1區中還存活着的對象複製到Survior2區中,而後清理掉Eden區和Survior1區。此外,這裏在複製轉移對象時,當Survior2區的空間不夠用時,會須要依賴其餘內存(老年代)進行分配擔保。線程

3.標記-整理算法

  首先標記處全部須要回收的對象,而後讓全部存活的對象向一端移動,最後直接清理掉端邊界之外的內存。(針對老年代特色)

4.分代收集算法

     根據對象存活週期的不一樣將內存劃分爲幾塊。通常分爲新生代(年輕代)和老年代(年老代),而後根據各個年代特色採用適當的收集算法。

 

二。垃圾收集器

  書中提到了總共7種不一樣的垃圾收集器,其中3種適用於年輕代,3種適用於年老代。此外,G1收集器爲二者通用

  (一)年輕代中的3種垃圾收集器(都是使用複製算法?):

  1.Serial收集器

    單線程的收集器(單線程,且在垃圾收集時必須暫停其餘全部的工做線程,即回收停頓)。Client 場景下的默認新生代收集器。

     這種收集器簡單高效,無線程交互開銷,所以擁有最高的單線程收集效率;能夠配合CMS或Serial Old使用

  2.ParNew收集器

     Serial收集器的多線程版本。Server模式下的虛擬機中首選的新生代收集器(由於只有Serial和ParNew能與CMS收集器配合)

     默認開啓的收集線程數與CPU的數量相同;能夠配合CMS或Serial Old使用

  3.Parallel Scavenge收集器

     多線程收集器。與其餘收集器目的不一樣,該收集器的目標是達到一個可控制的吞吐量(Throughput).

     吞吐量即CPU運行用戶代碼的時間和CPU總消耗時間的比值。能夠配合Parallel Old或Serial Old使用

 

  (二)年老代中的三種垃圾收集器:

  1.Serial Old收集器

     Serial收集器的老年代版本,單線程收集器,使用標記-整理算法

  2.Parallel Old收集器

     Parallel Scavenge收集器的老年代版本,使用多線程和標記-整理算法

  3.CMS(Concurrent Markup Sweap)收集器

    是一種以獲取最短回收停頓時間爲目標的收集器。是基於標記-清理算法的。

    運行分爲四個步驟:

      1.初始標記:標記 GC Roots 能直接關聯到的對象,須要回收停頓(即此時爲單線程模式)

      2.併發標記:進行 GC Roots Tracing 的過程,它在整個回收過程當中耗時最長,不須要停頓

      3.從新標記:須要回收停頓。此階段是爲了修正併發標記期間因用戶程序繼續運做而致使的標記變化部分

      4.併發清除

    CMS有三個明顯的缺點:

      1.對CPU資源很是敏感。CMS默認啓動的回收線程數量數爲 (CPU數量+3)/4;即當CPU數量較少時,會分出較多比率的運算能力去執行多線程,即會致使吞吐量太低

      2.沒法處理浮動垃圾。即併發清理過程當中用戶線程運做產生的垃圾。當超過啓動閾值時(JDK1.6,閾值爲92%),會出現"Concurrent Model Failure"而致使另外一次Full GC產生。

      3.收集結束時會產生大量不連續的內存空間碎片。由於CMS是基於 標記-清除 算法的垃圾收集器。

      

          G1(Garbage-First)收集器

      G1 能夠直接對新生代和老年代一塊兒回收。G1收集器中,它將整個Java堆劃分紅多個大小相等的獨立區域(Region),此時新生代和老年代不在是物理隔離的了,都是一部分Region的集合。

       經過引入 Region 的概念,從而將原來的一整塊內存空間劃分紅多個的小空間,使得每一個小空間能夠單獨進行垃圾回收。從而能夠有計劃的在Java堆中進行全區域的垃圾收集,進而可以創建可預測的停頓時間模型。G1經過跟蹤各個Region裏垃圾堆的價值大小,在後臺維護一個優先列表,每次優先回收價值最大的Region。經過使用Region劃份內存空間和優先級的區域回收方式,保證G1收集器在有效時間內得到儘量高的收集效率。此外,G1中的每一個Region都有一個與之對應的Remembered Set用於記錄其餘Region以及其餘收集器中對該Region中對象的引用以免虛擬機在作可達性分析的時避免全堆掃描

      不計算維護Remembered Set的操做,大體步驟以下:

      1.初始標記:須要停頓

      2.併發標記

      3.最終標記:爲了修正在併發標記期間因用戶程序繼續運做而致使標記產生變更的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程的 Remembered Set Logs 裏面,最終標記階段須要把 Remembered Set Logs 的數據合併到 Remembered Set 中。須要停頓,但能夠(多個最終標記線程)併發執行

      4.篩選標記:首先對各個 Region 中的回收價值和成本進行排序,根據用戶所指望的 GC 停頓時間來制定回收計劃。可與用戶程序一塊兒併發執行,但停頓用戶線程將大幅度提升收集效率。

      G1收集器的四個特色

      1.併發與並行:經過利用多個CPU來縮短回收停頓時間,須要停頓的GC動做,也可經過併發的方式讓Java程序繼續執行

      2.分代收集:分代概念依然保留,無需其餘收集器配合便可採用不一樣方式處理不一樣的生存週期對象

      3.空間整合總體上看是基於 標記-整理 算法,局部上(兩個Region)看是基於 複製 算法實現的。不會產生不連續的內存空間碎片

      4.可預測的停頓:除追求低停頓外,能創建可預測的停頓時間模型,並能讓使用者明確指定一個長度爲M毫秒的時間片斷。

 

再說說Java中如何自動化解決這兩個問題

三。內存分配和回收策略

   1.內存分配

    (1)對象優先在Eden區分配

      大多數狀況下,對象在新生代Eden區分配,當Eden區內存空間不夠時,將觸發Minor GC

    (2)大對象直接進入老年代

      大對象指須要連續內存空間的對象,如很長的字符串和數組;常常出現大對象會提早觸發垃圾收集以獲取足夠長的連續內存空間放置大對象,所以能夠經過設置參數來讓超過必定長度的大對象直接進入老年代。

    (3)長期存活的對象進入老年代

      爲每一個對象定義一個單獨的年齡計數器,每當該對象通過一次Minor GC以後存活,從Eden區(或survivor1區)複製到Survivor2區,則年齡+1;當對象的年齡增加到必定程度則進入老年代(默認年齡大於15的進入老年代)。

    (4)動態判斷對象年齡,知足必定條件進入老年代

      JVM並非必定要年齡到達必定程度才能進入老年代。當Survivor區中,某一年齡段的全部對象的大小和超過Survivor區的一半時,該年齡的全部對象都直接進入老年代。

 

  2.回收策略

    Minor GC:回收新生代。因爲新生代對象存活時間短,所以Minor GC會頻繁執行,執行效率通常較快。  

    Full GC:回收老年代和新生代。因爲老年代存活時間較長,所以Full GC不多執行,執行效率也比較慢。

    空間分配擔保:

      進行Minor GC前須要先檢查老年代的最大可用空間是否大於新生代全部對象的總空間,若是知足,則認爲Minor GC是安全的。

      若是不知足,則虛擬機會查看是否容許擔保;若是容許,就會查看老年代最大可用連續內存空間是否大於之前每次晉升到老年代的對象的平均大小,大於,則開始Minor GC;若是不容許或小於,則觸發一次Full GC

      以HotSpot虛擬機爲例,在Minor GC進行時,會將Eden區和Survivor1區的對象複製到Survivor2區中,若此時Survivor2區中的內存空間不足以放下全部的對象,此時會將多餘的對象暫時存放在老年代區域。若是此時老年代區域不足以放置剩餘對象,則會發生錯誤,並提早觸發Full GC。

 

四。類加載

  1.類加載的流程

    類的生命週期爲如下7個階段

      加載--->驗證--->準備--->解析--->初始化--->使用--->卸載

    其中類加載過程爲如下5個階段

      加載--->驗證--->準備--->解析--->初始化

    加載:加載主要分爲三個小步驟:

      (1)根據類的全限定路徑獲取定義該類的二進制字節流

      (2)將字節流的靜態存儲結構轉換爲方法區的運行時存儲結構

      (3)生成類的class對象,看成方法區中各類數據操做的入口

      此外,獲取二進制字節流的方式共有四種。

      (1)Zip包中讀取,如Jar包等

      (2)網絡中獲取,如Applet

      (3)運行時生成,如動態代理技術,在 java.lang.reflect.Proxy中

      (4)由其餘文件生成,如Jsp文件生成對應的Class類

      驗證:檢驗字節流是否符合虛擬機規範,且是否會危及虛擬機的安全問題

    準備:對類變量(static修飾的)進行初始賦值,使用的是方法區內存。但這裏的初始賦值並非根據代碼進行初始化(除非變量使用final修飾).如 static int i = 23中的i會被初始化爲0,但 static final int i = 23 會初始化爲23

    解析:將常量池中的符號引用替換爲直接引用。這一步也能夠發生在初始化階段以後

    初始化:執行Java代碼,根據Java代碼對類進行初始化。

        父類靜態變量->父類靜態代碼塊->子類靜態變量->子類靜態代碼塊->父類成員變量->父類成員代碼塊->父類構造函數->子類成員變量->子類成員代碼塊->子類構造函數

 

  2.類加載器

    啓動類加載器,對應加載JRE_HOME目錄中的文件

    擴展類加載器,對應於系統變量路徑

    應用程序類加載器,對應於用戶類路徑

 

    雙親委派模型:要求除了啓動類加載器以外,其餘的加載器都要有本身的父類加載器。這裏的父子關係經過組合方式得到,而不是繼承方式。

           啓動類加載器 --- 擴展類加載器 --- 應用程序類加載器 --- 用戶自定義類記載器

    工做流程:雙親委派模型中,子類加載器得到請求後,會將請求轉發給其父類加載器(一層一層往上傳,直到啓動類加載器),只有當父類加載器沒法加載完成時,子類加載器纔會嘗試加載

    優勢:使得Java類隨着它的類加載器一塊兒具備一種帶有優先級的關係,從而使得基礎類獲得統一。

相關文章
相關標籤/搜索