JVM原理分析,看了都說好

  • 2019 年 10 月 6 日
  • 筆記

1 什麼是JVM?

JVM是Java Virtual Machine(Java虛擬機)的縮寫,是通過在實際的電腦上模擬模擬各種電腦功能來實現的。由一套位元組碼指令集、一組暫存器、一個棧、一個垃圾回收堆和一個存儲方法域等組成。JVM屏蔽了與作業系統平台相關的資訊,使得Java程式只需要生成在Java虛擬機上運行的目標程式碼(位元組碼),就可在多種平台上不加修改的運行,這也是Java能夠「一次編譯,到處運行的」原因。

2 JRE、JDK和JVM的關係

JRE(Java Runtime Environment, Java運行環境)是Java平台,所有的程式都要在JRE下才能夠運行。包括JVM和Java核心類庫和支援文件。

JDK(Java Development Kit,Java開發工具包)是用來編譯、調試Java程式的開發工具包。包括Java工具(javac/java/jdb等)和Java基礎的類庫(java API )。

JVM(Java Virtual Machine, Java虛擬機)是JRE的一部分。JVM主要工作是解釋自己的指令集(即位元組碼)並映射到本地的CPU指令集和OS的系統調用。Java語言是跨平台運行的,不同的作業系統會有不同的JVM映射規則,使之與作業系統無關,完成跨平台性。

下圖表示了JDK、JRE和JVM三者間的關係:

總結:使用JDK(調用JAVA API)開發JAVA程式後,通過JDK中的編譯程式(javac)將Java程式編譯為Java位元組碼,在JRE上運行這些位元組碼,JVM會解析並映射到真實作業系統的CPU指令集和OS的系統調用。

3 JVM原理

Java 體系結構介紹:

  • Class Loader(類載入器):用於裝載.class文件。
  • Execution Engine(執行引擎):用於執行位元組碼或者本地方法。
  • 運行時數據區:方法區、堆、java棧、pc暫存器、本地方法棧。

JVM生命周期介紹:

Java實例對應一個獨立運行的Java程式(進程級別

1.啟動。啟動一個Java程式,一個JVM實例就產生。擁有public static void main(String[] args)函數的class可以作為JVM實例運行的起點。

2.運行。main()作為程式初始執行緒的起點,任何其他執行緒均可由該執行緒啟動。JVM內部有兩種執行緒:守護執行緒和非守護執行緒,main()屬於非守護執行緒,守護執行緒通常由JVM使用,程式可以指定創建的執行緒為守護執行緒。

3.消亡。當程式中的所有非守護執行緒都終止時,JVM才退出;若安全管理器允許,程式也可以使用Runtime類或者System.exit()來退出。

JVM執行引擎實例則對應了屬於用戶運行程式執行緒它是執行緒級別的。

Java類載入器:

Java載入類的過程:

1.裝載(loading):負責找到二進位位元組碼並載入至JVM中,JVM通過類名、類所在的包名、ClassLoader完成類的載入。因此,標識一個被載入了的類:類名 + 包名 + ClassLoader實例ID。

2.鏈接(linking):負責對二進位位元組碼的格式進行校驗、初始化裝載類中的靜態變數以及解析類中調用的介面。

完成校驗後,JVM初始化類中的靜態變數,並將其賦值為默認值。

最後對比類中的所有屬性、方法進行驗證,以確保要調用的屬性、方法存在,以及具備訪問許可權(例如private、public等),否則會造成NoSuchMethodError、NoSuchFieldError等錯誤資訊。

3.初始化(initializing):負責執行類中的靜態初始化程式碼、構造器程式碼以及靜態屬性的初始化,以下四種情況初始化過程會被觸發。

  • 調用 new
  • 反射調用了類中的方法
  • 子類調用了初始化
  • JVM啟動過程終止定的初始化類

JVM類載入順序:

層級結構

1.Booststrap ClassLoader

跟ClassLoader,C++實現,JVM啟動時初始化此ClassLoader,並由此完成$JAVA_HONE中jre/lib/rt.jar(Sun JDK的實現)中所有class文件的載入,這個jar中包含了java規範定義的所有介面以及實現。

2.Extension ClassLoader

JVM用此classloader來載入擴展功能的一些jar包

3.System ClassLoader

JVM用此ClassLoader來載入啟動參數中指定的ClassPath中的jar包以及目錄,在Sun JDK中ClassLoader對應的類名為AppClassLoader。

4.User-Defined ClassLoader

User-Defined ClassLoader是Java開發人員繼承ClassLoader抽象類實現的ClassLoader,基於自定義的ClassLoader可用於載入非ClassPath中的jar以及目錄。

委派模式(Delegation Mode)

當JVM載入一個類的時候,下層的載入器會將任務給上一層類載入器,上一層載入檢查它的命名空間中是否已經載入這個類,如果已經載入,直接使用這個類。如果沒有載入,繼續往上委託直到頂部。檢查之後,按照相反的順序進行載入。如果Bootstrap載入器不到這個類,則往下委託,直到找到這個類。一個類可以被不同的類載入器載入。

可見性限制:下層的載入器能夠看到上層載入器中的類,反之則不行,委派只能從下到上。

不允許卸載類:類載入器可以載入一個類,但不能夠卸載一個類。但是類載入器可以被創建或者刪除。

JVM執行引擎

類載入器將位元組碼載入記憶體後,執行引擎以java位元組碼為單元,讀取java位元組碼。java位元組碼機器讀不懂,必須將位元組碼轉化為平台相關的機器碼。這個過程就是由執行引擎完成的。

在執行方法時JVM提供了四種指令來執行:

  • invokestatic:調用類的static方法。
  • invokevirtual:調用對象實例的方法。
  • invokeinterface:將屬性定義為介面來進行調用。
  • invokespecial:JVM對於初始化對象(Java構造器的方法為:)以及調用對象實例的私有方法時。

主要的執行計數:

  • 解釋,即時執行,自適應優化、晶片級直接執行。
  • 解釋屬於第一代JVM
  • 即時編譯JIT屬於第二代JVM

自適應優化(目前sun的HotspotJVM採用這種技術),吸取第一代JVM和第二代JVM的經驗,採用兩者結合的方式,開始對所有的程式碼都採用解釋執行的方式,並監視程式碼執行情況,然後對那些經常調用的方法啟動一個後台執行緒,將其編譯為本地程式碼,並進行優化。若方法不再頻繁使用,則取消編譯過程式碼,仍對其進行解釋執行。

Java運行時數據區

PC暫存器

用於存儲每個執行緒下一步將要執行的JVM指令,若該方法為native的,則PC暫存器中不存儲任何資訊。Java多執行緒情況下,每個執行緒都有一個自己的PC,以便完成不同執行緒上下文環境的切換。

JVM棧

JVM棧是執行緒私有的,每個執行緒創建的同時都會創建JVM棧,JVM棧中存放當前執行緒中局部基本類型的變數(Java中定義的八種基本類型:boolean、char、byte、short、int、long、float、double)、部分的返回結果以及Stack Frame,非基本類型的對象在JVM棧上僅存放一個指向堆的地址。

堆(Heap)

它是JVM用來存儲對象實例以及數組值的區域,可以認為Java中所有通過new創建的對象的記憶體都在此分配,Heap中的對象的記憶體需要等待GC進行回收。

堆在JVM啟動的時候就被創建,堆中儲存了各種對象,這些對象被自動管理記憶體系統(Automatic Storage Management System),也就是常說的「Garbage Collector(垃圾回收器)」管理。這些對象無需、也無法顯示地被銷毀。

JVM將Heap分為兩塊:新生代New Generation和舊生代Old Generation

堆是JVM中所有執行緒共享的,因此在其上進行對象記憶體的分配均需要進行加鎖,導致new對象的開銷比較大。

Sun Hotspot JVM為了提升對象記憶體分配的效率,對於所有創建的執行緒都會分配一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據運行的情況計算而得,在TLAB上分配對象時不需要加鎖,因此JVM在給執行緒對象分配記憶體時會盡量的在TLAB上分配,在這種情況下JVM中分配對象記憶體的性能和C基本是一樣的,但如果對象過大的話則仍然要直接使用堆空間分配。

TLAB僅作用於新生代的Eden Space,因此在編寫Java程式時,通常多個小的對象比大的對象分配起來更加高效。

所有新創建的Object都將會存儲在新生代Young Generation中。如果Young Generation的數據在一次或多次GC後存活下來,那麼將被轉移到OldGeneration。新的Object總是創建在Eden Space。

方法區域(Method Area)

在Sun JDK中這塊區域對應的為PermanetGeneration,又稱為持久代。

方法區域存放所載入類的資訊(名稱、修飾符等)、類中的靜態變數、類中定義為final類型的常量、類中的Field資訊、類中的方法資訊,當開發人員在程式中通過Class對象中的getName,isInstance等方法來獲取資訊時,這些數據都來源於方法區域,同時方法區域也是全局共享的,在一定條件下它也會被GC,當方法區域需要使用的記憶體超過其允許的大小時,就會拋出OutOfMemory的錯誤資訊。

運行時常量池(Runtime Constant Pool)

存放的為類中的固定常量資訊、方法和Field的引用資訊等,其空間從方法區域中分配。

本地方法堆棧(Native Method Stacks)

JVM採用本地方法堆來支援native方法的執行,此區域用於存儲每個native方法調用的狀態。

JVM垃圾回收

GC的基本原理:將記憶體中不再被使用的對象進行回收,GC中用於回收的方法稱為收集器,由於GC需要消耗一些資源和時間,Java在對對象生命周期特徵進行分析後,按照新生代、舊生代的方式來對對象進行收集,以儘可能的縮短GC對應用造成的暫停。

對新生代的對象收集稱為minor GC

對舊生代的對象收集稱為Full GC

程式中主動調用System.gc()強制執行的GC為Full GC。

不同的對象引用類型,GC會採用不同的方法進行回收,JVM對象的引用分為了四種類型:

  • 強引用:默認情況下,對象採用的均為強引用(這個對象的實例沒有其他對象引用時, GC時才會被回收)
  • 軟引用:軟引用是Java中提供的一種比較適合於快取場景的應用(只有記憶體不夠的情況下才會被GC)
  • 弱引用:在GC時一定會被GC回收。
  • 虛引用:虛引用只是用來得知對象是否被GC。