天天看點

JVM-初見

目錄

  • JVM的體系結構
  • 類加載器
  • 雙親委派機制
  • Native
  • PC程式計數器
  • 方法區(Method Area)
  • 調優工具
  • 常見JVM調優參數
  • 常見垃圾回收算法
    • 引用計數算法
    • 複制算法
    • 标記-清除算法
    • 标記-壓縮算法
  • 分代回收政策
  • 垃圾收集器
    • 串行收集器(Serial)
    • 并行收集器(ParNew)
    • Parallel Scavenge 收集器
    • Serial Old 收集器
    • Parallel Old 收集器
    • CMS 收集器(Concurrent Mark Sweep)
    • G1 收集器
    • 為什麼要垃圾回收時要設計STW(stop the world)?

推薦:

JVM-超全-圖解

JVM-思維導圖

簡化圖:

JVM-初見

JVM-初見

類加載器作用:加載.class檔案

類加載流程(三個階段):

1.加載階段

将編譯好的class檔案加載到記憶體中(方法區),然後會生成一個代表這個類的Class對象。

2.連結階段

會為靜态變量配置設定記憶體并設定預設值。

3.初始化階段

執行類構造器()進行初始化指派。

java自帶的類加載器:

  • 啟動類加載器(Bootstrap ClassLoader):又名根類加載器或引導類加載器,負責加載%JAVA_HOME%\bin目錄下的所有jar包,或者是-Xbootclasspath參數指定的路徑,例:rt.jar
  • 拓展類加載器(Extension ClassLoader):負責加載%JAVA_HOME%\bin\ext目錄下的所有jar包,或者是java.ext.dirs參數指定的路徑
  • 系統類加載器(Application ClassLoader):又名應用類加載器,負責加載使用者類路徑上所指定的類庫,如果應用程式中沒有自定義加載器,那麼此加載器就為預設加載器

JVM-初見

類加載器收到加載請求

1.不會自己先去加載,把請求委托給父類加載器,如果父類加載器還存在其父類加載器,則進一步向上委托,最終将到達頂層的啟動類加載器

2.如果父類可以完成加載任務,就成功傳回

3.如果完不成,子加載器才會嘗試自己去加載

優點:避免重複加載 + 避免核心類篡改

程式中使用:private native void start0();

1.凡是帶了native關鍵字的,說明java的作用範圍達不到了,回去調用底層c語言的庫!

2.會進入本地方法棧,然後去調用本地方法接口将native方法引入執行

本地方法棧(Native Method Stack)

記憶體區域中專門開辟了一塊标記區域: Native Method Stack,負責登記native方法,

在執行引擎( Execution Engine )執行的時候通過本地方法接口(JNI)加載本地方法庫中的方法

本地方法接口(JNI)

本地接口的作用是融合不同的程式設計語言為Java所用,它的初衷是融合C/C++程式,

Java在誕生的時候是C/C++橫行的時候,想要立足,必須有調用C、C++的程式,

然後在記憶體區域中專門開辟了一塊标記區域: Native Method Stack,負責登記native方法,

程式計數器: Program Counter Register

每個線程都有一個程式計數器,是線程私有的,就是一個指針,

指向方法區中的方法位元組碼(用來存儲指向像一條指令的位址, 也即将要執行的指令代碼),

在執行引擎讀取下一條指令, 是一個非常小的記憶體空間,幾乎可以忽略不計

為什麼需要程式計數器?記錄要執行的代碼位置,防止線程切換重新執行

位元組碼執行引擎修改程式計數器的值

方法區是被所有線程共享,所有字段和方法位元組碼,以及一些特殊方法,如構造函數,接口代碼也在此定義,

簡單說,所有定義的方法的資訊都儲存在該區域,此區域屬于共享區間。

靜态變量(static)、常量(final)、類資訊(構造方法、接口定義)(Class)、運作時的常量池存在方法區中,但是執行個體變量存在堆記憶體中,和方法區無關

