1. 程式人生 > >阿里P7架構師對Java虛擬機器、類載入機制是怎麼理解的?

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

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

 

概述

類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入

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

(Initialization)、使用(Using)和解除安裝(Unloading)7 個階段。其中驗證、準備、解析

3 個部分統稱為連線(Linking)

於初始化階段,虛擬機器規範則是嚴格規定了有且只有 5 種情況必須立即對類進行“初始

化”(而載入、驗證、準備自然需要在此之前開始):

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虛擬機器、類載入機制是怎麼理解的?

相關推薦

阿里P7架構Java虛擬機器載入機制是怎麼理解的?

  概述 類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入 (Loading

Java虛擬機器載入機制詳解

    大家知道,我們的Java程式被編譯器編譯成class檔案,在class檔案中描述的各種資訊,最終都需要載入到虛擬機器記憶體才能執行和使用,那麼虛擬機器是如何載入這些class檔案的呢?在載入class檔案的過程中虛擬機器又幹了哪些事呢?今天我們來解密虛擬機器的類載入機制。

Java虛擬機器(三) 載入機制

類載入機制 ** 類載入器分類** 一、類載入器一般分為兩種,一種是JDK預設的,一種是使用者自定義的,JDK預設的載入器一般分為以下三類 1、Bootstrap ClassLoader 啟動類載入器:由native code實現,並非java程式碼.載入類的路徑為 3、 System Class

【深入理解Java虛擬機器載入機制

本文內容來源於《深入理解Java虛擬機器》一書,非常推薦大家去看一下這本書。本系列其他文章:【深入理解Java虛擬機器】垃圾回收機制1、類載入機制概述虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Jav

Java虛擬機器載入機制

一、引言 關於類的載入機制,我們先從面試題開始: public class ClassLoaderProcess { public static void main(String[] args) { System.out.pri

系統架構Java虛擬OSGi—JVM高級性能架構項目實戰開發

JVM系統架構師之Java虛擬機、OSGi—JVM高級性能架構項目實戰開發 分享網盤下載地址:https://pan.baidu.com/s/1hs3pz1M 密碼: g2wa 本課程由淺入深,全面、系統地介紹了JAVA 虛擬機基礎、應用、管理、性能優化、數據庫的架構,環境搭建實例,編程實例等內容

Java虛擬機器載入

類載入 類載入的時機 類載入宣告週期 類初始何時進行 類載入的過程 載入 驗證 檔案格式驗證 元資料驗證 位元組碼驗證 符號引用驗證 準

【深入理解Java虛擬機器載入器的名稱空間以及的解除安裝

類載入器的名稱空間 每個類載入器又有一個名稱空間,由其以及其父載入器組成 類載入器的名稱空間的作用和影響 每個類載入器又有一個名稱空間,由其以及其父載入器組成 在每個類載入器自己的名稱空間中不能出現相同類名的類 (此處值得是類的全名,包含包名) 在不同的類名稱空間中,可能會出現多個相同的類名的類 如下

是時候瞭解一波虛擬機器載入機制

程式語言發展的大步發展——程式碼編譯的結果,從本地機器碼變為位元組碼 從Java類到JVM執行Class檔案 Java類會被編譯為Class檔案,這裡,編譯的過程先不去具體瞭解,Class檔案中儲存的各種資訊,包括魔數、Class檔案的版本、常量池、訪問標誌、欄位表集合等等重要資訊,都需要被載入到JVM中

阿里P7架構經驗總結——Java架構必備技能之少走彎路系統學習

作為程式猿, 在這樣一個網際網路時代背景下,我們是很幸運的,我們能夠拿著比別的職業更高的工資,坐在高檔寫字樓,在冬暖夏涼辦公環境下,在鍵盤上揮舞著手指就能產出一個個成熟的產品提供給上億(吹牛)的使用者使用,那種成就感和滿足感,是讓我們在朝九晚九的情況下,或者一個個通宵的情況下能夠打雞血似得,只為把改

阿里P7架構淺談Java 的年薪 40W 是什麼水平?

做Java架構師(P7)崗位有三年時間了,期間也從事了很多招聘定級工作,來說說我見解吧。 既然樓主提到年薪40w,那我們看看什麼公司,什麼級別可以給到,再看看要求。 阿里是Java大廠,所以可以參考阿里的標準,阿里一般是16薪水,所以就是稅前2.5w,在阿里應該是P6就可以達到,而對P6的要

阿里P7帶你深入理解Java虛擬機器總結——初始化過程

類的初始化過程 非法向前引用 編譯器手機的順序是由語句在原始檔中出現的順序決定的,靜態語句塊中只能訪問到定義在靜態語句之前的變數,定義它之後的變數,可以賦值,但不能訪問 public class Test{ static{ i=0; system.out.print(

阿里P7架構告訴你Java架構必須知道的 6 大設計原則

開發十年,就只剩下這套架構體系了! >>>   

Java虛擬機器(JVM原始碼):JDK10Java虛擬機器執行時資料區的劃分(詳細圖解)

Java虛擬機器執行時資料區 為什麼要研究這個,因為JDK都已經發布到10了,必須要更新自己對Java虛擬機器新的認識。 一、執行時資料區的劃分 1.1 官方劃分 關於JDK10對執行時資料區的劃分,在官方文件說的非常清楚。 學習技術,一定要學會看第一手資料。 Ja

阿里資深架構構造Java架構學習樹(網際網路分散式架構解析)

1.分散式架構思維 2.架構開發基礎 3.架構核心服務層技術 歡迎加入Java高階架構學習交流群:805685193 免費獲取Dubbo、Redis、設計模式、Netty、zookeeper、Spring cloud、分散式、高併發等架構技術視訊資料,完整架構思維導圖,和B

阿里資深架構構造Java架構學習樹(效能調優+常用框架原始碼+微服務)

效能調優專題 效能優化如何理解 JVM調優 JAVA程式效能優化 Tomcat Mysql 歡迎加入Java高階架構學習交流群:805685193 免費獲取Dubbo、Redis、設計模式、Netty、zookeeper、S

阿里P7架構必修之路(年薪60萬)

阿里巴巴,是多少從事IT事業的程式設計師夢寐以求的地方,能進入這樣大廠的程式設計師可以說都是數一數二的人才。 最近有不少朋友問,成為阿里P7Java架構師需要系統學習哪些Java技術。 下面分享網際網路Java技術體系圖(圖片可以儲存) 一、構成架構師的技能體系  

轉頭條:阿里p7架構:三年經驗應該具備什麼樣的技能?

問:工作中,有時候實現一個功能,會去看有沒有現成的輪子可用。對於重複造輪子與改造輪子有什麼看法? 答:一定會的,其實這也是一個提高技術能力的方法,比如今天想做個日期轉換的功能,JDK8有日期的新特性就會考慮直接使用LocalDate.now().format(DateTimeFormatter.B

阿里P7架構談:MySQL慢查詢優化索引優化以及表等優化總結

MySQL優化概述 MySQL資料庫常見的兩個瓶頸是:CPU和I/O的瓶頸。 CPU在飽和的時候一般發生在資料裝入記憶體或從磁碟上讀取資料時候。 磁碟I/O瓶頸發生在裝入資料遠大於記憶體容量的時候,如果應用分佈在網路上,那麼查詢量相當大的時候那麼平瓶頸就會出現在網路上。

阿里P7架構談職業生涯規劃,給遇到瓶頸,迷茫期的人群一些建議

一、規劃 工作3年了,感覺自己的技術現在到了一個瓶頸,在做一些重複性的業務性的工作,沒有長進,提高太慢; 因此停下腳步對自己的職業生涯做了一個規劃,併為之努力奮鬥: 20-27歲:技術積累階段 在這 5 年時間裡面,你要積累足夠的技術底子,打磨自己的技術實力,成為某一