1. 程式人生 > 其它 >Java JVM學習筆記

Java JVM學習筆記

JVM

  • 基本概念:JVM是可執行Java程式碼的假象計算機,包括一套位元組碼指令集,一組暫存器,一個棧,一個垃圾回收,堆和一個儲存方法域。JVM是執行在作業系統之上的它與硬體沒有直接的互動。

  • Java程式碼的執行:

    • java程式碼編譯為class-javac:Java原始檔通過編譯期產生相應的.Class檔案(Java到JVM的中間位元組碼)
    • 裝載class-ClassLoader:將class檔案裝載到虛擬機器(JVM)中
    • 執行class:虛擬機器(JVM)將位元組碼檔案通過直譯器編譯成機器上的機器碼。(解釋執行的方式)
      • 解釋執行
      • 編譯執行(Java引入了JIT,當JIT第一次編譯完成後就會將機器碼儲存下來,下次可以直接使用。這種方式屬於編譯執行
        • client compiler
        • server compiler
    • 每一種平臺的直譯器是不同的但是實現的虛擬機器是相同的。
  • 當一個程式開始執行,虛擬機器就例項化了,多個程式啟動就會存在多個虛擬機器例項。程式退出或者關閉虛擬機器就會消亡,多個虛擬機器之間資料不能共享。

JVM執行緒

  • JVM允許一個應用併發執行多個執行緒,每個執行緒都是虛擬機器執行過程中的一個執行緒實體。Hotspot JVM中的Java執行緒與原生作業系統有直接的對映關係。

  • 作業系統負責排程所有執行緒,並把它們分配到任何可以使用的cpu上。當原生執行緒初始化完畢,就會呼叫Java run()方法。當執行緒結束時,就會釋放原生執行緒和Java執行緒的所有資源。

    • 當執行緒本地儲存,緩衝區分配,同步物件,棧,程式計數器等準備好以後,就會建立一個作業系統原生執行緒。
    • Java執行緒結束,原生執行緒隨之被回收。

Hotspot後臺執行執行緒

執行緒 說明
虛擬機器執行緒(VM thread) 這個執行緒等待JVM到達安全點操作出現。這些操作必須要在獨立的執行緒中執行,因為當堆修改無法進行時,執行緒都需要JVM位於安全點。這些操作有:stop-the-world垃圾回收,執行緒棧dump,執行緒暫停,執行緒偏向鎖(biased blocking)解除。
週期性任務執行緒 這執行緒負責定時器事件(也就是中斷),用來排程週期性操作的進行。
GC執行緒 這些執行緒支援JVM中不同的垃圾回收活動。
編譯期執行緒 這些執行緒在JVM執行期間,將位元組碼動態編譯為平臺相關的機器碼。
訊號分發執行緒 這個執行緒在接收發送到JVM的訊號並呼叫適當的JVM方法處理。

JVM記憶體區域

JVM中執行緒主要分為執行緒私有區域【程式計數器,虛擬機器棧,本地方法區】,執行緒共享區【JAVA堆,方法區】,直接記憶體。

直接記憶體不是Java執行時資料區的一部分不歸JVM管理,在NIO中存在DirectByteBuffer可以使用堆外記憶體,避免了從核心空間(Native堆)到使用者空間(Java堆)的頻繁複制

  • 執行緒私有資料區域宣告週期與執行緒相同,依賴使用者執行緒的啟動/結束而建立/銷燬(Hotspot中這部分記憶體區域和本地執行緒生死對應)
  • 執行緒共享區域隨著虛擬機器的啟動和關閉而建立和銷燬

程式計數器(執行緒私有)

是一塊較小的記憶體區域,是當前執行緒所執行的位元組碼的行號指示器。在Java虛擬機器的概念模型中,位元組碼直譯器的工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,它是程式控制流的指示器,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都要依賴這個計數器完成。(參考計算機中的pc指標)

由於Jaava虛擬機器的多執行緒是通過多執行緒輪流切換,分配處理器執行時間的方式來實現,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)都只會執行一條執行緒中的指令。因此,為了執行緒切換的正確進行,每條執行緒都需要一個獨立的程式計數器,每個計數器之間互不影響,獨立儲存(這類記憶體區域被稱為執行緒私有的區域)

如果正在執行Java方法,計數器記錄的是當前的指令地址(虛擬機器位元組碼指令地址),如果是Native方法則為空。

程式計數器是唯一一個沒有記憶體溢位錯誤(OutOfMemoryError)的區域

虛擬機器棧(執行緒私有)

虛擬機器棧也是執行緒私有的,她的生命週期與執行緒相同。

