编程知识 cdmana.com

Java虛擬機|JVM【適合初學者入門】

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)):每個類的結構信息,例如:運行時常量池、字段和方法數據、構造方法等
      • 常量池:常量池其實是存放在方法區中的
    • 私有:每個線程都有一個,包含私有數據

      • 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開頭,主次版本號是否在當前虛擬機處理範圍之內等。
    • 代碼邏輯校驗 方法傳入參數是否與方法定義時相同,返回參數類型是否與方法定義相同,引用了某個類,那這個類有沒有聲明等等。
  • 准備:為「類變量」分配內存並初始化
    • 內存分配對象: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 回收器類似,不同點是:其注重系統的吞吐量
  • 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 命令

版权声明
本文为[Jack·Kwok]所创,转载请带上原文链接,感谢
https://cdmana.com/2021/10/20211014030420165b.html

Tags java jvm
Scroll to Top