棧:先出後進,每個線程都有自己的棧,棧記憶體主管程式的運作,生命周期和線程同步,線程結束,棧記憶體也就是釋放。

對于棧來說,不存在垃圾回收問題,一旦線程結束,棧就結束。

棧記憶體中運作:8大基本類型 + 對象引用 + 執行個體的方法。

棧運作原理:棧桢

棧滿了:StackOverflowError

隊列:先進先出(FIFO:First Input First Output)

JVM-初見

一個JVM隻有一個堆記憶體,堆記憶體的大小是可以調節的,

類加載器讀取類檔案後,一般會把類,方法,常量,變量,我們所有引用類型的真實對象,放入堆中。

堆記憶體細分為三個區域:

  • 新生區(伊甸園區):Young/New
  • 養老區old
  • 永久區Perm
JVM-初見

新生區:類的誕生,成長和死亡的地方

分為:

  • 伊甸園區:所有對象都在伊甸園區new出來
  • 幸存0區和幸存1區:輕GC之後存下來的

老年區(養老區):多次輕GC存活下來的對象放在老年區

真理:經過研究,99%的對象都是臨時對象

永久區

這個區域常駐記憶體的,用來存放IDK自身攜帶的Class對象,Interface中繼資料,存儲的是Java運作時的一些環境或

類資訊,這個區域不存在垃圾回收。

關閉VM虛拟就會釋放這個區域的記憶體,一個啟動類,加載了大量的第三方jar包。

Tomcat部署了太多的應用,大量動态生成的反射類,不斷的被加載,直到記憶體滿,就會出現0OM;

  • jdk1.6之前:永久代,常量池是在方法區。
  • jdk1.7永久代,但是慢慢的退化了,去永久代, 常量池在堆中
  • jdk1.8之後:無永久代,常量池在元空間

注意:

元空間:邏輯上存在,實體上不存在 ,因為:存儲在本地磁盤内,不占用虛拟機記憶體

預設情況下,JVM使用的最大記憶體為電腦總記憶體的四分之一,JVM使用的初始化記憶體為電腦總記憶體的六十四分之一.

總結:

  • 棧:基本類型的變量,對象的引用變量,執行個體對象的方法
  • 堆:存放由new建立的對象和數組
  • 方法區:Class對象,static變量,常量池(常量)

public class Test2 {
    static String a="111111111111";

    public static void main(String[] args) {
        while (true){
            a=a+ new Random().nextInt(99999999)+new Random().nextInt(99999999);
        }
    }
}

           

下載下傳位址:https://www.ej-technologies.com/download/jprofiler/version_92

安裝完成後,需要在IDEA中安裝插件。

添加參數運作程式:

-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError:當出現OOM錯誤,會生成一個dump檔案(程序的記憶體鏡像)

在項目目錄下找到dump檔案,輕按兩下打開 , 可以看到什麼占用了大量的記憶體

配置參數 功能
-Xms 初始堆大小。如:-Xms256m
-Xmx 最大堆大小。如:-Xmx512m
-Xmn 新生代大小。通常為 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間為 = Eden + 1 個 Survivor,即 90%
-XX:NewRatio 新生代與老年代的比例,如 –XX:NewRatio=2,則新生代占整個堆空間的1/3,老年代占2/3
-XX:SurvivorRatio 新生代中 Eden 與 Survivor 的比值。預設值為 8。即 Eden 占新生代空間的 8/10,另外兩個 Survivor 各占 1/10
-XX:+PrintGCDetails 列印 GC 資訊
XX:+HeapDumpOnOutOfMemoryError 讓虛拟機在發生記憶體溢出時 Dump 出目前的記憶體堆轉儲快照,以便分析用

原理是此對象有一個引用,即增加一個計數,删除一個引用則減少一個計數。

垃圾回收時,隻用收集計數為 0 的對象。此算法最緻命的是無法處理循環引用的問題。