虛擬機器棧描述的是java方法執行的記憶體模型:每個方法在執行的時候,Java虛擬機器都會同步建立一個棧幀(Stack Frame),用於儲存區域性變量表,運算元棧,動態連線,方法出口等資訊。每一個方法被呼叫直至執行完畢的過程,就對應著一個棧幀在虛擬機器中入棧到出棧的過程。

  • 區域性變量表:存放了編譯期可知的各種Java虛擬機器基本資料型別(boolean,byte,...)、物件引用(reference型別,物件起始地址的引用指標)和returnAddress型別(指向了一條位元組碼指令的地址)。這些資料型別在區域性變量表中的儲存空間以區域性變數槽(Slot)來表示,其中 64 位長度的 long 和 double 型別的資料會佔用兩個變數槽,其餘的資料型別只佔用一個。區域性變量表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在棧幀中分配多大的區域性變數空間是完全確定的,在方法執行期間不會改變區域性變量表的大小。

棧幀(Stack Frame):用來儲存資料和部分過程結果的資料結構,同時也被用來處理動態連結(Dynamic Linking),方法返回值和異常分派(Dispatch Exception)。棧幀隨著方法呼叫而建立,隨著方法結束而銷燬(丟擲異常也算結束)

虛擬機器棧有兩種異常情況:

  1. 當執行緒請求深度大於虛擬機器所允許深度,丟擲:StackOverflowError異常

  2. 如果Java虛擬機器棧容量可以動態擴充套件,當棧無法申請到足夠的記憶體會丟擲OutofMemoryError異常

    HotSpot 虛擬機器的棧容量是不可以動態擴充套件的,以前的 Classic 虛擬機器倒是可以。所以在 HotSpot 虛擬機器上是不會由於虛擬機器棧無法擴充套件而導致 OutOfMemoryError 異常——只要執行緒申請棧空間成功了就不會有 OOM,但是如果申請時就失敗,仍然會出現OOM異常)

本地方法區(執行緒私有)

本地方法棧(Native Method Stack)與虛擬機器棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機器棧為虛擬機器執行 Java 方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的本地(Native)方法服務。在虛擬機器規範中對本地方法棧中方法使用的語言、使用方式與資料結構並沒有強制規定,因此具體的虛擬機器可以自由實現它。甚至有的虛擬機器(譬如Sun HotSpot虛擬機器)直接就把本地方法棧和 Java 虛擬機器棧合二為一。與虛擬機器棧一樣,本地方法棧區域也會丟擲 StackOverflowEror(棧溢位) 和 OutOfMemoryEror(堆溢位) 異常。

Hotspot虛擬機器的棧容量是不允許動態擴充套件的(一個執行緒的虛擬機器棧申請到的空間是多少就是固定的),以前的 Classic 虛擬機器倒是可以。所以在 HotSpot 虛擬機器上是不會由於虛擬機器棧無法擴充套件而導致 OutOfMemoryError 異常——只要執行緒申請棧空間成功了就不會有 OOM,但是如果申請時就失敗,仍然會出現OOM異常)

堆(Heap-執行緒共享)-執行時資料區

  • 對於Java應用程式來說,Java堆是虛擬機器管理的記憶體中最大的一塊。

  • Java堆(Java Heap)是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。

    Java堆(Java Heap)記憶體的唯一目的就是存放物件例項,Java中幾乎所有物件例項都在這裡分配記憶體。(建立的物件和陣列都儲存在Java堆記憶體中)

    ("所有物件例項以及陣列都應當在堆上分配")但隨著即使編譯技術和逃逸技術分析的日漸強大,棧上分配,標量替換優化手段已經導致一些微妙的變化悄然發生,現在Java物件都在棧上分配已經不那麼絕對了。

  • Java堆是垃圾收集器管理的記憶體區域(是垃圾收集器進行垃圾收集的最重要的記憶體區域),因此一些資料中稱Java堆(Java Heap)為GC堆(Garbage Collected Heap)

    現代VM採用分代收集演算法,因此Java堆從GC的角度還可以細分為新生代(Eden區,From Survivor區和To Survivor區)和老年代。

  • (Thread Local Allocation Buffer,TLAB)快取區:從記憶體分配的角度看,所有的執行緒共享的Java堆中可以劃分出多個執行緒私有的分配緩衝區,以提升物件分配時的效率。不過不管如何劃分Java堆中儲存的都只能是物件的例項,將Java Heap細分的目的只是為了更好的回收記憶體或者分配記憶體。

    Java堆可以處於物理上的不連續記憶體空間中,但在邏輯上應該被視為連續的(抽象出來就是倆資料結構)。

  • Java堆可以被實現成固定大小的,也可以是可擴充套件的(引數-Xmx (memory max最大可分配),-Xms (memory start啟動時記憶體)設定記憶體大小)。當記憶體不夠也無法擴充套件時會丟擲OutOfMemoryError異常。

  • 執行時資料區的GC

