JAVA虛擬機器學習筆記

SIMBA1949發表於2018-04-01

JAVA虛擬機器學習筆記

1.java虛擬機器執行時資料區模型

這裡寫圖片描述

  • 程式計數器:是一塊較小的記憶體空間,它可以看做是當前執行緒所執行的位元組碼的行號指示器。此記憶體區域是唯一一個java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。
  • java虛擬機器棧:描述的是java方法執行的記憶體模型:每個方法在執行的同時會建立一個棧幀用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。區域性變數表存放了編譯期可知的各種基本資料型別(boolean、byte、char、short、int、float、long、double)、物件引用(reference型別)。
  • 本地方法棧:與java虛擬機器棧所發揮的作用非常相似,他們之間的區別不過是java虛擬機器棧為虛擬機器執行的是java程式碼(也就是位元組碼)的服務,本地方法棧則為虛擬機器使用到的Native方法服務。
  • java堆:java虛擬機器所管理的記憶體中最大的一塊。java堆是被所有執行緒所共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項。java堆是垃圾收集器主要管理區域,所以也成為GC堆。
  • 方法區:與java堆一樣,是各個執行緒所共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。雖然java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它有一個別名Non-Heap(非堆)。
  • 執行時常量池:是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池,用於存放編譯期生成的各種字面量(即常量)和符號引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。(常量池中不直接儲存字串常量,儲存的是首次建立此字串的引用)
  • 直接記憶體:並不是虛擬機器執行時資料區的一部分,也不是java虛擬機器規範中定義的記憶體區域。在JDK1.4中新加入I/O方式,它可以使用Native函式庫直接分配對外記憶體,然後通過一個儲存在java堆中的DircetByteBuffer物件作為這塊記憶體的引用進行操作。

2.物件的建立過程

3.物件的記憶體佈局

在HotSpot虛擬機器中,物件在記憶體中儲存的佈局可以分為3個區域:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)

  • 物件頭:包括倆部分,第一部分用於儲存物件自身的執行時資料,如雜湊碼、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,另一部分是型別指標,即物件指向它的類後設資料的指標。
  • 例項資料:是物件真正儲存的有效資訊,也是在程式程式碼中所定義的各種型別的欄位內容。無論是從父類繼承下來的,還是在子類中定義的,都需要記錄下來。
  • 對齊填充:並不是必然存在的,也沒有特別的含義,他僅僅起著佔位符的作用。

4.垃圾收集器與記憶體分配策略

4.1 判斷物件是否存活

4.1.1 引用計數演算法

給物件中新增一個引用計算器,每當有一個地方引用它,計數器的值就加 1;當引用失效就減 1;任何時刻計算器為 0 的物件就是不可能再被使用的。

4.1.2 可達性分析演算法

這個演算法的基本思路就是通過一系列的稱為“ GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到 GC Roots沒有任何引用鏈相連(即從 GC Roots到這個物件不可達)時,則證明此物件時不可用的。

在 java 語言中 ,可作為 GC Roots 的物件包括下面幾種:

  • 虛擬機器棧(棧幀中的本地變數表)中引用的物件
  • 方法區中類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中 JNI (即一般說的是 Native 方法)引用的物件

4.1.3 生存還是死亡

即使在可達性分析演算法中不可達的物件,也並非是“非死不可”的,這時候他們暫時處於“緩刑”階段,要真正宣告一個物件死亡,至少經歷倆次標記過程:如果物件在進行可達性分析後發現沒有與 GC Roots 相連線的引用鏈,那他將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行 finalize() 方法。當物件沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機器呼叫過,虛擬機器將這倆種情況都視為“沒有必要執行”。

如果這個物件被判定為有必要執行 finalize() 方法,那麼這個物件將會放置一個叫做 F-Queue 的佇列之中,並在稍後由一個虛擬機器自動建立的、低優先順序的 Finalizer 執行緒去執行它。這裡所謂的“執行”是指虛擬機器會觸發這個方法,但是並不承諾等待它執行結束,這個做的原因是,如果一個物件在 finalize() 方法中執行緩慢,或者發生了死迴圈(更極端的情況),將可能會導致 F-Queue 佇列中其他物件永久處於等待,甚至導致整個記憶體回收系統崩潰。 finalize() 方法是物件逃脫死亡命運的最後一次機會,稍後 GC 將對 F-Queue 中的物件進行第二次小規模的標記,如果物件要在 finalize() 中成功拯救自己—— 只要重新與引用鏈上的任何一個物件建立關聯即可,譬如把自己(this 關鍵字)賦值給某個類變數或者物件的成員變數,拿在第二次標記時它將被移除“即將回收”的集合;如果物件這時候還沒有逃脫,那基本上它就真的被回收了。

4.2 垃圾收集演算法

4.2.1 標記 - 清楚演算法

演算法分為“標記”和“清楚”倆階段:首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件。

它的主要不足有倆個:一個是效率問題,標記和清楚倆個過程的效率都不高;另外一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後再程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

4.2.2 複製演算法(主要用於新生代)

