JVM 從入門到出門

一、JVM 是什么

Java虛擬機(Java Virtual Machine,JVM)是運行所有 Java 程序的抽象計算機,是Java語言的運行環(huán)境。

Java 是一種跨平臺的語言,但是 Java 源文件是不能直接運行的,而是需要將 Java 源文件編譯成一種“中間碼”——字節(jié)碼,但是字節(jié)碼也是不能直接運行,字節(jié)碼是需要在 Java 虛擬機(Java Virtual Machine,JVM)上運行。并且每個系統(tǒng)平臺都有自己的 JVM,所以 Java 語言編譯后能通過 JVM 在不同平臺上運行,從而實現(xiàn)了跨平臺。JVM 的功能就解釋執(zhí)行字節(jié)碼。

簡單的說,JVM 就是一個操作系統(tǒng),這個操作系統(tǒng)是基于其他操作系統(tǒng)之上的一個運行 Java 程序的操作系統(tǒng)。所以 Java 的跨平臺性實質(zhì)上是 字節(jié)碼 和 JVM 的跨平臺性。

總結起來就是,Java 的跨平臺性并不是 Java 文件能跨平臺運行,而是編譯后的字節(jié)碼文件能由不同平臺上的 JVM 轉化成平臺上的機器指令。

Java 跨平臺

JVM 執(zhí)行程序時,主要做了一下幾點事情:

  1. 加載 Class 文件
  2. 管理并分配內(nèi)存
  3. 執(zhí)行垃圾回收(參見《Java 垃圾回收(GC)機制》

下面分別說說。

二、JVM 如何加載 Class 文件

上面說了 Java 文件編譯成字節(jié)碼文件,然后將字節(jié)碼文件交給 JVM 加載,那 JVM 如何加載呢?

Java 程序啟動時,并不是一次把所有的類全部加載、運行,而是把保證程序運行的基礎類一次性加載到 JVM 中,其他類等到 JVM 用到的時候再加載。

加載有兩種方式:

  1. 隱式裝載:程序在運行過程中,遇到 new 等方式生成類或者子類對象、使用類或者子類的靜態(tài)域時,隱式調(diào)用類加載器(ClassLoader)加載對應的的類到 JVM 中。
  2. 顯式裝載:通過調(diào)用Class.forName()或者ClassLoader.loadClass(className)等方法,顯式加載需要的類。
類的生命周期

加載的過程包括了加載、驗證、準備、解析、初始化五個階段。在這五個階段中,加載、驗證、準備和初始化這四個階段發(fā)生的順序是確定的,而解析階段則不一定,它在某些情況下可以在初始化階段之后開始,這是為了支持 Java 語言的運行時綁定。另外注意這里的幾個階段是按順序開始,而不是按順序進行或完成,因為這些階段通常都是互相交叉地混合進行的,通常在一個階段執(zhí)行的過程中調(diào)用或激活另一個階段。

上述只是概念性的過程,對應到 JVM 的具體的執(zhí)行情況如下圖:

JVM 加載 Class 文件

其中:

  • Class Loader:依據(jù)特定格式,加載 Class 文件到內(nèi)存。
  • Runtime Data Area:JVM 內(nèi)存空間結構模型。
  • Execution Engine:對命令進行解析。
  • Native Interface:融合不同開發(fā)語言的原生庫為 Java 所用。

小結一下,類從編譯到執(zhí)行的過程:

  1. 編譯器將 Java 源文件編譯為 Class 字節(jié)碼文件;
  2. ClassLoader 將字節(jié)碼轉化為 JVM 中的對象;
  3. JVM 根據(jù)字節(jié)碼初始化對象。

接下來將結合上圖,具體談談 JVM 加載 Class 過程中很重要的兩部分:ClassLoader 和 Runtime Data Area。

三、ClassLoader 與 雙親委派模型

ClassLoader 在 Java 中有著非常重要的作用,它主要工作在 Class 的加載階段,其主要作用是從系統(tǒng)外部獲取 Class 二進制數(shù)據(jù)流。它是 Java 的核心組件,所有的 Class 都是由 ClassLoader 進行加載的,ClassLoader 負責通過將 Class 文件里的數(shù)據(jù)流裝載進系統(tǒng),然后交給 Java 虛擬機進行鏈接、初始化等操作。

那么 ClassLoader 的加載流程是怎樣的呢?這就涉及雙親委派模型了。

雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。

雙親委派模型

如上圖,雙親委派模型的工作過程是:

如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。

每一個層次的類加載器都是如此。因此,所有的加載請求最終都應該傳送到頂層的啟動類加載器中。

只有當父加載器反饋自己無法完成這個加載請求時(搜索范圍中沒有找到所需的類),子加載器才會嘗試自己去加載。

ClassLoader 加載過程

為什么需要雙親委托機制?

采用雙親委派模式的是好處是 Java 類隨著它的類加載器一起具備了帶有優(yōu)先級的層次關系,通過這種層級關可以避免類的重復加載,當父類已經(jīng)加載了該類時,就沒必要子 ClassLoader 再加載一次。

其次是考慮到安全因素,Java 核心 API 中定義類型不會被隨意替換,假設我自定義一個名為 java.lang.Integer 的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心 Java API 發(fā)現(xiàn)這個名字的類,發(fā)現(xiàn)該類已被加載,并不會重新加載自定義的 java.lang.Integer,而直接返回系統(tǒng)已加載過的 Integer.class,這樣便可以防止核心 API 庫被隨意篡改。

四、Runtime Data Area

Runtime Data Area

線程共享:Heap、Metaspace。
線程私有:本地方法棧、程序計數(shù)器、虛擬機棧。

程序計數(shù)器

程序計數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它可以看作是當前線程所執(zhí)行的字節(jié)碼的行號指示器,用來指示當前執(zhí)行的是哪條指令

由于 Java 虛擬機的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方式來實現(xiàn)的,在任何一個確定的時刻,一個處理器內(nèi)核都只會執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復到正確的執(zhí)行位置,每條線程都需要有一個獨立的程序計數(shù)器,各條線程之間計數(shù)器互不影響,獨立存儲,稱這類內(nèi)存區(qū)域為“線程私有”的內(nèi)存。

Java 虛擬機棧

與程序計數(shù)器一樣,Java 虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。

虛擬機棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame,是方法運行時的基礎數(shù)據(jù)結構)用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。當線程執(zhí)行一個方法時,就會隨之創(chuàng)建一個對應的棧幀,并將建立的棧幀壓棧。當方法執(zhí)行完畢之后,便會將棧幀出棧。