作為垃圾回收器管理的區域,Java堆從GC的角度可以分為新生代(Eden區,From Survivor區和To Survivor區)和老年代。

  • 新生代:是用來存放新生的物件,一般佔據堆的1/3空間。用於頻繁建立物件,所以新生代會頻繁觸發MinorGC進行回收。新生代又分為Eden區,ServivorFrom、ServivorTo三個區

    • Eden區:Java新物件的出身地若(若新建立的物件佔用記憶體很大,直接分配到老年代)。當Eden區記憶體不夠的時候就會觸發MinorGC,對新生代進行一次垃圾回收。

    • SurvivorFrom:上一次GC的倖存者,作為這一次GC的被掃描者。

    • SurvivorTo:保留了一次MirrorGC過程中的倖存者。

    • MirrorGC:採用複製演算法(複製->清空->互換)

      1. Eden,SurvivroFrom複製到SurvivorTo,年齡+1:首先吧Eden和Survivor中的存活的物件複製到SurvivorTo區域(如果有物件的年齡達到了老年代的標準則複製到老年代),同時吧這些物件年齡+1(若Survivor不夠位置就放到老年代區)

      2. 清空Eden,SurvivorFrom中的物件。

      3. 最後將SurvivorTo和SurvivorFrom互換,源ServivorTo中的物件稱為下一次GC時的ServivorFrom區

  • 老年代:主要存放應用程式中生命週期長的記憶體物件。

    • 老年代的物件比較穩定,所以MajorGC不會頻繁執行。
      • 在進行MajorGC之前一般都先進行了一次MinorGC,使得有新生代的物件晉升老年代,導致使用者空間不足時才會觸發(MajorGC的回收耗費事件所以不能頻繁進行)。
      • 當無法找到足夠大的連續空間分配給新建立的較大的物件時也會提前觸發一次MajorGC進行垃圾回收騰出空間。
  • 永久代(一種方法區的實現方式):主要存放Class和Meta(元資料)的資訊

    • Class在被載入的時候放入永久區域,和存放例項的區域不同,GC不會在主程式執行期間對永久區域進行清理。所以這也導致了永久代的區域會隨著載入的Class的增多而脹滿,最終導致OOM。

方法區/永久代(執行緒共享)

基本資訊:

  • 方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,他用於儲存已經被虛擬機器載入的型別資訊,常量,靜態變數,即時編譯器(JIT)程式碼快取等資料(儲存著被載入過的每一個類的資訊,這些資訊由類載入器在載入類的時候,從類的原始檔中加載出來)。
    • 常常將方法區稱為永久代因為Java8之前Hotspot VM使用Java堆的永久代方法來實現方法區,將GC擴充套件至方法區使得Hotspot的垃圾收集器能夠像管理Java堆一樣管理這部分記憶體,而不必為方法區來開發一個專門的記憶體管理器(永久代的記憶體回收的主要目標是針對常量池的回收和型別的解除安裝,收益很小)。
    • 方法區是執行緒共享的,當有多個執行緒都使用到了一個類,這個類還未被載入,這時進行類初始化的話可能會引起多個執行緒阻塞。
    • 方法區的大小不必是固定的,jvm可以根據應用的需要動態調整。jvm也可以允許使用者和程式指定方法區的初始大小,最小和最大限制;
    • 垃圾收集器在這個區域較少出現,但永久代不代表永久存在。
      • 通過使用者自定義的類載入器可以動態的擴充套件Java程式,這樣可能會導致一些類不再被使用,變為fw。這時需要垃圾清理對類進行回收
        • 該類所有例項都已經被回收,也就是Java堆中不存在該類的任何例項
        • 載入該類的ClassLoader已經被回收
        • 該類的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

方法區/元空間(執行緒共享)

  • 永久代在Java8中被移除,取而代之的是元空間(元資料區)的實現方式。
    • 永久代中存放元資料的大小由虛擬機器決定,存在記憶體溢位的情況。而元空間實現的方法區預設情況下僅受本地記憶體限制。
    • 永久代的記憶體回收(Full GC)僅針對常量池的回收和型別的解除安裝,收益低。
  • 類的元資料放入本地記憶體,字串池和靜態常量放入Java堆中,這樣可以載入多少類的元資料由系統的實際可用空間來決定。

