JVM系列(一) - JVM總體概述

零壹技術棧發表於2018-07-17

前言

JVMJava Virtual Machine(Java虛擬機器)的縮寫,JVM是一種用於計算裝置的規範,它是一個虛構的計算機,是通過在實際的計算機上模擬模擬各種計算機功能來實現的。

JVM系列(一) - JVM總體概述

JVM遮蔽了與具體作業系統平臺相關的資訊,使Java程式只需生成在Java虛擬機器上一次編譯,多次執行,具有跨平臺性JVM在執行位元組碼時,實際上最終還是把位元組碼解釋成具體平臺上的機器指令執行。

Java虛擬機器包括一套位元組碼指令集、一組暫存器、一個、一個垃圾回收堆和一個儲存方法區

本文將簡述以下內容:

正文


JVM是什麼

JDK、JRE和JVM對比

JVM系列(一) - JVM總體概述

JVMJREJDK 都是 java 語言的支柱,他們分工協作。但不同的是 JdkJRE 是真實存在的,而 JVM 是一個抽象的概念,並不真實存在。

JDK

JDK(Java Development Kit) 是 Java 語言的軟體開發工具包(SDK)。JDK 物理存在,是 programming toolsJREJVM 的一個集合。

JVM系列(一) - JVM總體概述

JRE

JRE(Java Runtime Environment)Java 執行時環境,JRE 是物理存在的,主要由Java APIJVM 組成,提供了用於執行 java 應用程式最低要求的環境。

JVM系列(一) - JVM總體概述

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普通執行緒。daemonJVM自己使用的執行緒比如GC執行緒,main方法的初始執行緒是non-daemon

  • 消亡:所有執行緒終止時,JVM例項結束生命。

JVM組成架構

JAVA 程式碼執行過程如下:

JVM系列(一) - JVM總體概述

1. 類載入器(Class Loader)

類載入器 負責載入程式中的型別(類和介面),並賦予唯一的名字予以標識。

JDK 預設提供的三種 ClassLoader如下:

JVM系列(一) - JVM總體概述

類載入器的關係

  1. Bootstrap Classloader 是在Java虛擬機器啟動後初始化的。

  2. Bootstrap Classloader 負責載入 ExtClassLoader,並且將 ExtClassLoader的父載入器設定為 Bootstrap Classloader

  3. 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程式預設的類載入器

雙親委託機制

JavaClassLoader的載入採用了雙親委託機制,採用雙親委託機制載入類的時候採用如下的幾個步驟:

  1. 當前ClassLoader首先從自己已經載入的類中查詢是否此類已經載入,如果已經載入則直接返回原來已經載入的類。

  2. 當前ClassLoader的快取中沒有找到被載入的類的時候,委託父類載入器去載入,父類載入器採用同樣的策略,首先檢視自己的快取,然後委託父類的父類去載入,一直到Bootstrap ClassLoader

  3. 當所有的父類載入器都沒有載入的時候,再由當前的類載入器載入,並將其放入它自己的快取中,以便下次有載入請求的時候直接返回。

小結 :雙親委託機制的核心思想分為兩個步驟。其一,自底向上檢查類是否已經載入;其二,自頂向下嘗試載入類。

ClassLoader隔離問題

每個類裝載器都有一個自己的名稱空間用來儲存已裝載的類。當一個類裝載器裝載一個類時,它會通過儲存在名稱空間裡的類全侷限定名(Fully Qualified Class Name)進行搜尋來檢測這個類是否已經被載入了。

JVMDalvik 對類唯一的識別是 ClassLoader id + PackageName + ClassName,所以一個執行程式中是有可能存在兩個包名和類名完全一致的類的。並且如果這兩個”類”不是由一個 ClassLoader 載入,是無法將一個類的示例強轉為另外一個類的,這就是 ClassLoader 隔離。

雙親委託ClassLoader類一致問題的一種解決方案,也是 Android 差價化開發和熱修復的基礎。

類裝載器特點

Java提供了動態載入特性。在執行時的第一次引用到一個class的時候會對它進行裝載(Loading) 、** 連結(Linking)** 和 ** 初始化(Initialization) ** ,而不是在編譯時進行。不同的JVM的實現不同,本文所描述的內容均只限於Hotspot JVM

JVM的類裝載器負責動態裝載,Java的類裝載器有如下幾個特點:

  • 層級結構:Java裡的類裝載器被組織成了有父子關係的層級結構。Bootstrap類裝載器是所有裝載器的父親。

  • 代理模式: 基於層級結構,類的代理可以在裝載器之間進行代理。當裝載器裝載一個類時,首先會檢查它在父裝載器中是否進行了裝載。如果上層裝載器已經裝載了這個類,這個類會被直接使用。反之,類裝載器會請求裝載這個類

  • 可見性限制:一個子裝載器可以查詢父裝載器中的類,但是一個父裝載器不能查詢子裝載器裡的類。

  • 不允許解除安裝:類裝載器可以裝載一個類但是不可以解除安裝它,不過可以刪除當前的類裝載器,然後建立一個新的類裝載器裝載。