為了解決效率問題,一種稱為“複製”的收集演算法出現了,它將可用記憶體按容量劃分大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後在把已使用過的記憶體空間一次清理掉。這樣使得每次都對整個半區進行記憶體回收,記憶體分配時也不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。只是這種算的代價將記憶體縮小為了原來的一半,未免太高了一點。

IBM研究新生代中的物件98%是“朝生暮死”的,所以不需要按照 1:1 的比例來劃分記憶體空間,而將記憶體分為一塊較大的Eden空間和兩塊較小的 Survivor 空間,每次使用 Eden 和其中一塊 Survivor 。當回收時,將 Eden 和 Survivor 中還存活這的物件一次性複製到另一款 Survivor 空間上,最後清理掉 Eden 和剛才用過的 Survivor 空間。當 Survivor 空間不夠用是需要依賴其他記憶體(這裡只老年代)進行分配擔保。

4.2.3 標記 - 整理演算法(主要用於老年代)

標記 - 整理演算法,標記過程仍與“標記 - 清除”演算法一樣,但是後續步驟不是直接對可回收物件進行整理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

4.2.4 分代收集演算法

根據物件存活週期的不同將記憶體劃分幾塊。一般是把 JAVA 堆劃分為新聲代和老年代,根據各個年代的特點採用最適合的收集演算法。在新生代中,每次垃圾收集都會發現有大批物件死去,只有少量存活,那就選用複製演算法,只需付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高、沒有額外的空間對它進行分配擔保,必須使用“標記 - 清理”或者“標記 - 整理”演算法來進行回收。

4.3 垃圾收集器

4.3.1 Serial 收集器

4.3.2 ParNew 收集器

4.3.3 Parallel Scavenge 收集器

4.3.4 Serial Old 收集器

4.3.5 Parallel Old 收集器

4.3.6 CMS 收集器

4.3.7 G1 收集器

4.4 記憶體分配與回收策略

物件的記憶體分配,往大方向講,就是在堆上分配(但也可能經過 JIT 編譯後被拆散為標量型別並間接地棧上分配),物件主要分配在新生代的 Eden 區上,如果啟動了本地執行緒分配緩衝,將按照執行緒優先在 TLAB 上分配。少數情況下也可能會直接分配在老年代中,分配的規則不是百分之百固定的,其細節取決於當前使用的哪一種垃圾收集器組合,還有虛擬機器中與記憶體相關的引數的設定。

  • 物件會優先在 Eden 分配:大多數情況下,物件在新生代 Eden 區中分配。當 Eden 區沒有足夠空間進行分配時,虛擬機器將發起一次 Minor GC。
  • 大物件直接進入老年代:所謂的大物件是指,需要大量的連續記憶體空間的 java 物件,最典型的大物件就是那種很長的字串以及陣列。經常出現大物件容易導致記憶體還有不少空間是就提前觸發垃圾收集以獲取足夠的連續空間。
  • 長期存活的物件將進入老年代:虛擬機器給每個物件定義一個物件年齡(Age)計算器。如果物件在 Eden 出生並經過第一次 Minor GC 後仍然存活,並且能被 Survivor 容納的話,將被移動到 Survivor 空間中,並且物件年齡設為 1 。物件在 Survivor 區中每“熬過”一次 Minor GC ,年齡就增加一歲,當它的年齡增加到一定程度(預設為15),就將進入老年代。
  • 動態物件年齡判定:為了更好適應不同程式的記憶體狀況,虛擬機器並不是永遠地要求物件的年齡必須達到了 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 空間中相同年齡所有物件大小的總和大於 Survivor 空間的一半,年齡大於等於的改年齡的物件直接進入老年代。
  • 空間分配擔保:在發生 Minor GC 之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那麼 Minor GC 可以確保是安全的。如果不成立,則虛擬機器會檢視 HandlePromotionFailure 設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試進行一次 Minor GC ,儘管這次 Minor GC 是有風險的;如果小於,或者 HandlePromotionFailure 設定不允許冒險,那這是也要進行一次 Full GC.

5.虛擬機器效能監控與故障處理工具

5.1-JDK的命令列工具

Sun JDK 監控和故障處理工具

名稱 主要作用
jps JVM Process Status Tools,顯示指定系統內所有的HostSpot虛擬機器程式
jstat JVM Statistics Monitoring Tool,用於收集HostSpot虛擬機器各方面的執行資料
jinfo Configuration Info for Java,顯示虛擬機器配置資訊
jmap Memory Map for Java,生成虛擬機器的記憶體轉儲快照(heapdump檔案)
jhat JVM Heap Dump Browser,用於分析heapdump檔案,他會建立一HTTP/HTML伺服器,讓使用者可以在瀏覽器檢視分析結果
jstack Stack Trace for Java,顯示虛擬機器的執行緒快照

5.1.1-jps 虛擬機器程式狀況工具

jps命令格式:

jps [ options ] [ hostid ]

jps工具主要選項

選項 作用
-q 只輸出LVMID,省略主類的名稱
-m 輸出虛擬機器程式啟動時傳遞給主類main()函式引數
-l 輸出主類的全名,如果程式執行的是 jar 包,則輸出jar的路徑
-v 輸出虛擬機器程式啟動時JVM引數