執行時常量池(Runtime Constant Pool)

  • 執行時常量池是方法區中的一部分(jdk1.8之後方法區實現由永久代變為元空間 Metaspace),在載入類和介面到虛擬機器後就會建立執行時常量池。

  • 執行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量(static final),也包括到執行期解析後才能夠獲得的方法或者欄位引用。此時不再是常量池中的符號地址了,這裡換為真實地址。

    • 每一個Class檔案中都維護著一個常量池(非執行時常量池)存放著字面量和符號引用。這個常量池的內容在類載入的時候,被複制到方法區的執行時常量池。
      • 字面量:string字串和基本資料型別以及他們的包裝類的值,以及final修飾的變數
      • 符號引用:一組符合JVM規範定義的符號,用於描述所引用的目標,在編譯的時候由虛擬機器翻譯成為真正的地址。

    Class的常量池將在類載入存放到方法區的執行時常量池中。

  • 方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的常量和不再使用的型別。

方法區結構

  • 虛擬機器載入類會在方法區儲存已載入的類資訊(不是Class物件,Class物件儲存在堆中是載入的最終產品,方法區的類資訊中儲存一個Class物件和ClassLoader的引用),類資訊存放形式根據不同虛擬機器不同實現。

  • 類資訊:

    • 型別資訊:

      • 類的完整名稱(許可權定名稱)
      • 類的直接父類的完整名稱(父類索引)
      • 類的實現介面的有序列表
      • 類的修飾符,訪問標誌
    • 型別的常量池:

      • JVM為每個已載入的型別(類或介面)都維護一個常量池。池中的資料項像陣列項一樣,是通過索引訪問的。(這個類用到的常量的有序集合)
      • 為什麼需要常量池?
        • 一個java原始檔中的類、介面,編譯後產生一個位元組碼檔案。而Java中的位元組碼需要資料支援,通常這種資料會很大以至於不能直接存到位元組碼裡,換另一種方式,可以存到常量池。
      • 執行時常量池(Runtime Constant Pool)是方法區的一部分。
      • 常量池表(Constant Pool Table)是Class位元組碼檔案的一部分,
    • 欄位資訊(例項域中的欄位資訊):

      • 宣告的順序
      • 修飾符
      • 資料型別
      • 欄位名稱
    • 方法資訊:

      • 宣告的順序
      • 修飾符
      • 返回值名稱
      • 方法名稱
      • 引數列表(有序儲存)
      • 異常表(丟擲的異常)
      • 方法位元組碼(Native、abstract方法除外)
      • 運算元棧和區域性變量表大小
    • 類變數(static靜態變數,靜態域中的欄位資訊)

      • 靜態域非final變數隨著類的載入而載入,他們成為類資料在邏輯上的一部分。

        JDK1.7 字串常量池在堆,執行時常量池在方法區(永久代) 。

        JDK1.8 字串常量池在堆,執行時常量池在方法區(元空間)。

      • 被宣告為final的類變數在編譯的時候就會被分配了,被儲存在類的常量池中,在載入類的時候複製進執行時常量池中,每一個使用它的類儲存著一個對其的引用(複用)。

    • 對類載入器的引用

      • JVM必須知道一個型別是由啟動載入器還是使用者類載入器載入的。如果一個型別是由使用者類載入器載入的,那麼jvm會將這個類載入器的一個引用作為型別資訊的一部分儲存在方法區中(方法區存在使用者自定義ClassLoader資訊)。
    • 對Class類的引用

      • JVM為每一個載入的類都為其在堆上建立一個java.lang.Class例項(我們通過三種方式獲得的Class例項是在這個類被載入的時候儲存在堆上的)。
      • JVM必須以某種方式將Class的這個例項和儲存在方法區中的類的元資料聯絡起來。所以方法區的類資訊中存在一個對Class的引用。
        • 元資料:又稱中介資料中繼資料,為描述資料的資料(data about data),主要是描述資料屬性(property)的資訊,用來支援如指示儲存位置、歷史資料、資源查詢、檔案記錄等功能。JAVA中的元資料可以參考部落格
    • 方法表

      • 為了提高訪問效率,必須仔細的設計儲存在方法區中的資料資訊結構。除了以上討論的結構,jvm的實現者還可以新增一些其他的資料結構,如方法表。
      • jvm對每個載入的非虛擬類的型別資訊中都添加了一個方法表,方法表是一組對類例項方法的直接引用(包括從父類繼承的方法)。JVM可以通過方法錶快速啟用例項方法。可以像陣列那樣使用索引快速訪問到方法表上儲存的方法,正像java宣稱沒有 指標了,其實java裡全是指標。更安全只是加了更完備的檢查機制,但這都是以犧牲效率為代價的(個人認為java的設計者 始終是把安全放在效率之上的,所以java才更適合於網路開發)

