1. 程式人生 > >JVM系列(一) - JVM總體概述

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

可見性 層級 插入 驗證 虛擬 ring 啟動 計算機體系 main

前言

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

技術分享圖片

JVM屏蔽了與具體操作系統平臺相關的信息,使Java程序只需生成在Java虛擬機上一次編譯,多次運行,具有跨平臺性JVM在執行字節碼時,實際上最終還是把字節碼解釋成具體平臺上的機器指令執行。

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

本文將簡述以下內容:

  • JVM是什麽?
  • JVM能幹什麽?
  • JVM生命周期?
  • JVM組成架構?

正文


JVM是什麽

JDK、JRE和JVM對比

技術分享圖片

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

JDK

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

技術分享圖片

JRE

JRE(Java Runtime Environment)Java 運行時環境,JRE 是物理存在的,主要由Java APIJVM 組成,提供了用於執行 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普通線程。daemonJVM自己使用的線程比如GC線程,main方法的初始線程是non-daemon

  • 消亡:所有線程終止時,JVM實例結束生命。

JVM組成架構

JAVA 代碼執行過程如下:

技術分享圖片

1. 類加載器(Class Loader)

類加載器 負責加載程序中的類型(類和接口),並賦予唯一的名字予以標識。

JDK 默認提供的三種 ClassLoader如下:
技術分享圖片

類加載器的關系

  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)

    技術分享圖片

    • 驗證

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

    • 準備

      準備過程通常分配一個結構用來存儲類信息,這個結構中包含了類中定義的成員變量方法接口信息等。

    • 解析

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

  • 初始化(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高級特性與最佳實踐,機械工業出版社


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

技術分享圖片

本帳號將持續分享後端技術幹貨,包括虛擬機基礎,多線程編程,高性能框架,異步、緩存和消息中間件,分布式和微服務,架構學習和進階等學習資料和文章。

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