5.1.2-jstat 虛擬機器統計資訊監視工具

jstat命令格式為:

jstat [ option  vmid [ s | ms ] [ count ] ]

注意:option代表使用者希望查詢的虛擬機器資訊,主要分為3類:類載入、垃圾收集、執行期編譯狀況。

選項 作用
-class 監視類裝載、解除安裝數量、總空間以及類裝載所耗費的時間
-gc 監視 java 對狀況,包括 Eden 區、兩個 Survivor 區、老年代、永久代等的容量、已用空間、GC 時間合計等資訊
-gccapacity 監視內容與 -gc 基本相同,但輸出主要關注 java 堆各個區域使用到的最大、最小空間
    gcutil
監視內容與 -gc 基本相同,但輸出主要關注已使用空間佔總空間的百分比
-gccause 與 -gcutil 功能一樣,但是會額外輸出導致上一次 GC 產生原因
-gcnew 監視新生代 GC 狀況
-gcnewcapacity 監視內容與 -gnew 基本相同,輸出主要關注使用到的最大、最小空間
-gcold 監視老年代 GC 狀況
-gcoldcapacity 監視內容與 -gold 基本相同,輸出主要關注使用到的最大、最小空間
-gcpermcapacity 輸出永久代使用到的最大、最小空間
-compiler 輸出 JIT 編譯期編輯過的方法、耗時等資訊
-printcompilation 輸出已經被 JIT 編譯的方法

5.1.3-jinfo Java配置資訊工具

jinfo命令格式:

jinfo [ option ] pid

5.1.4-jmap Java記憶體影像工具

jmap命令格式:

jmap [ option ] vmid

jmap工具主要選項

選項 作用
-dump 生成 java 堆轉儲快照。格式為:-dump:[live, ] format=b, file=< filename >,其中 live 子引數說明是否只 dump 出存活的物件
-finalizerinfo 顯示在 F-Queue 中等待 Finalizer 執行緒執行 Finalize() 方法的物件。只有在 Linux/Solaris 平臺有效
-heap 顯示 JAVA堆詳細資訊,如使用哪種回收器、引數配置、分代狀況等。只有在 Linux/Solaris 平臺有效
-histo 顯示堆中物件統計資訊,包括類、例項數量、合計容量
-permstat 以 ClassLoader 為統計口徑顯示永久代記憶體狀態。只有在 Linux/Solaris 平臺有效
-F 當虛擬機器程式對 -dump 選項沒有響應時,可使用這個選項強制生成 dump 快照。只有在 Linux/Solaris 平臺有效

5.1.5-jhat 虛擬機器堆轉儲快照分析工具

5.1.6-jstack Java堆疊工具

jstack命令格式:

jstack [ option ] vmid

jstack工具主要選項

選項 作用
-F 當正常輸出的請求不被響應時,強制輸出執行緒堆疊
-l 除堆疊外,顯示關於鎖的附加資訊
-m 如果呼叫到本地方法的話,可以顯示 C/C++的堆疊

5.1.7-HSDIS: JIT生成程式碼反彙編

5.2-JDK的視覺化工具

倆個功能強大的視覺化工具:JConsole和VisualVM

5.2.1-JConsole:Java監視與管理控制檯

5.2.2-VisualVM:多合一故障處理工具

6.類檔案結構

7.虛擬機器類載入機制

7.1類載入的時機

類從被載入到虛擬機器記憶體中開始,到解除安裝出內初為止,他的整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和解除安裝(Unloading)7個階段。其中驗證、準備、解析部分統稱為連線(Linking)。

這裡寫圖片描述

載入、驗證、準備、初始化和解除安裝這5個階段是確定的,類的載入過程必須按照這種順序按部就班地開始,而解析階段不一定:它在某些情況下可以在初始化階段之後在開始。

初始化階段,虛擬機器規範嚴格規定了有且只有5種情況必須立即對類進行“初始化”(而載入、驗證、準備自然需要在此之前開始)

  1. 遇到 new、getstatic、putstatic 或 invokestatic 這4條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。
  2. 使用 java.lang.reflect 包的方法對類進行反射呼叫的時候,如果類沒有進行初始化,則需要先觸發其初始化。
  3. 當初始化一個類的時候,如果發現其父類沒有進行過初始化,則需要先觸發其父類的初始化。
  4. 當虛擬機器啟動時,使用者需要制定一個要執行的主類(包含 main()方法的那個類),虛擬機器會先初始化這個主類。
  5. 當使用 JDK 1.7 的動態語言支援時,如果一個 java.lang.invoke.MethodHandle 例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有進行過初始化,則需要先觸發其初始化。

7.2 類載入器

7.2.1 類與類載入器

7.2.2 雙親委派模型

7.2.3 破壞雙親委派模型

嚴重申明

本文完全參考周志明老師的《深入理解 java 虛擬機器–JVM 高階特性與最佳實踐》一書,特別感謝周志明老師的辛苦付出。本文完全用於博主的自我學習和與 java 學習愛好者的分享,若用於商業用途請自覺聯絡周志明老師。謝謝。

相關文章