字串常量池 StringTable 為什麼要調整位置?

  • JDK7中將StringTable放到了堆空間中。因為永久代的回收效率很低,在Full GC的時候才會執行永久代的垃圾回收,而Full GC是老年代的空間不足、永久代不足時才會觸發。(這就導致StringTable回收效率不高,而我們開發中會有大量的字串被建立,回收效率低,導致永久代記憶體不足。放到堆裡,能及時回收記憶體。)

垃圾回收與演算法

垃圾回收之前應該搞清楚三點:

  1. 那些記憶體需要回收(引用計數,可達性分析)
  2. 什麼時候回收(分代)
  3. 如何回收(複製,標記清除)

在Java記憶體模型中,程式計數器、虛擬機器棧、本地房發棧隨執行緒的而生,隨執行緒而亡,棧中的棧幀隨著方法的進入和退出有條不紊的進行著入棧和出棧。因此這幾個執行緒私有的區域的記憶體分配和回收都具備確定性。當方法退出時記憶體就隨著回收了。

而Java堆和方法區這兩個區域具有著很顯著的不確定性:同一個介面的多個實現類可能需要記憶體不同,只有處於執行期間我們才能知道程式究竟會建立哪些物件,建立多少個物件,這部分記憶體回收和管理是動態的。

如何確定垃圾

  • 引用計數法

    • 在Java中,引用和物件是有關聯的。對物件的操作必然是通過引用進行的,這樣就能通過引用計數來判斷一個物件是否可以回收。
    • 在物件之中新增一個引用計數器,每當有一個地方引用她時,她的計數器就加一;當引用失效時計數器值就減一;任何時刻計數器為0的物件就是不在被使用的(就是可以回收的)。
    • 但引用計數需要考慮很多例外情況,必須要配合大量額外處理才能保證正確工作。(例如:單純的引用計數很難解決物件的迴圈引用)
  • 可達性分析

    • 為了解決迴圈引用的問題,Java使用了可達性分析的演算法。通過一系列的“GC roots”物件作為起點搜尋。(如果在“GC roots”和一個物件之間沒有可達路徑(引用鏈),則稱物件是不可達的)。

    • GC roots:

      • 虛擬機器棧中引用的物件
      • 方法區中靜態引用的物件
      • 方法區中常量引用的物件
      • 本地方法引用的物件
      • 虛擬機器內部的引用如Class物件,系統類載入器。
      • 同步鎖持有的物件
      • JMXBean,JVMTI中註冊的回撥,原生代碼快取

標記清除演算法

  • 最基礎的垃圾回收演算法,分為兩個階段:標記和清除(在標記期間標記出需要回收的物件,在清除期間清除需要回收的物件)

  • 存在記憶體碎片化嚴重的問題(標記出的需要回收的記憶體是零散的,可能導致大物件找不到可利用連續空間的情況)

複製演算法

  • 按記憶體容量將記憶體劃分為等大小的兩塊。每次只使用其中一塊,當一塊記憶體滿了(觸發垃圾回收)就將存活的物件複製到另一塊。
  • 存在記憶體利用率低(浪費了50%空間),物件存活數多時Coping演算法效率大大降低。

標記整理演算法

  • 先將記憶體中需要回收的記憶體進行標記,然後將存活的物件移動到記憶體的一端。

  • 移動活動物件,特別是老年代有大量物件存活的區域是一種負擔極大的操作。

分代收集演算法

目前大部分JVM都採用分代收集演算法,根據物件的存活不同生命週期將記憶體劃為不同的域(如:老年代,新生代)。老年代每次回收的物件較少,而新生代回收的物件較多,所以兩個域可以使用不同的垃圾回收演算法。

  • 新生代與複製演算法(Copying)
    • 新生代中每次垃圾回收都要回收大量物件(大對數物件都是“朝生夕滅”的),新生代每次都要回收大部分物件(所以Eden區比較大),即複製操作比較少。
    • 每次使用Eden和一塊Survivor,回收後將存活的物件複製到另外一塊Survivor中。
  • 老年代與標記複製演算法(Mark-Compact)
    • 老年代每次只回收少量物件,所以使用標記複製演算法
    • 永生代(1.8之前)的回收主要包括廢棄常量和無用的類
    • 當新生物件過大,也會直接分配到老年代。
    • 年齡(熬過的GC次數,預設是15)達到條件會從新生代移動到老年代

