阿里P7架構師對Java虛擬機、類加載機制是怎麼理解的?

阿里P7架構師對Java虛擬機、類加載機制是怎麼理解的?

 

概述

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載java

(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化程序員

(Initialization)、使用(Using)和卸載(Unloading)7 個階段。其中驗證、準備、解析web

3 個部分統稱爲鏈接(Linking)bootstrap

於初始化階段,虛擬機規範則是嚴格規定了有且只有 5 種狀況必須當即對類進行「初始api

化」(而加載、驗證、準備天然須要在此以前開始):tomcat

1)遇到 new、getstatic、putstatic 或 invokestatic 這 4 條字節碼指令時,若是類沒有進行安全

過初始化,則須要先觸發其初始化。生成這 4 條指令的最多見的 Java 代碼場景是:使用數據結構

new 關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被 final 修飾、已在編譯期多線程

把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。架構

2)使用 java.lang.reflect 包的方法對類進行反射調用的時候,若是類沒有進行過初始化,

則須要先觸發其初始化。

3)當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類

的初始化。

4)當虛擬機啓動時,用戶須要指定一個要執行的主類(包含 main()方法的那個類),

虛擬機會先初始化這個主類。

5)當使用 JDK 1.7 的動態語言支持時,若是一個 java.lang.invoke.MethodHandle 實例最

後的解析結果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,而且這個

方法句柄所對應的類沒有進行過初始化,則須要先觸發其初始化。

注意:

對於靜態字段,只有直接定義這個字段的類纔會被初始化,所以經過其子類來引用父類中

定義的靜態字段,只會觸發父類的初始化而不會觸發子類的初始化。

常量 HELLOWORLD,但其實在編譯階段經過常量傳播優化,已經將此常量的值「hello

world」存儲到了 NotInitialization 類的常量池中,之後 NotInitialization 對常量

ConstClass.HELLOWORLD 的引用實際都被轉化爲 NotInitialization 類對自身常量池的引

用了。

也就是說,實際上 NotInitialization 的 Class 文件之中並無 ConstClass 類的符號引用入

口,這兩個類在編譯成 Class 以後就不存在任何聯繫了。

加載階段

虛擬機須要完成如下 3 件事情:

1)經過一個類的全限定名來獲取定義此類的二進制字節流。

2)將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。

3)在內存中生成一個表明這個類的 java.lang.Class 對象,做爲方法區這個類的各類數據

的訪問入口。

驗證

是鏈接階段的第一步,這一階段的目的是爲了確保 Class 文件的字節流中包含的信息符合

當前虛擬機的要求,而且不會危害虛擬機自身的安全。但從總體上看,驗證階段大體上會

完成下面 4 個階段的檢驗動做:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗

證。

準備階段

是正式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法

區中進行分配。這個階段中有兩個容易產生混淆的概念須要強調一下,首先,這時候進行

內存分配的僅包括類變量(被 static 修飾的變量),而不包括實例變量,實例變量將會在

對象實例化時隨着對象一塊兒分配在 Java 堆中。其次,這裏所說的初始值「一般狀況」下

是數據類型的零值,假設一個類變量的定義爲:

public static int value=123;

那變量 value 在準備階段事後的初始值爲 0 而不是 123,由於這時候還沒有開始執行任何

Java 方法,而把 value 賦值爲 123 的 putstatic 指令是程序被編譯後,存放於類構造器<

clinit>()方法之中,因此把 value 賦值爲 123 的動做將在初始化階段纔會執行。表 7-1

列出了 Java 中全部基本數據類型的零值。

假設上面類變量 value 的定義變爲:public static final int value=123;

編譯時 Javac 將會爲 value 生成 ConstantValue 屬性,在準備階段虛擬機就會根據

ConstantValue 的設置將 value 賦值爲 123。

解析階段

是虛擬機將常量池內的符號引用替換爲直接引用的過程

類初始化階段

是類加載過程的最後一步,前面的類加載過程當中,除了在加載階段用戶應用程序能夠經過

自定義類加載器參與以外,其他動做徹底由虛擬機主導和控制。到了初始化階段,才真正

開始執行類中定義的 Java 程序代碼在準備階段,變量已經賦過一次系統要求的初始值,

而在初始化階段,則根據程序員經過程序制定的主觀計劃去初始化類變量和其餘資源,或

者能夠從另一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。<

clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊(static{}

塊)中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的。

<clinit>()方法對於類或接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒

有對變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()方法。

虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,若是多個

線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()方法,其餘

線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。若是在一個類的<clinit

>()方法中有耗時很長的操做,就可能形成多個進程阻塞。

阿里P7架構師對Java虛擬機、類加載機制是怎麼理解的?

 

類加載器

如何自定義類加載器,看代碼

系統的類加載器

對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在 Java 虛擬機中

的惟一性,每個類加載器,都擁有一個獨立的類名稱空間。這句話能夠表達得更通俗一

些:比較兩個類是否「相等」,只有在這兩個類是由同一個類加載器加載的前提下才有意

義,不然,即便這兩個類來源於同一個 Class 文件,被同一個虛擬機加載,只要加載它們

