一、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 轉化成平臺上的機器指令。

JVM 執(zhí)行程序時,主要做了一下幾點事情:
- 加載 Class 文件
- 管理并分配內(nèi)存
- 執(zhí)行垃圾回收(參見《Java 垃圾回收(GC)機制》)
下面分別說說。
二、JVM 如何加載 Class 文件
上面說了 Java 文件編譯成字節(jié)碼文件,然后將字節(jié)碼文件交給 JVM 加載,那 JVM 如何加載呢?
Java 程序啟動時,并不是一次把所有的類全部加載、運行,而是把保證程序運行的基礎類一次性加載到 JVM 中,其他類等到 JVM 用到的時候再加載。
加載有兩種方式:
- 隱式裝載:程序在運行過程中,遇到 new 等方式生成類或者子類對象、使用類或者子類的靜態(tài)域時,隱式調(diào)用類加載器(ClassLoader)加載對應的的類到 JVM 中。
- 顯式裝載:通過調(diào)用Class.forName()或者ClassLoader.loadClass(className)等方法,顯式加載需要的類。

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

其中:
- Class Loader:依據(jù)特定格式,加載 Class 文件到內(nèi)存。
- Runtime Data Area:JVM 內(nèi)存空間結構模型。
- Execution Engine:對命令進行解析。
- Native Interface:融合不同開發(fā)語言的原生庫為 Java 所用。
小結一下,類從編譯到執(zhí)行的過程:
- 編譯器將 Java 源文件編譯為 Class 字節(jié)碼文件;
- ClassLoader 將字節(jié)碼轉化為 JVM 中的對象;
- 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 的加載流程是怎樣的呢?這就涉及雙親委派模型了。
雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應當有自己的父類加載器。

如上圖,雙親委派模型的工作過程是:
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。
每一個層次的類加載器都是如此。因此,所有的加載請求最終都應該傳送到頂層的啟動類加載器中。
只有當父加載器反饋自己無法完成這個加載請求時(搜索范圍中沒有找到所需的類),子加載器才會嘗試自己去加載。

為什么需要雙親委托機制?
采用雙親委派模式的是好處是 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

線程共享: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 的基礎。