GC分代收集演算法VS分割槽收集演算法

  • 當前主流都是採用分代收集演算法,這種演算法會根據存活週期將記憶體分塊,然後對不同的塊採用不同的垃圾收集演算法
    • 分割槽收集演算法:將整個堆空間劃分為連續的不同小區間(區域性化),每個小區間單獨使用,獨立回收。這樣做可以控制一次回收多少個區間,根據目標停頓的時間,合理的分配一次回收多少個區間,減少了GC所產生的停頓

JAVA中的四種引用型別

強引用

  • Java中最常見的引用,吧一個物件賦值給一個引用變數,這個引用變數就是一個強引用。當一個物件被強引用時,他處於可達狀態,它是不可能被垃圾回收機制回收的,即該物件以後永遠不會被用到JVM也不會被回收。(造成記憶體洩漏的主要原因之一)

軟引用

  • 軟引用需要使用SoftReference類來實現,對於軟引用物件來說,當系統記憶體足夠時它不會被回收,當系統記憶體不足時才會被回收。

弱引用

  • 弱引用需要使用WeakReference(WeakHashMap),對於只有弱引的物件來說,不管系統記憶體是不是足夠,GC總會將其回收

虛引用

  • 弱引用使用PhantomReference來實現,她不能單獨使用,必須和引用佇列聯合使用,虛引用的主要作用是跟蹤物件被垃圾回收的狀態。

GC垃圾收集器

Java記憶體被劃分為了新生代和老年代兩部分,新生代主要使用複製標記演算法,老年代主要使用複製整理演算法。所以,Java為這兩個塊提供了許多不同的垃圾收集器。

  1. Serial收集器

    Serial是一個單執行緒收集器,他不但只使用一個執行緒去完成垃圾回收工作,而且必須暫停所有其他執行緒直到垃圾回收結束。

    • 採用序列回收
    • 使用複製演算法

    Serial Old新生代使用單執行緒複製演算法,老年代使用單執行緒標記整理演算法。

  2. ParNew收集器

    Serial的多執行緒版本,也是用複製演算法,除了多執行緒以外和之前的Serial收集器完全一樣。

    • 多執行緒
    • 使用複製演算法
  3. Parallel Scavenge收集器

    Parallel Scavenge的關注點是程式達到一個可控制的吞吐量(Thoughput,CPU用於執行使用者程式碼的時間/CPU總消耗時間),同時Parallel Scavenge具有自適應調節策略。

    • 多執行緒回收
    • 使用複製演算法

    Parallel Old的老版本中新生代採用多執行緒複製演算法,老年代採用多執行緒標記整理演算法。

  4. CMS收集器

    • 主要目標是獲取最短垃圾回收停頓時間
    • Concurrent Mark Sweep 併發標記清除(應用程式執行緒和GC執行緒交替執行)
    • 使用標記-清除演算法

    CMS整個過程分為四個階段

    初始標記

    • 僅做標記:標記GC Roots能直接關聯到的物件

    併發標記

    • 遍歷整個物件圖的流程(是和垃圾收集器一起併發執行的,不需要暫停使用者執行緒)

    重新標記

    • 由於併發標記時,使用者執行緒依然執行,因此在正式清理前,再做修正

    併發清除

    • 基於標記結果,直接清理物件

    CMS收集器特點:

    • 儘可能降低停頓

    • 會影響系統整體吞吐量和效能:比如,在使用者執行緒執行過程中,分一半CPU去做GC,系統性能在GC階段,反應速度就下降一半
      清理不徹底

    • 因為在清理階段,使用者執行緒還在執行,會產生新的垃圾,無法清理

    • 因為和使用者執行緒一起執行,不能在空間快滿時再清理(因為也許在併發GC的期間,使用者執行緒又申請了大量記憶體,導致記憶體不夠)

    CMS的提出是想改善GC的停頓時間,在GC過程中的確做到了減少GC時間,但是同樣導致產生大量記憶體碎片,又需要消耗大量時間去整理碎片,從本質上並沒有改善時間。

  5. G1收集器

  • 基於標記整理演算法,不產生記憶體碎片。

  • 可以預測停頓時間,在不犧牲吞吐量的情況下,實現低停頓垃圾回收(區域性回收)。

  • G1收集器開創了面向區域性收集的設計思路和基於Region的記憶體佈局形式。

G1不再堅持固定大小以及固定數量的的分代記憶體區域劃分,而是把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要扮演新生代的Eden,Survivor空間,或者老年代空間。這樣新生代和老生代就成為了一系列的區域的動態集合

Humongous區域:專門用於存放大物件,,G1認為只要大小超過了Region容量的一半大小就會被認為是大物件。

