從本課時開始我們就正式進入 JVM 的學習,如果你是一名軟件開發工程師,在日常工作中除了 Java 這個關鍵詞外,還有一個名詞也一定經常被提及,那就是 JVM。提到 JVM 我們經常會在面試中遇到這樣的問題:
* 為什么 Java 研發系統需要 JVM?
* 對你 JVM 的運行原理了解多少?
* 我們寫的 Java 代碼到底是如何運行起來的?
想要在面試中完美地回答這三個問題,就需要首先了解 JVM 是什么?它和 Java 有什么關系?又與 JDK 有什么淵源?接下來,我就帶你撥開這些問題的層層迷霧,想要弄清楚這些問題,我們首先需要從這三個維度去思考:
* JVM 和操作系統的關系?
* JVM、JRE、JDK 的關系?
* Java 虛擬機規范和 Java 語言規范的關系?
弄清楚這幾者的關系后,我們再以一個簡單代碼示例來看一下一個 Java 程序到底是如何執行的。
### JVM 和操作系統的關系

在武俠小說中,想要煉制一把睥睨天下的寶劍,是需要下一番功夫的。除了要有上等的鑄劍技術,還需要一鼎經百煉的劍爐,而工程師就相當于鑄劍的劍師,JVM 便是劍爐。
JVM 全稱 Java Virtual Machine,也就是我們耳熟能詳的 Java 虛擬機。它能識別 .class后綴的文件,并且能夠解析它的指令,最終調用操作系統上的函數,完成我們想要的操作。
一般情況下,使用 C++ 開發的程序,編譯成二進制文件后,就可以直接執行了,操作系統能夠識別它;但是 Java 程序不一樣,使用 javac 編譯成 .class 文件之后,還需要使用 Java 命令去主動執行它,操作系統并不認識這些 .class 文件。
你可能會想,我們為什么不能像 C++ 一樣,直接在操作系統上運行編譯后的二進制文件呢?而非要搞一個處于程序與操作系統中間層的虛擬機呢?
這就是 JVM 的過人之處了。大家都知道,Java 是一門抽象程度特別高的語言,提供了自動內存管理等一系列的特性。這些特性直接在操作系統上實現是不太可能的,所以就需要 JVM 進行一番轉換。
有了上面的介紹,我們就可以做如下的類比。
* JVM:等同于操作系統;
* Java 字節碼:等同于匯編語言。
Java 字節碼一般都比較容易讀懂,這從側面上證明 Java 語言的抽象程度比較高。你可以把 JVM 認為是一個翻譯器,會持續不斷的翻譯執行 Java 字節碼,然后調用真正的操作系統函數,這些操作系統函數是與平臺息息相關的。
如果你還是對上面的介紹有點模糊,可以參考下圖:

從圖中可以看到,有了 JVM 這個抽象層之后,Java 就可以實現跨平臺了。JVM 只需要保證能夠正確執行 .class 文件,就可以運行在諸如 Linux、Windows、MacOS 等平臺上了。
而 Java 跨平臺的意義在于一次編譯,處處運行,能夠做到這一點 JVM 功不可沒。比如我們在 Maven 倉庫下載同一版本的 jar 包就可以到處運行,不需要在每個平臺上再編譯一次。
現在的一些 JVM 的擴展語言,比如 Clojure、JRuby、Groovy 等,編譯到最后都是 .class 文件,Java 語言的維護者,只需要控制好 JVM 這個解析器,就可以將這些擴展語言無縫的運行在 JVM 之上了。
我們用一句話概括 JVM 與操作系統之間的關系:JVM 上承開發語言,下接操作系統,它的中間接口就是字節碼。
而 Java 程序和我們通常使用的 C++ 程序有什么不同呢?這里用兩張圖進行說明。


對比這兩張圖可以看到 C++ 程序是編譯成操作系統能夠識別的 .exe 文件,而 Java 程序是編譯成 JVM 能夠識別的 .class 文件,然后由 JVM 負責調用系統函數執行程序。
### JVM、JRE、JDK的關系