類裝載器過程

  • 載入(Loading)

    首先,根據類的全限定名找到代表這個類的Class檔案,然後讀取到一個位元組陣列中。接著,這些位元組會被解析檢驗它們是否代表一個Class物件 幷包含正確的majorminor版本資訊。直接父類 的類和介面也會被載入進來。這些操作一旦完成,類或者介面物件 就從二進位制表示中建立出來了。

  • 連結(Linking)

    連結是檢驗類或介面並準備型別和父類介面的過程。連結過程包含三步:校驗(Verifying)準備(Preparing)部分解析(Optionally resolving)

    JVM系列(一) - JVM總體概述

    • 驗證

    這是類裝載中最複雜的過程,並且花費的時間也是最長的。任務是確保匯入型別的準確性,驗證階段做的檢查,執行時不需要再做。雖然減慢加了載速度,但是避免了多次檢查。

    • 準備

    準備過程通常分配一個結構用來儲存類資訊,這個結構中包含了類中定義的成員變數方法介面資訊等。

    • 解析

    解析是可選階段,把這個類的常量池中的所有的符號引用改變成直接引用。如果不執行,符號解析要等到位元組碼指令使用這個引用時才會進行。

  • 初始化(Initialization)

把類中的變數初始化成合適的值。執行靜態初始化程式,把靜態變數初始化成指定的值。

JVM規範定義了上面的幾個任務,不過它允許具體執行的時候能夠有些靈活的變動。

2. 執行引擎(Execution Engine)

通過類裝載器裝載的,被分配到JVM執行時資料區的位元組碼會被執行引擎執行。

執行引擎指令為單位讀取 Java 位元組碼。它就像一個 CPU 一樣,一條一條地執行機器指令。每個位元組碼指令都由一個1位元組的操作碼和附加的運算元組成。執行引擎 取得一個操作碼,然後根據運算元來執行任務,完成後就繼續執行下一條操作碼

不過 Java 位元組碼是用一種人類可以讀懂的語言編寫的,而不是用機器可以直接執行的語言。因此,執行引擎 必須把位元組碼轉換成可以直接被 JVM 執行的語言。

位元組碼 可以通過以下兩種方式轉換成機器語言

  • 直譯器

    直譯器 一條一條地讀取位元組碼解釋 並且 執行 位元組碼指令。因為它一條一條地解釋和執行指令,所以它可以很快地解釋位元組碼,但是執行起來會比較慢。這是解釋執行的語言的一個缺點。位元組碼這種“語言”基本來說是解釋執行的。

  • 即時(Just-In-Time)編譯器

    即時編譯器 被引入用來彌補直譯器的缺點。執行引擎 首先按照 解釋執行 的方式來執行,然後在合適的時候,即時編譯器整段位元組碼 編譯成 原生程式碼。然後,執行引擎就沒有必要再去解釋執行方法了,它可以直接通過原生程式碼去執行它。執行原生程式碼比一條一條進行解釋執行的速度快很多。編譯後的程式碼可以執行的很快,因為原生程式碼是儲存在快取裡的。

Java 位元組碼是解釋執行的,但是沒有直接在 JVM 宿主執行原生程式碼快。為了提高效能,Oracle Hotspot 虛擬機器會找到執行最頻繁的位元組碼片段並把它們編譯成原生機器碼。編譯出的原生機器碼被儲存在非堆記憶體的程式碼快取中。

通過這種方法(JIT)Hotspot 虛擬機器將權衡下面兩種時間消耗:將位元組碼編譯成原生程式碼需要的額外時間和解釋執行位元組碼消耗更多的時間。

JVM系列(一) - JVM總體概述

這裡插入一下 Android 5.0 以後用的 ART 虛擬機器使用的是 AOT 機制。

Dalvik 是依靠一個 Just-In-Time (JIT)編譯器去解釋位元組碼。開發者編譯後的應用程式碼需要通過一個直譯器在使用者的裝置上執行,這一機制並不高效,但讓應用能更容易在不同硬體和架構上執行。ART 則完全改變了這套做法,在應用安裝時就預編譯位元組碼到機器語言,這一機制叫Ahead-Of-Time (AOT)編譯。在移除解釋程式碼這一過程後,應用程式執行將更有效率,啟動更快。

參考

周志明,深入理解Java虛擬機器:JVM高階特性與最佳實踐,機械工業出版社


歡迎關注技術公眾號: 零壹技術棧

image

本帳號將持續分享後端技術乾貨,包括虛擬機器基礎,多執行緒程式設計,高效能框架,非同步、快取和訊息中介軟體,分散式和微服務,架構學習和進階等學習資料和文章。

相關文章