G1的垃圾回收每次回收的都是Region大小的整數倍(這樣就使得停頓時間可以預測),同時會對跟蹤各個Region中垃圾堆的價值大小並維護一個優先順序表。優先回收最有價值的區域(最垃圾的區域)。

G1將完整的堆區域性化為了一個一個的小堆(或許能和緩衝區關聯起來),每個小堆都可以扮演一個域角色(域不再固定,而是動態的區域性化的)

Region 區域劃分和優先順序區域回收策略確保G1收集器可以在有限時間內獲得最高的垃圾回收集效率。

JVM類載入機制

一個型別從被載入到虛擬機器記憶體中開始,到解除安裝為止,她的整個生命週期將會經歷載入,驗證,準備,解析,初始化,使用,解除安裝七個階段。

其中載入,驗證,準備,初始化,解除安裝的順序是確定的。

Java虛擬機器規範中規定了只有6中情況必須立即對類進行初始化(此前已經載入驗證準備過了)。

  • 遇到new,geststatic,pustatic或invokestatic這四條位元組碼時如果型別沒有過初始化,則需要先觸發初始化階段。
    • 使用new關鍵字例項化物件的時候
    • 讀取或設定一個型別的靜態欄位(被final修飾,已經在編譯期吧結果放入常量池的靜態欄位除外)的時候。
    • 呼叫一個型別的靜態方法的時候
  • 使用java.lang.reflect包對類進行反射呼叫的時候
  • 當初始化類的時候,如果發現父類還沒有被初始化,則需要先觸發其父類的初始化。
  • 當虛擬機器啟動的時候,使用者需要指定一個要執行的主類
  • JDK7動態語言支援中java.lang.invoke.MethodHandle例項最後解析結果為REF_getStatic,REF_putStatic,REF_invokeStatic,REF_newInvokeSpecial四種類型的方法控制代碼,並且方法控制代碼對應的類沒有進行過個初始化,那麼就要先對這個類進行初始化。
  • JDK8中,介面加入了預設方法(default),如果有這個介面的實現類發生了初始化,那介面就要在之前被初始化。
flowchart LR A[載入\nLoading];B[驗證\nVerification];C[準備\nPreparation];D[解析\nResolution];E[初始化\nInitialization];F[使用\nUsing];G[解除安裝\nUnloading]; A-->B subgraph 連線 B-->C-->D end D-->E--類初始化結束-->F-->G

載入(Loading)

在類載入過程的載入階段,Java虛擬機器需要完成以下三件事情:

  • 通過一個類的許可權定類名來獲取定義此類的二進位制位元組流。(可以是網路,任何可以讀取的檔案,資料庫,執行時計算生成等等等)
  • 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構
  • 在記憶體(Java Heap)中生成一個java.lang.Class物件,作為方法區這個類的各種資料的訪問入口(型別必須與類加器一起確定唯一性)。

對於陣列型別的載入建立需要遵循以下過程:

  • 如果陣列元件型別是引用型別,就遞迴採用上面的定義去載入這個元件,陣列C將被標識在載入該類元件型別的類的類載入器的類名稱空間上。
  • 如果元件是非引用型別,Java虛擬機器將會把陣列C標記為與引導類載入器相關聯。
  • 陣列類的可訪問性預設與她的元件的可訪問性一致,如果元件型別不是引用型別她的預設可訪問性為public,可被所有類和介面訪問到。

載入結束後,Java虛擬機器外部的二進位制位元組流就按照虛擬機器所設定的格式儲存在方法區之中(方法區資料儲存格式依據JVM不同而不同),型別資料存放在方法區之後,會在Java堆記憶體中例項化一個對應的Class物件,這個物件將作為程式訪問方法區中的型別資料的外部介面。

驗證(Verification)

這一階段的目的是確保Class檔案的位元組流中包含的資訊是否符合當前虛擬機器的要求,並不會危害虛擬機器的自身安全。

  • 檔案格式驗證

  • 元資料驗證

  • 位元組碼驗證

  • 符號引用驗證

  • ...

準備(Preparation)

準備階段是正式為類變數分配記憶體並設定類變數的初始值階段(靜態變數),即在方法區中分配這些變數所使用的記憶體空間。

JDK1.7時將static區移動到了Java Heap中

而靜態常量(final)在編譯階段會生成ConstantValue屬性,在準備階段會根據ConstantValue屬性將v賦值為8080。

解析(Resolution)