通過上面的學習我們了解到 JVM 是 Java 程序能夠運行的核心。但是需要注意,JVM 自己什么也干不了,你需要給它提供生產原料(.class 文件)。俗語說的好,巧婦難為無米之炊。它雖然功能強大,但仍需要為它提供 .class 文件。
僅僅是 JVM,是無法完成一次編譯,處處運行的。它需要一個基本的類庫,比如怎么操作文件、怎么連接網絡等。而 Java 體系很慷慨,會一次性將 JVM 運行所需的類庫都傳遞給它。JVM 標準加上實現的一大堆基礎類庫,就組成了 Java 的運行時環境,也就是我們常說的 JRE(Java Runtime Environment)。
有了 JRE 之后,我們的 Java 程序便可以在瀏覽器中運行了。大家可以看一下自己安裝的 Java 目錄,如果是只需要執行一些 Java 程序,只需要一個 JRE 就足夠了。
對于 JDK 來說,就更龐大了一些。除了 JRE,JDK 還提供了一些非常好用的小工具,比如 javac、java、jar 等。它是 Java 開發的核心,讓外行也可以煉劍!
我們也可以看下 JDK 的全拼,Java Development Kit。我非常怕 kit(裝備)這個單詞,它就像一個無底洞,預示著你永無休止的對它進行研究。JVM、JRE、JDK 它們三者之間的關系,可以用一個包含關系表示。
JDK>JRE>JVM

**Java 虛擬機規范和 Java 語言規范的關系**
我們通常談到 JVM,首先會想到它的垃圾回收器,其實它還有很多部分,比如對字節碼進行解析的執行引擎等。廣義上來講,JVM 是一種規范,它是最為官方、最為準確的文檔;狹義上來講,由于我們使用 Hotspot 更多一些,我們一般在談到這個概念時,會將它們等同起來。
如果再加上我們平常使用的 Java 語言的話,可以得出下面這樣一張圖。這是 Java 開發人員必須要搞懂的兩個規范。

