Java虛擬機|JVM【適合初學者入門】
- 0. 前言
- 1. 學習JVM的目的
- 2. 主要的虛擬機
- 3. 什么是虛擬機
- 4. 源代碼到機器碼的程序
- 5. 位元組碼檔案的結構
- 6. Java虛擬機記憶體結構
- 7. JVM類的加載機制
- 8. JVM垃圾回識訓制
- 9. JVM垃圾回收期
- 10. 垃圾回收的幾種型別
- 11. JVM引數之堆疊空間配置
- 12. JVM引數之查看JVM引數
- 13. JVM引數之追蹤類資訊
- 14. JVM引數之GC日志配置
- 15. JDK性能監控命令
0. 前言
為什么要有標題0?不要問,問就是程式員數數都是從0開始的,
前段時間正在看《深入理解Java虛擬機》這本書,看完之后頗有感受,這本書寫的非常好,我本人也很喜歡周志明老師的風格,真心佩服周老師對虛擬機的理解這么透徹,但從書的標題也可以看得出“深入”二字,如同書名,該書內容確實對新手來說有些晦澀,所以在這里我總結了一篇Java虛擬機的博文,大家可以把它當做閱讀這本書的前奏,讓自己在心里有一些Java虛擬機的概念,并有一定的理解,如果喜歡的話,可以點個贊和收藏哦!
本文章參考了博主陳樹義的JVM專欄,并在已經過博主本人同意的情況下發布這篇文章,
1. 學習JVM的目的
- 深入地理解 Java 這門語言
- 為線上排查問題打下基礎
2. 主要的虛擬機
- 虛擬機的始祖:Sun Classic
- 無疾而終:Sun Exact VM
- 武林盟主:Sun HotSpot VM
- 百家爭鳴:BEA JRockit / IBM J9 VM
- 武林外傳(那些無名虛擬機):Apache Harmony、Google Android Dalvik VM、Mircosoft JVM等等
3. 什么是虛擬機
我們知道不同的作業系統底層的實作是不一樣的,因此在一個作業系統上編譯的機器碼不能在另一個作業系統上被識別,所以和其他語言不同,Java語言不直接編譯成與系統有關的機器碼,而是編譯位元組碼,再通過不同的系統上提前安裝好的Java虛擬機分別解釋成機器碼,
4. 源代碼到機器碼的程序
編譯器:
-
前端編譯器:源代碼到位元組碼,代表:Sun的javac
- 編譯器將Java源代碼編譯成為位元組碼檔案(A.java–>A.class),位元組碼檔案是由16進制數字組成
-
JIT 編譯器:從位元組碼到機器碼,代表:HotSpot VM的C1、C2
-
分類
- 使用 Java 解釋器解釋執行位元組碼,啟動速度快但運行速度慢
- 使用 JIT 編譯器(即時編譯器)將位元組碼轉化為本地機器代碼,啟動速度慢但運行速度快
- Client Compiler(C1 編譯器)
- Server Compiler(C2 編譯器)
-
運行模式
- 混合模式
- C1 和 C2 兩種模式混合起來使用(默認方式)
- 如果想單獨使用 C1 模式或 C2 模式,使用
-client或-server打開
- 解釋模式
- 所有代碼都解釋執行
- 使用
-Xint引數打開
- 編譯模式
- 優先采用編譯,但是無法編譯時也會解釋執行
- 使用
-Xcomp引數打開
- 混合模式
-
-
AOT 編譯器:源代碼到機器碼,代表:GNU Compiler for the Java(GCJ)
對比:
- 編譯速度上,解釋執行 > AOT 編譯器 > JIT 編譯器,
- 編譯質量上,JIT 編譯器 > AOT 編譯器 > 解釋執行,
5. 位元組碼檔案的結構
位元組碼檔案由以下七個部分組成
- 魔數與Class檔案版本
- 常量池
- 訪問標志
- 類索引、父類索引、介面索引
- 欄位表集合
- 方法表集合
- 屬性表集合
位元組碼檔案中的十六進制數字以若干位為單位,分別代表著以上的資訊,
具體內容可查看這篇文章:https://www.cnblogs.com/chanshuyi/p/jvm_serial_05_jvm_bytecode_analysis.html
6. Java虛擬機記憶體結構
-
虛擬機記憶體結構(官方也叫運行時資料區)
-
公有:所有執行緒都共享一個,包含公有資料
- Java堆:幾乎所有的實體物件
- 年輕代
- Eden區
- From Survivor 0區
- From Survivor 1區
- 老年代
- 年輕代
- 方法區(1.7版本稱為永久代(Permanent Space),1.8版本稱為元空間(MetaSpace)):每個類的結構資訊,例如:運行時常量池、欄位和方法資料、構造方法等
- 常量池:常量池其實是存放在方法區中的
- Java堆:幾乎所有的實體物件
-
私有:每個執行緒都有一個,包含私有資料
-
PC暫存器(Program Counter 暫存器):保存執行緒當前正在執行的方法
- 如果是native方法,保存的值是undefined
- 如果不是native方法,保存的值是Java虛擬機正在執行的位元組碼指令地址,
-
Java虛擬機堆疊
- 與執行緒同時創建,用來存盤堆疊幀,即存盤區域變數與一些程序結果的地方,
- 堆疊幀存盤的資料包括:區域變數表、運算元堆疊,
-
本地方法堆疊
- Java 虛擬機使用其他語言(例如 C 語言)來實作指令集解釋器時,會使用到本地方法堆疊,
-
-
當有一個物件需要分配時,先分配到年輕代的Eden區,等到Eden 區域記憶體不夠時,Java 虛擬機會啟動垃圾回收(GC),此時 Eden 區中沒有被參考的物件的記憶體就會被回收,而一些存活時間較長的物件則會進入到老年代,在JVM中-XX:MaxTenuringThreshold引數用來設定晉升到老年代所需要經歷的GC次數,即一個物件分配進來后,如果經歷這么多次的GC,它都還沒有被作為垃圾回收,也就是一直有被參考,那么這個物件到指定的GC次數之后就會晉升到老年代,
PC暫存器保存的是某個執行緒當前正在執行的方法,由于一個執行緒在某一時刻執行的方法只有唯一一個,而這個方法被叫做該執行緒的當前方法,
在JVM中除了這幾個記憶體外,其實還有直接記憶體、堆疊幀等,但用的比較少,
問:為什么給物件分配空間也需要分為年輕代和老年代呢?意義是什么?
根據經驗,有些物件的存活時間很長,而有些物件的存活時間很短,如果我們把它們混在一起,那么必然會導致有部分物件一直被掃描,但又一直不是垃圾,這就很浪費時間,那采取的措施就是掃描若干次之后,某個物件仍然不是垃圾,那就把它移動到老年區,
問:Eden:from:to磁區的比例是多少?
默認的虛擬機配置是 Eden:from :to = 8:1:1,這是IBM公司統計的結果,他們發現80%的物件存活的時間都很短,于是將Eden區設定為80%,
問:什么是native方法?
看以下文章:https://blog.csdn.net/qq_23501635/article/details/78902721
7. JVM類的加載機制
JVM 虛擬機執行 class 位元組碼的程序可以分為七個階段:加載、驗證、準備、決議、初始化、使用、卸載,
- 加載:把位元組碼資料加載到記憶體中,
- 驗證:加載完Class檔案并在方法區創建對應的Class物件后,JVM會對位元組碼流進行校驗
- JVM規范校驗 例如是否以
cafe babe開頭,主次版本號是否在當前虛擬機處理范圍之內等, - 代碼邏輯校驗 方法傳入引數是否與方法定義時相同,回傳引數型別是否與方法定義相同,參考了某個類,那這個類有沒有宣告等等,
- JVM規范校驗 例如是否以
- 準備:為「類變數」分配記憶體并初始化
- 記憶體分配物件:Java 中的變數有「類變數」和「類成員變數」兩種型別,「類變數」指的是被 static 修飾的變數,而其他所有型別的變數都屬于「類成員變數」,在準備階段,JVM 只會為「類變數」分配記憶體,而不會為「類成員變數」分配記憶體,「類成員變數」的記憶體分配需要等到初始化階段才開始,
- 初始化的型別:在準備階段,JVM 會為類變數分配記憶體,并為其初始化,但是這里的初始化指的是為變數賦予 Java 語言中該資料型別的零值,而不是用戶代碼里初始化的值,但如果一個變數是常量(被 static final 修飾)的話,那么在準備階段,屬性便會被賦予用戶希望的值,
- 決議:JVM 針對類或介面、欄位、類方法、介面方法、方法型別、方法句柄和呼叫點限定符 7 類參考進行決議,
- 作用:將其在常量池中的符號參考替換成直接其在記憶體中的直接參考,
- 初始化(最常見的就是我們new一個物件和反射這兩種情況,這個時候會為「類成員變數」分配記憶體,類成員變數不包括方法)
- 遇到 new、getstatic、putstatic、invokestatic 這四條位元組碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化,生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實體化物件的時候、讀取或設定一個類的靜態欄位(被final修飾、已在編譯器把結果放入常量池的靜態欄位除外)的時候,以及呼叫一個類的靜態方法的時候,
- 使用 java.lang.reflect 包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先觸發其初始化,
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化,
- 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類,
- 當使用 JDK1.7 動態語言支持時,如果一個 java.lang.invoke.MethodHandle實體最后的決議結果 REF_getstatic,REF_putstatic,REF_invokeStatic 的方法句柄,并且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發其初始化,
- 使用:當 JVM 完成初始化階段之后,JVM 便開始從入口方法開始執行用戶的程式代碼
- 卸載:當用戶程式代碼執行完畢后,JVM 便開始銷毀創建的 Class 物件,最后負責運行的 JVM 也退出記憶體,
8. JVM垃圾回識訓制
我們都在說回收垃圾,那么到底什么是垃圾?
事實上,如果一個物件不可能再被參考,那么這個物件就是垃圾,應該被回收,
那么怎么找到垃圾并回收呢?
首先我們會想到,用計數的方式來判斷,即當一個物件被參考時計數加一,被去除參考時減一,這樣,當計數為0時,我們就認為是垃圾,
這種方法有一個弊端,就是當A 參考了 B,B 參考了 C,C 參考了 A,它們各自的參考計數都為 1,但是它們三個物件卻從未被其他物件參考,只有它們自身互相參考,從垃圾的判斷思想來看,它們三個確實是不被其他物件參考的,但是此時它們的參考計數卻不為零,這就是參考計數法存在的回圈參考問題,
所以現在Java虛擬機使用的是GC Root Tracing 演算法,其大概的程序是這樣:從 GC Root 出發,所有可達的物件都是存活的物件,而所有不可達的物件都是垃圾,最后形成一個被參考物件集合,
那么擁有了這種演算法之后,如何回收垃圾呢?
這個時候就要用到垃圾回收演算法了,主要有三種:
- 標記清除演算法(缺點:產生空間碎片)
- 標記階段
- 標記所有被參考的物件,此時所有未被參考的物件就是垃圾物件
- 清除階段
- 清除所有未被標記的物件
- 標記階段
- 復制演算法(缺點:記憶體空間折半)
- 將記憶體分為兩塊,每次只用一塊記憶體,垃圾回收時,將正在使用的記憶體中的存活物件復制到未使用的記憶體塊中,然后清除正在使用的記憶體中的所有物件,之后交換記憶體塊的角色(注意:是交換角色,不是交換兩塊記憶體里面的物件!)
- 標記壓縮演算法:標記清除演算法的優化版
- 標記結算
- 從 GC Root 參考集合觸發去標記所有物件
- 壓縮階段
- 將所有存活的物件壓縮在記憶體的一邊,之后清理邊界外的所有空間,
- 標記結算
三者比較:
標記清除演算法:會產生記憶體碎片,但是不需要移動太多物件,比較適合在存活物件比較多的情況,
復制演算法:雖然需要將記憶體空間折半,并且需要移動存活物件,但是其清理后不會有空間碎片,比較適合存活物件比較少的情況,
標記壓縮演算法:標記清除演算法的優化版,減少了空間碎片,
綜上所述:每種演算法都有自己的優缺點,最好的方法當然是分情況靈活使用它們,而其實JVM虛擬機正是如此,因此,出現了分代演算法,
所謂分代演算法,就是根據 JVM 記憶體的不同記憶體區域,采用不同的垃圾回收演算法,
(舉個例子:老年代中物件的存活率幾乎可以是100%,這個時候如果使用復制演算法,作業量巨大!而對于新生代來說,很多物件都是沒有被參考的垃圾,所以適合使用復制演算法,因此,像前面說到的,新生代是有磁區的,即Eden 區域、from 區域、to 區域,并且比例是8:1:1,那么為什么要這么分呢?實際上前面已經講過,因為很多物件都是垃圾,所以復制之后的物件其實很少,所以我們先在Eden 區域、from 區域使用GC演算法,并將存活物件復制到to區域,然后洗掉Eden 區域、from 區域的所有物件,最后,交換from和to區域的角色并等待下一次GC)
實際上,除了分代的概念,還有磁區思想,即將整個堆空間劃分成連續的不同小區間,每一個小區間都獨立使用,獨立回收,這種演算法的好處是可以控制一次回收多少個區間,可以較好地控制 GC 時間,
9. JVM垃圾回收期
Java 虛擬機的垃圾回收器可以分為四大類別:
-
串行回收器
-
特點:單執行緒,在并發能力較弱的計算機上,性能較好,會觸發 Stop-The-World 現象,即其他執行緒都需要暫停,等待垃圾回收完成,
-
開啟命令:
-XX:UseSerialGC:新生代、老年代都使用串行回收器-XX:UseParNewGC:新生代使用 ParNew 回收器,老年代使用串行回收器-XX:UseParallelGC:新生代使用 ParallelGC 回收器,老年代使用串行回收器
-
分類:
-
新生代串行回收器
-
特點:最古老的一種、 JDK 中最基本的垃圾回收器之一
-
演算法:復制演算法
-
-
老年代串行回收器
-
特點:
-
演算法:標記壓縮演算法
-
-
-
-
并行回收器
- 特點:對比串行回收器有所改進,使用多執行緒進行垃圾回收,對于并行能力強的機器,可以有效縮短垃圾回收所使用的時間,會觸發 Stop-The-World 現象,即其他執行緒都需要暫停,等待垃圾回收完成,但因為是多執行緒,所以停頓時間要短于串行回收器
- 開啟命令
-XX:+UseParNewGC:新生代使用 ParNew 回收器,老年代使用串行回收器,-XX:UseConcMarkSweepGC:新生代使用 ParNew 回收器,老年代使用 CMS,-XX:ParallelGCThreads:指定 ParNew 回收器的作業執行緒數量,-XX:+UseParallelGC:新生代使用 Parallel 回收器,老年代使用串行回收器,-XX:+UseParallelOldGC:新生代使用 ParallelGC 回收器,老年代使用 ParallelOldGC 回收器,
- 分類:
- 新生代 ParNew 回收器
- 特點:只是簡單地將串行回收器多執行緒化,其余一樣
- 演算法:復制演算法
- 新生代 Parallel GC 回收器
- 特點:與新生代 ParNew 回收器類似,不同點是:其注重系統的吞吐量
- 新生代 ParNew 回收器
-
CMS 回收器
- 特點:關注系統停頓時間、多執行緒并行,
- 演算法:標記清除演算法
-
G1 回收器
-
特點:是 JDK 1.7 中使用的全新垃圾回收器,依然使用了分代垃圾回收,但增加了磁區演算法,從而使得Eden 區、From 區、Survivor 區和老年代等各塊記憶體不必連續,
-
目的:為了取代CMS回收器
-
開啟命令:
-
打開 G1 收集器,我們可以使用引數:
-XX:+UseG1GC -
設定目標最大停頓時間,可以使用引數:
-XX:MaxGCPauseMillis -
設定 GC 作業執行緒數量,可以使用引數:
-XX:ParallelGCThreads -
設定堆使用率觸發并發標記周期的執行,可以使用引數:
-XX:InitiatingHeapOccupancyPercent
-
-
作業流程:
- 新生代 GC
- 并發標記周期
- 混合收集
- 如果需要,可能進行 FullGC
-
10. 垃圾回收的幾種型別
Minor GC:從年輕代空間回收記憶體被稱為 Minor GC,有時候也稱之為 Young GC,
Major GC:從老年代空間回收記憶體被稱為 Major GC,有時候也稱之為 Old GC,
Young GC:如上
Old GC:如上
Full GC:Full GC 是清理整個堆空間 —— 包括年輕代、老年代和永久代(如果有的話)
Stop-The-World:是指在進行垃圾回收時因為標記或清理的需要,必須讓所有執行任務的執行緒停止執行任務,從而讓垃圾回收執行緒回收垃圾的時間間隔,
11. JVM引數之堆疊空間配置
- 堆空間:
- 年輕代:java -Xms20m -Xmn10M GCDemo
- Eden區
- 永久代(JDK1.7叫法,原方法區)
- 元空間(JDK1.8叫法,原方法區)
- 堆疊空間
- 直接記憶體
堆空間:java -Xms20m -Xmx30m GCDemo 設定 JVM 的初始堆大小為 20M,最大堆空間為 30M
年輕代:java -Xms20m -Xmn10M GCDemo 設定 JVM 堆初始大小為20M,其中年輕代的大小為 10M,剩下的自然為老年代的,有10M,
Eden區: java -Xms20m -Xmn10M -XX:SurvivorRatio=2 -XX:+PrintGCDetails GCDemo 我們在前面說過,年輕代分為eden 空間、from 空間、to 空間,這里我們設定堆初始大小為 20M,年輕代大小為 10M,年輕代的 SurvivorRatio 比例為 2,意思是eden/from=eden/to=2,那么最終分配的結果將會是:年輕代 10M,其中 Eden 區 5M、From 區 2.5M、To 區 2.5 M,老年代 10M,
永久代:java -XX:PermSize10m -XX:MaxPermSize50m -XX:+PrintGCDetails GCDemo 設定永久代初始大小為 10M,最大大小為 50M,
元空間:java -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=50m -XX:+PrintGCDetails GCDemo設定的是元空間發生 GC 的初始閾值為10M,設定元空間的最大大小為50M,
堆疊空間:java -Xss2m GCDemo 設定最大堆疊空間為 2M
直接記憶體:java -XX:MaxDirectMemorySize=50m GCDemo 設定直接記憶體最大值為 50M,默認為最大堆空間
12. JVM引數之查看JVM引數
程式運行時,列印虛擬機接收到的命令列顯式引數 -XX:+PrintVMOptions
輸入命令:
java -XX:+UseSerialGC -XX:+PrintVMOptions Demo
運行結果:
VM option '+UseSerialGC'
VM option '+PrintVMOptions'
Hello, I'm chenshuyi
程式運行時,列印傳遞給虛擬機的顯式和隱式引數 -XX:+PrintCommandLineFlags
輸入命令:
java -XX:+UseSerialGC -XX:+PrintCommandLineFlags Demo
運行結果:
-XX:InitialHeapSize=134217728 -XX:MaxHeapSize=2147483648 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC
Hello, I'm chenshuyi
程式運行時,列印所有系統引數-XX:+PrintFlagsFinal
輸入命令:
java -XX:+UseSerialGC -XX:+PrintFlagsFinal Demo > jvm_flag_final.txt
運行結果放在了jvm_flag_final.txt 檔案,打開后部分內容如下:
...
uintx InitialHeapSize := 134217728 {product}
...
uintx MaxMetaspaceSize = 18446744073709547520 {product}
...
uintx MetaspaceSize = 21807104 {pd product}
13. JVM引數之追蹤類資訊
跟蹤類的加載和卸載-verbose:class
輸入以下命令:
java -verbose:class Demo > class_load_info.txt
打開 class_load_info.txt 檔案
...省略...
[Loaded java.util.ArrayList from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
...省略...
[Loaded java.util.HashMap from /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/jre/lib/rt.jar]
...省略...
[Loaded com.chenshuyi.ClassLoadDemo from file:/Users/yurongchan/Yosemite/Code/practice/target/classes/]
...省略...
跟蹤類的加載-XX:+TraceClassLoading
跟蹤類的卸載-XX:+TraceClassUnloading
14. JVM引數之GC日志配置
Java 虛擬機的GC(Garbage Collection)日志系統,
| 引數 | 含義 |
|---|---|
| -XX:PrintGC | 列印GC日志 |
| -XX:+PrintGCDetails | 列印詳細的GC日志,還會在退出前列印堆的詳細資訊, |
| -XX:+PrintHeapAtGC | 每次GC前后列印堆資訊, |
| -XX:+PrintGCTimeStamps | 列印GC發生的時間, |
| -XX:+PrintGCApplicationConcurrentTime | 列印應用程式的執行時間 |
| -XX:+PrintGCApplicationStoppedTime | 列印應用由于GC而產生的停頓時間 |
| -XX:+PrintReferenceGC | 跟蹤軟參考、弱參考、虛參考和Finallize佇列, |
| -XLoggc | 將GC日志以檔案形式輸出, |
15. JDK性能監控命令
查看虛擬機行程:jps 命令
虛擬機統計資訊:jstat 命令
查看虛擬機引數:jinfo 命令
匯出堆到檔案:jmap 命令
堆分析工具:jhat 命令
查看執行緒堆疊:jstack 命令
遠程主機資訊收集:jstatd 命令
多功能命令列:jcmd 命令
性能統計工具:hprof 命令
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/310682.html
標籤:其他