本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別是虛擬機棧為虛擬機執(zhí)行 Java 方法(也就是字節(jié)碼)服務,而本地方法棧則為虛擬機使用到的 Native 方法服務。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二為一。

Java堆

對于大多數(shù)應用來說,Java 堆(Java Heap)是 Java 虛擬機所管理的內(nèi)存中最大的一塊。Java 堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存。同時這里也是垃圾回收的核心區(qū)域。

方法區(qū)

方法區(qū)(Method Area)與 Java 堆一樣,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。

JVM 三大性能調(diào)優(yōu)參數(shù)調(diào)優(yōu)參數(shù):

  • -Xss:規(guī)定了每個線程堆棧的大小。一般情況下256K是足夠了。影響此進程中并發(fā)線程數(shù)大??;
  • -Xms:設置堆的初始分配大小,默認為物理內(nèi)存的 1/64;
  • -Xmx:堆的最大分配內(nèi)存,默認為物理內(nèi)存的 1/4;

堆與棧的區(qū)別:

  • 管理方式:棧由系統(tǒng)自動釋放,堆需要 GC 管理;
  • 空間大?。簵1榷研?;
  • 碎片相關:棧產(chǎn)生的碎片遠少于堆;
  • 分配方式:棧支持靜態(tài)和動態(tài)分配,而堆僅支持動態(tài)分配;
  • 效率:棧的效率比堆高。

五、總結

本篇文章先從 Java 的跨平臺性說起,提到了 Java 源文件編譯成字節(jié)碼文件,字節(jié)碼文件通過 JVM 運行在各個平臺上,然后通過了解類的生命周期,解釋了 JVM 是如何加載字節(jié)碼文件,接著介紹了在加載的過程,需要深入了解的 ClassLoader 和 Runtime Data Area。

這些也都是 JVM 的基礎。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容