此算法把記憶體空間劃為兩個相等的區域,每次隻使用其中一個區域。

垃圾回收時,周遊目前使用區域,把正在使用中的對象複制到另外一個區域中。

此算法每次隻處理正在使用中的對象,是以複制成本比較小,同時複制過去以後還能進行相應的記憶體整理。

優點:不會出現碎片化問題

缺點:需要兩倍記憶體空間,浪費

JVM-初見

此算法執行分兩階段。

第一階段從引用根節點開始标記

可回收對象

第二階段周遊整個堆,把未标記的對象清除。

優點:不會浪費記憶體空間

缺點:此算法需要暫停整個應用,同時,會産生記憶體碎片

JVM-初見

此算法結合了 " 标記-清除 ” 和 “ 複制 ” 兩個算法的優點。

也是分兩階段,

第一階段從根節點開始标記所有

可回收對象

第二階段周遊整個堆,清除未标記對象并且把存活對象“壓縮”到堆的其中一塊,按順序排放。

此算法避免了“标記-清除”的碎片問題,同時也避免了“複制”算法的空間問題。

JVM-初見

JVM-初見

1.絕大多數剛剛被建立的對象會存放在Eden區

2.當Eden區第一次滿的時候,會觸發MinorGC(輕GC)。首先将Eden區的垃圾對象回收清除,并将存活的對象複制到S0,此時S1是空的。

3.下一次Eden區滿時,再執行一次垃圾回收,此次會将Eden和S0區中所有垃圾對象清除,并将存活對象複制到S1,此時S0變為空。

4.如此反複在S0和S1之間切換幾次(預設15次)之後,還存活的對象将他們轉移到老年代中。

5.當老年代滿了時會觸發FullGC(全GC)

MinorGC

  • 使用的算法是複制算法
  • 年輕代堆空間緊張時會被觸發
  • 相對于全收集而言,收集間隔較短

FullGC

  • 使用的算法一般是标記壓縮算法
  • 當老年代堆空間滿了,會觸發全收集操作
  • 可以使用 System.gc()方法來顯式的啟動全收集
  • 全收集非常耗時

垃圾回收器的正常比對:

JVM-初見

Serial 收集器是 Hotspot 運作在 Client 模式下的預設新生代收集器, 它的特點是:單線程收集,但它卻簡單而高效

JVM-初見

ParNew 收集器其實是前面 Serial 的多線程版本

與 ParNew 類似,Parallel Scavenge 也是使用複制算法,也是并行多線程收集器。

但與其他收集器關注盡可能縮短垃圾收集時間不同,Parallel Scavenge 更關注系統吞吐量,

系統吞吐量=運作使用者代碼時間/(運作使用者代碼時間+垃圾收集時間)

Serial Old 是 Serial 收集器的老年代版本, 同樣是單線程收集器,使用 “ 标記-整理 ” 算法

Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 使用多線程和 “ 标記-整理 ” 算

法,吞吐量優先

CMS是一種以擷取最短回收停頓時間為目标的收集器(CMS又稱多并發低暫停的收集器),

基于 ” 标記-清除 ” 算法實作, 整個 GC 過程分為以下 4 個步驟:

初始标記(CMS initial mark)

并發标記(CMS concurrent mark: GC Roots Tracing 過程)

重新标記(CMS remark)

并發清除(CMS concurrent sweep: 已死對象将會就地釋放, 注意:此處沒有壓縮)

G1将堆記憶體 “ 化整為零 ” ,将堆記憶體劃分成多個大小相等獨立區域(Region),

每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。

收集器能夠對扮演不同角色的Region采用不同的政策去處理,這樣無論是新建立的對象還是已經存活了一段時間、熬過多次收集的舊對象都能擷取很好的收集效果。

如果不設計STW,可能在垃圾回收時使用者線程就執行完了,堆中的對象都失去了引用,全部變成了垃圾,索性就

設計了STW,快速做完垃圾回收,再恢複使用者線程運作。