左半部分是 Java 虛擬機規范,其實就是為輸入和執行字節碼提供一個運行環境。右半部分是我們常說的 Java 語法規范,比如 switch、for、泛型、lambda 等相關的程序,最終都會編譯成字節碼。而連接左右兩部分的橋梁依然是 Java 的字節碼。
如果 .class 文件的規格是不變的,這兩部分是可以獨立進行優化的。但 Java 也會偶爾擴充一下 .class 文件的格式,增加一些字節碼指令,以便支持更多的特性。
我們可以把 Java 虛擬機可以看作是一臺抽象的計算機,它有自己的指令集以及各種運行時內存區域,學過《計算機組成結構》的同學會在課程的后面看到非常多的相似性。
你可能會有疑問,如果我不學習 JVM,會影響我寫 Java 代碼么?理論上,這兩者沒有什么必然的聯系。它們之間通過 .class 文件進行交互,即使你不了解 JVM,也能夠寫大多數的 Java 代碼。就像是你寫 C++ 代碼一樣,并不需要特別深入的了解操作系統的底層是如何實現的。
但是,如果你想要寫一些比較精巧、效率比較高的代碼,就需要了解一些執行層面的知識了。了解 JVM,主要用在調優以及故障排查上面,你會對運行中的各種資源分配,有一個比較全面的掌控。
### 我們寫的 Java 代碼到底是如何運行起來的
最后,我們簡單看一下一個 Java 程序的執行過程,它到底是如何運行起來的。
這里的 Java 程序是文本格式的。比如下面這段 HelloWorld.java,它遵循的就是 Java 語言規范。其中,我們調用了 System.out 等模塊,也就是 JRE 里提供的類庫。
```
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
```
使用 JDK 的工具 javac 進行編譯后,會產生 HelloWorld 的字節碼。
我們一直在說 Java 字節碼是溝通 JVM 與 Java 程序的橋梁,下面使用 javap 來稍微看一下字節碼到底長什么樣子。
```
0 getstatic #2 <java/lang/System.out>
3 ldc #3 <Hello World>
5 invokevirtual #4 <java/io/PrintStream.println>
8 return
```
Java 虛擬機采用基于棧的架構,其指令由操作碼和操作數組成。這些字節碼指令,就叫作 opcode。其中,getstatic、ldc、invokevirtual、return 等,就是 opcode,可以看到是比較容易理解的。
我們繼續使用 hexdump 看一下字節碼的二進制內容。與以上字節碼對應的二進制,就是下面這幾個數字(可以搜索一下)。
```
b2 00 02 12 03 b6 00 04 b1
```
>[info]注意:這里是二進制文件的16進制表示,也就是hex,一般分析二進制文件都是以hex進行分析。
我們可以看一下它們的對應關系。
```
0xb2 getstatic 獲取靜態字段的值
0x12 ldc 常量池中的常量值入棧
0xb6 invokevirtual 運行時方法綁定調用方法
0xb1 return void 函數返回
```
opcode 有一個字節的長度(0~255),意味著指令集的操作碼個數不能操作 256 條。而緊跟在 opcode 后面的是被操作數。比如 b2 00 02,就代表了 getstatic #2 <java/lang/System.out>。
JVM 就是靠解析這些 opcode 和操作數來完成程序的執行的。當我們使用 Java 命令運行 .class 文件的時候,實際上就相當于啟動了一個 JVM 進程。
然后 JVM 會翻譯這些字節碼,它有兩種執行方式。常見的就是解釋執行,將 opcode + 操作數翻譯成機器代碼;另外一種執行方式就是 JIT,也就是我們常說的即時編譯,它會在一定條件下將字節碼編譯成機器碼之后再執行。
這些 .class 文件會被加載、存放到 metaspace 中,等待被調用,這里會有一個類加載器的概念。
而 JVM 的程序運行,都是在棧上完成的,這和其他普通程序的執行是類似的,同樣分為堆和棧。比如我們現在運行到了 main 方法,就會給它分配一個棧幀。當退出方法體時,會彈出相應的棧幀。你會發現,大多數字節碼指令,就是不斷的對棧幀進行操作。
而其他大塊數據,是存放在堆上的。Java 在內存劃分上會更為細致,關于這些概念,我們會在接下來的課時里進行詳細介紹。
最后大家看下面的圖,其中 JVM 部分,就是我們課程的要點。