的類加載器不一樣,那這兩個類就一定不相等。

這裏所指的「相等」,包括表明類的 Class 對象的 equals()方法、isAssignableFrom

()方法、isInstance()方法的返回結果,也包括使用 instanceof 關鍵字作對象所屬關

系斷定等狀況。

在自定義 ClassLoader 的子類時候,咱們常見的會有兩種作法,一種是重寫 loadClass 方

法,另外一種是重寫 findClass 方法。其實這兩種方法本質上差很少,畢竟 loadClass 也會

調用 findClass,可是從邏輯上講咱們最好不要直接修改 loadClass 的內部邏輯。我建議的

作法是隻在 findClass 裏重寫自定義類的加載方法。

loadClass 這個方法是實現雙親委託模型邏輯的地方,擅自修改這個方法會致使模型被破

壞,容易形成問題。所以咱們最好是在雙親委託模型框架內進行小範圍的改動,不破壞原

有的穩定結構。同時,也避免了本身重寫 loadClass 方法的過程當中必須寫雙親委託的重複

代碼,從代碼的複用性來看,不直接修改這個方法始終是比較好的選擇。

雙親委派模型

從 Java 虛擬機的角度來說,只存在兩種不一樣的類加載器:一種是啓動類加載器

(Bootstrap ClassLoader),這個類加載器使用 C++語言實現,是虛擬機自身的一部分;

另外一種就是全部其餘的類加載器,這些類加載器都由 Java 語言實現,獨立於虛擬機外

部,而且全都繼承自抽象類 java.lang.ClassLoader。

啓動類加載器(Bootstrap ClassLoader):這個類將器負責將存放在<JAVA_HOME>\lib

目錄中的,或者被-Xbootclasspath 參數所指定的路徑中的,而且是虛擬機識別的(僅按照

文件名識別,如 rt.jar,名字不符合的類庫即便放在 lib 目錄中也不會被加載)類庫加載到

虛擬機內存中。啓動類加載器沒法被 Java 程序直接引用,用戶在編寫自定義類加載器

時,若是須要把加載請求委派給引導類加載器,那直接使用 null 代替便可。

擴展類加載器(Extension ClassLoader):這個加載器由

sun.misc.Launcher$ExtClassLoader 實現,它負責加載<JAVA_HOME>\lib\ext 目錄中

的,或者被 java.ext.dirs 系統變量所指定的路徑中的全部類庫,開發者能夠直接使用擴展

類加載器。

應用程序類加載器(Application ClassLoader):這個類加載器由 sun.misc.Launcher

$App-ClassLoader 實現。因爲這個類加載器是 ClassLoader 中的 getSystemClassLoader

()方法的返回值,因此通常也稱它爲系統類加載器。它負責加載用戶類路徑

(ClassPath)上所指定的類庫,開發者能夠直接使用這個類加載器,若是應用程序中沒

有自定義過本身的類加載器,通常狀況下這個就是程序中默認的類加載器。

咱們的應用程序都是由這 3 種類加載器互相配合進行加載的,若是有必要,還能夠加入自

己定義的類加載器。

雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載

器。這裏類加載器之間的父子關係通常不會以繼承(Inheritance)的關係來實現,而是都

使用組合(Composition)關係來複用父加載器的代碼。

使用雙親委派模型來組織類加載器之間的關係,有一個顯而易見的好處就是 Java 類隨着

它的類加載器一塊兒具有了一種帶有優先級的層次關係。例如類 java.lang.Object,它存放在

rt.jar 之中,不管哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類

加載器進行加載,所以 Object 類在程序的各類類加載器環境中都是同一個類。相反,若是

沒有使用雙親委派模型,由各個類加載器自行去加載的話,若是用戶本身編寫了一個稱爲

java.lang.Object 的類,並放在程序的 ClassPath 中,那系統中將會出現多個不一樣的

Object 類,Java 類型體系中最基礎的行爲也就沒法保證,應用程序也將會變得一片混亂。

Tomcat 類加載機制

Tomcat 自己也是一個 java 項目,所以其也須要被 JDK 的類加載機制加載,也就必然存在

引導類加載器、擴展類加載器和應用(系統)類加載器。

Common ClassLoader 做爲 Catalina ClassLoader 和 Shared ClassLoader 的 parent,而

Shared ClassLoader 又可能存在多個 children 類加載器 WebApp ClassLoader,一個

WebApp ClassLoader 實際上就對應一個 Web 應用,那 Web 應用就有可能存在 Jsp 頁

面,這些 Jsp 頁面最終會轉成 class 類被加載,所以也須要一個 Jsp 的類加載器。

須要注意的是,在代碼層面 Catalina ClassLoader、Shared ClassLoader、Common

ClassLoader 對應的實體類實際上都是 URLClassLoader 或者 SecureClassLoader,通常

咱們只是根據加載內容的不一樣和加載父子順序的關係,在邏輯上劃分爲這三個類加載器;

而 WebApp ClassLoader 和 JasperLoader 都是存在對應的類加載器類的。

當 tomcat 啓動時,會建立幾種類加載器:

1 Bootstrap 引導類加載器 加載 JVM 啓動所需的類,以及標準擴展類(位於 jre/lib/ext

下)

2 System 系統類加載器 加載 tomcat 啓動的類,好比 bootstrap.jar,一般在 catalina.bat

或者 catalina.sh 中指定。位於 CATALINA_HOME/bin 下。

3 Common 通用類加載器 加載 tomcat 使用以及應用通用的一些類,位於

CATALINA_HOME/lib 下,好比 servlet-api.jar

4 webapp 應用類加載器每一個應用在部署後,都會建立一個惟一的類加載器。該類加載器

會加載位於 WEB-INF/lib 下的 jar 文件中的 class 和 WEB-INF/classes 下的 class 文件。

方法調用詳解

解析

調用目標在程序代碼寫好、編譯器進行編譯時就必須肯定下來。這類方法的調用稱爲解

析。

在 Java 語言中符合「編譯期可知,運行期不可變」這個要求的方法,主要包括靜態方法

和私有方法兩大類,前者與類型直接關聯,後者在外部不可被訪問,這兩種方法各自的特

點決定了它們都不可能經過繼承或別的方式重寫其餘版本,所以它們都適合在類加載階段

進行解析。

靜態分派

多見於方法的重載。

阿里P7架構師對Java虛擬機、類加載機制是怎麼理解的?

 

「Human」稱爲變量的靜態類型(Static Type),或者叫作的外觀類型(Apparent

Type),後面的「Man」則稱爲變量的實際類型(Actual Type),靜態類型和實際類型在

程序中均可以發生一些變化,區別是靜態類型的變化僅僅在使用時發生,變量自己的靜態

類型不會被改變,而且最終的靜態類型是在編譯期可知的;而實際類型變化的結果在運行

期纔可肯定,編譯器在編譯程序的時候並不知道一個對象的實際類型是什麼。

代碼中定義了兩個靜態類型相同但實際類型不一樣的變量,但虛擬機(準確地說是編譯器)

在重載時是經過參數的靜態類型而不是實際類型做爲斷定依據的。而且靜態類型是編譯期

可知的,所以,在編譯階段,Javac 編譯器會根據參數的靜態類型決定使用哪一個重載版

本,因此選擇了 sayHello(Human)做爲調用目標。全部依賴靜態類型來定位方法執行版

本的分派動做稱爲靜態分派。靜態分派的典型應用是方法重載。靜態分派發生在編譯階

段,所以肯定靜態分派的動做實際上不是由虛擬機來執行的。

動態分派

靜態類型一樣都是 Human 的兩個變量 man 和 woman 在調用 sayHello()方法時執行了

不一樣的行爲,而且變量 man 在兩次調用中執行了不一樣的方法。致使這個現象的緣由很明

顯,是這兩個變量的實際類型不一樣。

在實現上,最經常使用的手段就是爲類在方法區中創建一個虛方法表。虛方法表中存放着各個

方法的實際入口地址。若是某個方法在子類中沒有被重寫,那子類的虛方法表裏面的地址

入口和父類相同方法的地址入口是一致的,都指向父類的實現入口。若是子類中重寫了這

個方法,子類方法表中的地址將會替換爲指向子類實現版本的入口地址。PPT 圖中,Son

重寫了來自 Father 的所有方法,所以 Son 的方法表沒有指向 Father 類型數據的箭頭。但

是 Son 和 Father 都沒有重寫來自 Object 的方法,因此它們的方法表中全部從 Object 繼承

來的方法都指向了 Object 的數據類型。

基於棧的字節碼解釋執行引擎

Java 編譯器輸出的指令流,基本上]是一種基於棧的指令集架構,指令流中的指令大部分

都是零地址指令,它們依賴操做數棧進行工做。與

基於寄存器的指令集,最典型的就是 x86 的二地址指令集,說得通俗一些,就是如今咱們

主流 PC 機中直接支持的指令集架構,這些指令依賴寄存器進行工做。

舉個最簡單的例子,分別使用這兩種指令集計算「1+1」的結果,基於棧的指令集會是這

樣子的:

iconst_1

iconst_1

iadd

istore_0

兩條 iconst_1 指令連續把兩個常量 1 壓入棧後,iadd 指令把棧頂的兩個值出棧、相加,然

後把結果放回棧頂,最後 istore_0 把棧頂的值放到局部變量表的第 0 個 Slot 中。

若是基於寄存器,那程序可能會是這個樣子:

mov eax,1

add eax,1

mov 指令把 EAX 寄存器的值設爲 1,而後 add 指令再把這個值加 1,結果就保存在 EAX

寄存器裏面。

基於棧的指令集主要的優勢就是可移植,寄存器由硬件直接提供,程序直接依賴這些硬件

寄存器則不可避免地要受到硬件的約束。棧架構指令集的主要缺點是執行速度相對來講會

稍慢一些。全部主流物理機的指令集都是寄存器架構也從側面印證了這一點。

阿里P7架構師對Java虛擬機、類加載機制是怎麼理解的?

相關文章
相關標籤/搜索