解析階段是指虛擬機器將常量池中的符號引用替換為直接引用的過程。

  1. 類或介面的解析
  2. 欄位解析
  3. 方法解析
  • 符號引用:CONSTANT_Class_info,CONSTANT_Filed_info,CONSTANT_Method_info等型別的常量。

    • 序號引用是與虛擬機器實現無關的,明確規定在了Java虛擬機器規範的Class檔案格式中。
  • 直接引用:

    • 直接引用可以是指向目標的指標,相對偏移量或是一個能間接定位到目標的控制代碼。如果有了直接引用,那引用的目標必定是已經存在記憶體中。

除invokeddynamic指令外,虛擬機器可以實現對第一次解析結果進行快取(如執行時直接引用常量池中的記錄),並把常量標識為已解析狀態。

初始化(Initialization)

初始化階段是類載入的最後一個階段,前面的類載入階段之後,除了在載入階段可以自定義類載入器之外,其他操作都由JVM主導,到了初始階段才真正開始執行類中定義的Java程式程式碼。

  • 初始化階段是執行類構造器方法的過程(是由編譯期自動收集類中的所有類變數的賦值動作和靜態語句塊(static{})中的語句產生的),編譯器收集的順序是語句在原始檔中出現的順序決定的,靜態語句塊只能訪問到定義在靜態語句塊之前的變數,定義在他之後的變數,在之前可以賦值但不能訪問。
  • 虛擬機器保證在子類的方法執行之前父方法已經執行完畢。

類載入器

Java虛擬機器將載入動作放到JVM外部實現,讓程式設計師可以自己決定如何獲取所需的類(通過類載入器實現)。

類載入器雖然只用於實現類的載入動作,但它在Java程式中起到的作用卻遠超類載入階段。

對於任意一個類,都必須由載入它的類載入器和這個類本身一起共同確立其在Java虛擬機器中的唯一性。

對於兩個類只要類載入器不同那麼久不相等

  • 啟動類載入器/引導類載入器

    負責載入JAVA_HOME\lib目錄中的,或通過-Xbootclasspath引數指定路徑中的,且被虛擬機器認可的(按檔名識別,如rt.jar)的類。

  • 擴充套件類載入器(JDK9中被平臺類載入器所取代)

    負責載入JAVA_HOME\lib\ext目錄中的,或者通過java.ext.dirs系統變數指定路徑中的類庫。

  • 應用程式類載入器/系統類載入器

    負責載入使用者路徑(classpath)上的類庫。

    JVM通過雙親委派模型進行類的載入,當然可以可以通過java.lang.ClassLoader實現自定義的類載入器。

雙親委派模型

  • 從Java虛擬機器角度來看,只存在兩種不同的類載入器:一種是啟動類載入器(由C++實現,是虛擬機器的一部分),一種是其他載入器(由Java實現,獨立於虛擬機器之外,並全部繼承自java.lang.ClassLoader)。

  • 雙親委派模型的工作過程:如果一個類載入器收到了類載入的請求,他首先不會自己嘗試載入這個類,而是把這個請求委派給父類去完成,每一個層次的類載入器都是如此,因此所有的載入請求都應該傳送到啟動類載入器中。只有當父載入器發現自己無法完成這個請求的時候(在她的載入路徑之下沒有發現要載入的Class檔案),子類才會嘗試去載入。

  • 雙親委派模型的實現:

    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded 
            Class<?> c = findLoadedClass(name); //判斷請求的類是否已經被載入過了
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) { //若父載入器載入路徑下沒有目標類(丟擲ClassNotFoundException)
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                if (c == null) { //若父類無法載入
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name); //子類載入器自身的findClass
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment(); //增加尋找到得類數量
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    

OSGI

OSGi(Open Service Gateway Initiative):動態模型系統(使得Java應用程式能像電腦外設一樣隨意更換,即插即用等),是面向Java的動態模型系統,是Java動態模組化系統的一系列規範。

動態改變構造

  • OSGi服務平臺提供多種網路裝置上無需重啟的動態改變構造的功能。為了最小化耦合度和促使這些耦合度可管理,OSGI技術提供一種面向服務的架構,他是這些元件動態的發現對方。

模組化程式設計與熱插拔

  • OSGi旨在為實現Java程式設計師的模組化程式設計提供基礎條件,基於OSGi的程式很可能可以實現模組級的熱插拔功能,當程式更新時,可以只停用,重新安裝然後啟動程式的一部分,這對企業及程式開發來說是非常具有誘惑力的選擇。
  • OSGi描繪了一個很好的模組化開發目標,而且定義了實現這個目標的所需要服務與架構,同時也有成熟的框架進行實現支援。但並非所有的應用都是和採用OSGi作為基礎架構,它在提供強大功能的同時,也引入了額外的複雜度,因為她破壞了雙親委派模型。