### 選用的版本
既然 JVM 只是一個虛擬機規范,那肯定有非常多的實現。其中,最流行的要數 Oracle 的 HotSpot。
目前,最新的版本是 Java13(注意最新的LTS版本是11)。學技術當然要學最新的,我們以后的課時就以 13 版本的 Java 為基準,來講解發生在 JVM 上的那些事兒。
為了完成這個過程,你可以打開瀏覽器,輸入下載網址(https://www.oracle.com/technetwork/java/ javase/downloads/jdk13-downloads-5672538.html)并安裝軟件。當然你也可以用稍低點的版本,但是有些知識點會有些許差異。相信對于聰明的你來說,這寫都不算問題,因為整個 JVM,包括我們的調優,就是在不斷試錯中完成的。
### 小結
我們再回頭看看上面的三個問題。
#### 為什么 Java 研發系統需要 JVM?
JVM 解釋的是類似于匯編語言的字節碼,需要一個抽象的運行時環境。同時,這個虛擬環境也需要解決字節碼加載、自動垃圾回收、并發等一系列問題。JVM 其實是一個規范,定義了 .class 文件的結構、加載機制、數據存儲、運行時棧等諸多內容,最常用的 JVM 實現就是 Hotspot。
#### 對你 JVM 的運行原理了解多少?
JVM 的生命周期是和 Java 程序的運行一樣的,當程序運行結束,JVM 實例也跟著消失了。JVM 處于整個體系中的核心位置,關于其具體運行原理,我們在下面的課時中詳細介紹。
#### 我們寫的 Java 代碼到底是如何運行起來的?
一個 Java 程序,首先經過 javac 編譯成 .class 文件,然后 JVM 將其加載到`元數據`區,執行引擎將會通過`混合模式`執行這些字節碼。執行時,會翻譯成操作系統相關的函數。JVM 作為 .class 文件的黑盒存在,輸入字節碼,調用操作系統函數。
過程如下:Java 文件->編譯器>字節碼->JVM->機器碼。
### 總結
到這里本課時的內容就全部講完了,今天我們分別從三個角度,了解了 JVM 在 Java 研發體系中的位置,并以一個簡單的程序,看了下一個 Java 程序基本的執行過程。
我們所說的 JVM,狹義上指的就 HotSpot。如非特殊說明,我們都以 HotSpot 為準。我們了解到,Java 之所以成為跨平臺,就是由于 JVM 的存在。Java 的字節碼,是溝通 Java 語言與 JVM 的橋梁,同時也是溝通 JVM 與操作系統的橋梁。
JVM 是一個非常小的集合,我們常說的 Java 運行時環境,就包含 JVM 和一部分基礎類庫。如果加上我們常用的一些開發工具,就構成了整個 JDK。我們講解 JVM 就聚焦在字節碼的執行上面。
Java 虛擬機采用基于棧的架構,有比較豐富的 opcode。這些字節碼可以解釋執行,也可以編譯成機器碼,運行在底層硬件上,可以說 JVM 是一種混合執行的策略。
那么你可以思考一下,既然 JVM 的運行在棧上,那棧上都會有哪些數據?垃圾回收又發生在什么地方呢?同時,你也可以搜索一下 Java 的字節碼指令,看一下 opcode 的大體分類。這些問題的答案我們會在接下來的課時一一解答。
- 前言
- 開篇詞
- 基礎原理
- 第01講:一探究竟:為什么需要 JVM?它處在什么位置?
- 第02講:大廠面試題:你不得不掌握的 JVM 內存管理
- 第03講:大廠面試題:從覆蓋 JDK 的類開始掌握類的加載機制
- 第04講:動手實踐:從棧幀看字節碼是如何在 JVM 中進行流轉的
- 垃圾回收
- 第05講:大廠面試題:得心應手應對 OOM 的疑難雜癥
- 第06講:深入剖析:垃圾回收你真的了解嗎?(上)
- 第06講:深入剖析:垃圾回收你真的了解嗎?(下)
- 第07講:大廠面試題:有了 G1 還需要其他垃圾回收器嗎?
- 第08講:案例實戰:億級流量高并發下如何進行估算和調優
- 實戰部分
- 第09講:案例實戰:面對突如其來的 GC 問題如何下手解決
- 第10講:動手實踐:自己模擬 JVM 內存溢出場景
- 第11講:動手實踐:遇到問題不要慌,輕松搞定內存泄漏
- 第12講:工具進階:如何利用 MAT 找到問題發生的根本原因
- 第13講:動手實踐:讓面試官刮目相看的堆外內存排查
- 第14講:預警與解決:深入淺出 GC 監控與調優
- 第15講:案例分析:一個高死亡率的報表系統的優化之路
- 第16講:案例分析:分庫分表后,我的應用崩潰了
- 進階部分
- 第17講:動手實踐:從字節碼看方法調用的底層實現
- 第18講:大廠面試題:不要搞混 JMM 與 JVM
- 第19講:動手實踐:從字節碼看并發編程的底層實現
- 第20講:動手實踐:不為人熟知的字節碼指令
- 第21講:深入剖析:如何使用 Java Agent 技術對字節碼進行修改
- 第22講:動手實踐:JIT 參數配置如何影響程序運行?
- 第23講:案例分析:大型項目如何進行性能瓶頸調優?
- 彩蛋
- 第24講:未來:JVM 的歷史與展望
- 第25講:福利:常見 JVM 面試題補充