前言
JVM
是Java Virtual Machine
(Java虛擬機器)的縮寫,JVM
是一種用於計算裝置的規範,它是一個虛構的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。
JVM遮蔽了與具體作業系統平臺相關的資訊,使Java
程式只需生成在Java
虛擬機器上一次編譯,多次執行,具有跨平臺性。JVM
在執行位元組碼時,實際上最終還是把位元組碼解釋成具體平臺上的機器指令執行。
Java
虛擬機器包括一套位元組碼指令集、一組暫存器、一個棧、一個垃圾回收堆和一個儲存方法區。
本文將簡述以下內容:
正文
JVM是什麼
JDK、JRE和JVM對比
JVM
,JRE
,JDK
都是 java
語言的支柱,他們分工協作。但不同的是 Jdk
和 JRE
是真實存在的,而 JVM
是一個抽象的概念,並不真實存在。
JDK
JDK
(Java Development Kit) 是 Java
語言的軟體開發工具包(SDK
)。JDK
物理存在,是 programming tools
、JRE
和 JVM
的一個集合。
JRE
JRE
(Java Runtime Environment)Java
執行時環境,JRE
是物理存在的,主要由Java API
和 JVM
組成,提供了用於執行 java
應用程式最低要求的環境。
JVM
JVM
是一種用於計算裝置的規範,它是一個虛構的計算機的軟體實現,簡單的說,JVM
是執行byte code
位元組碼程式的一個容器。
JVM的特點
-
基於堆疊的虛擬機器:最流行的計算機體系結構,如英特爾
X86
架構和ARM
架構上執行基於 暫存器。比如,安卓的Davilk
虛擬機器就是基於 暫存器 結構,而JVM
是基於棧結構的。 -
符號引用 :除了基本型別以外的資料 (類和介面) 都是通過符號來引用,而不是通過顯式地使用記憶體地址來引用。
-
垃圾收集 :一個類的例項是由使用者程式建立和垃圾回收自動銷燬。
-
網路位元組順序 :
Java class
檔案用網路位元組碼順序來進行儲存,保證了小端的Intel x86
架構和大端的RISC
系列的架構之間的無關性。
JVM位元組碼
JVM
使用Java位元組碼的方式,作為Java
使用者語言 和 機器語言 之間的中間語言。實現一個通用的、 機器無關 的執行平臺。
JVM能幹什麼
基於安全方面考慮,JVM
要求在 class
檔案中使用強制性的語法和約束,但任意一門語言都可以轉換為被 JVM
接受的有效的 class
檔案。作為一個通用的、機器無關的執行平臺,任何其他語言的實現者都可將 JVM
當作他的語言產品交付媒介。
JVM
中執行過程如下:
- 載入程式碼
- 驗證程式碼
- 執行程式碼
- 提供執行環境
JVM
生命週期
-
啟動:任何一個擁有
main
方法的class
都可以作為JVM
例項執行的起點。 -
執行:
main
函式為起點,程式中的其他執行緒均有它啟動,包括daemon
守護執行緒和non-daemon
普通執行緒。daemon
是JVM
自己使用的執行緒比如GC
執行緒,main
方法的初始執行緒是non-daemon
。 -
消亡:所有執行緒終止時,
JVM
例項結束生命。
JVM
組成架構
JAVA
程式碼執行過程如下:
1. 類載入器(Class Loader)
類載入器 負責載入程式中的型別(類和介面),並賦予唯一的名字予以標識。
JDK
預設提供的三種 ClassLoader
如下:
類載入器的關係
-
Bootstrap Classloader
是在Java
虛擬機器啟動後初始化的。 -
Bootstrap Classloader
負責載入ExtClassLoader
,並且將ExtClassLoader
的父載入器設定為Bootstrap Classloader
-
Bootstrap Classloader
載入完ExtClassLoader
後,就會載入AppClassLoader
,並且將AppClassLoader
的父載入器指定為ExtClassLoader
。
類載入器的作用
Class Loader | 實現 | 負責載入 |
---|---|---|
Bootstrap Loader | C++ | %JAVA_HOME%/jre/lib , %JAVA_HOME%/jre/classes 以及-Xbootclasspath引數指定的路徑以及中的類 |
Extension ClassLoader | Java | %JAVA_HOME%/jre/lib/ext ,路徑下的所有classes 目錄以及java.ext.dirs 系統變數指定的路徑中類庫 |
Application ClassLoader | Java | Classpath 所指定的位置的類或者是jar 文件,它也是Java 程式預設的類載入器 |
雙親委託機制
Java
中ClassLoader
的載入採用了雙親委託機制,採用雙親委託機制載入類的時候採用如下的幾個步驟:
-
當前
ClassLoader
首先從自己已經載入的類中查詢是否此類已經載入,如果已經載入則直接返回原來已經載入的類。 -
當前
ClassLoader
的快取中沒有找到被載入的類的時候,委託父類載入器去載入,父類載入器採用同樣的策略,首先檢視自己的快取,然後委託父類的父類去載入,一直到Bootstrap ClassLoader
。 -
當所有的父類載入器都沒有載入的時候,再由當前的類載入器載入,並將其放入它自己的快取中,以便下次有載入請求的時候直接返回。
小結 :雙親委託機制的核心思想分為兩個步驟。其一,自底向上檢查類是否已經載入;其二,自頂向下嘗試載入類。
ClassLoader
隔離問題
每個類裝載器都有一個自己的名稱空間用來儲存已裝載的類。當一個類裝載器裝載一個類時,它會通過儲存在名稱空間裡的類全侷限定名(Fully Qualified Class Name
)進行搜尋來檢測這個類是否已經被載入了。
JVM
及 Dalvik
對類唯一的識別是 ClassLoader id
+ PackageName
+ ClassName
,所以一個執行程式中是有可能存在兩個包名和類名完全一致的類的。並且如果這兩個”類”不是由一個 ClassLoader
載入,是無法將一個類的示例強轉為另外一個類的,這就是 ClassLoader
隔離。
雙親委託 是 ClassLoader
類一致問題的一種解決方案,也是 Android
差價化開發和熱修復的基礎。
類裝載器特點
Java
提供了動態載入特性。在執行時的第一次引用到一個class
的時候會對它進行裝載(Loading) 、** 連結(Linking)** 和 ** 初始化(Initialization) ** ,而不是在編譯時進行。不同的JVM的實現不同,本文所描述的內容均只限於Hotspot JVM
。
JVM
的類裝載器負責動態裝載,Java
的類裝載器有如下幾個特點:
-
層級結構:Java裡的類裝載器被組織成了有父子關係的層級結構。Bootstrap類裝載器是所有裝載器的父親。
-
代理模式: 基於層級結構,類的代理可以在裝載器之間進行代理。當裝載器裝載一個類時,首先會檢查它在父裝載器中是否進行了裝載。如果上層裝載器已經裝載了這個類,這個類會被直接使用。反之,類裝載器會請求裝載這個類
-
可見性限制:一個子裝載器可以查詢父裝載器中的類,但是一個父裝載器不能查詢子裝載器裡的類。
-
不允許解除安裝:類裝載器可以裝載一個類但是不可以解除安裝它,不過可以刪除當前的類裝載器,然後建立一個新的類裝載器裝載。
類裝載器過程
-
載入(Loading)
首先,根據類的全限定名找到代表這個類的
Class
檔案,然後讀取到一個位元組陣列中。接著,這些位元組會被解析檢驗它們是否代表一個Class
物件 幷包含正確的major
、minor
版本資訊。直接父類 的類和介面也會被載入進來。這些操作一旦完成,類或者介面物件 就從二進位制表示中建立出來了。 -
連結(Linking)
連結是檢驗類或介面並準備型別和父類介面的過程。連結過程包含三步:校驗(Verifying)、準備(Preparing)、部分解析(Optionally resolving)。
- 驗證
這是類裝載中最複雜的過程,並且花費的時間也是最長的。任務是確保匯入型別的準確性,驗證階段做的檢查,執行時不需要再做。雖然減慢加了載速度,但是避免了多次檢查。
- 準備
準備過程通常分配一個結構用來儲存類資訊,這個結構中包含了類中定義的成員變數,方法 和介面資訊等。
- 解析
解析是可選階段,把這個類的常量池中的所有的符號引用改變成直接引用。如果不執行,符號解析要等到位元組碼指令使用這個引用時才會進行。
-
初始化(Initialization)
把類中的變數初始化成合適的值。執行靜態初始化程式,把靜態變數初始化成指定的值。
JVM
規範定義了上面的幾個任務,不過它允許具體執行的時候能夠有些靈活的變動。
2. 執行引擎(Execution Engine)
通過類裝載器裝載的,被分配到JVM
的執行時資料區的位元組碼會被執行引擎執行。
執行引擎 以指令為單位讀取 Java
位元組碼。它就像一個 CPU
一樣,一條一條地執行機器指令。每個位元組碼指令都由一個1位元組的操作碼和附加的運算元組成。執行引擎 取得一個操作碼,然後根據運算元來執行任務,完成後就繼續執行下一條操作碼。
不過 Java
位元組碼是用一種人類可以讀懂的語言編寫的,而不是用機器可以直接執行的語言。因此,執行引擎 必須把位元組碼轉換成可以直接被 JVM
執行的語言。
位元組碼 可以通過以下兩種方式轉換成機器語言:
-
直譯器
直譯器 一條一條地讀取位元組碼,解釋 並且 執行 位元組碼指令。因為它一條一條地解釋和執行指令,所以它可以很快地解釋位元組碼,但是執行起來會比較慢。這是解釋執行的語言的一個缺點。位元組碼這種“語言”基本來說是解釋執行的。
-
即時(Just-In-Time)編譯器
即時編譯器 被引入用來彌補直譯器的缺點。執行引擎 首先按照 解釋執行 的方式來執行,然後在合適的時候,即時編譯器 把 整段位元組碼 編譯成 原生程式碼。然後,執行引擎就沒有必要再去解釋執行方法了,它可以直接通過原生程式碼去執行它。執行原生程式碼比一條一條進行解釋執行的速度快很多。編譯後的程式碼可以執行的很快,因為原生程式碼是儲存在快取裡的。
Java
位元組碼是解釋執行的,但是沒有直接在 JVM
宿主執行原生程式碼快。為了提高效能,Oracle Hotspot
虛擬機器會找到執行最頻繁的位元組碼片段並把它們編譯成原生機器碼。編譯出的原生機器碼被儲存在非堆記憶體的程式碼快取中。
通過這種方法(JIT)
,Hotspot
虛擬機器將權衡下面兩種時間消耗:將位元組碼編譯成原生程式碼需要的額外時間和解釋執行位元組碼消耗更多的時間。
這裡插入一下 Android 5.0 以後用的 ART 虛擬機器使用的是 AOT 機制。
Dalvik
是依靠一個Just-In-Time (JIT)
編譯器去解釋位元組碼。開發者編譯後的應用程式碼需要通過一個直譯器在使用者的裝置上執行,這一機制並不高效,但讓應用能更容易在不同硬體和架構上執行。ART
則完全改變了這套做法,在應用安裝時就預編譯位元組碼到機器語言,這一機制叫Ahead-Of-Time (AOT)
編譯。在移除解釋程式碼這一過程後,應用程式執行將更有效率,啟動更快。
參考
周志明,深入理解Java虛擬機器:JVM高階特性與最佳實踐,機械工業出版社
歡迎關注技術公眾號: 零壹技術棧
本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。