Java類加載機制詳解

2020-12-25 計算機java編程

很多介紹JVM內存模型的時候都會提到在運行時數據區之前,有個Class Loader,這個就是類加載器。用以把Class文件中的描述信息加載到內存中運行和使用。以下是《深入理解Java虛擬機第二版》對類加載器機制的定義原文:

虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

一般我們把類從加載到內存到卸載出內存的整個過程分為七個階段:加載,驗證,準備,解析,初始化,使用和卸載。其中,驗證、準備和解析統稱為連接。

在這幾個階段中,加載、驗證、準備、初始化和卸載這五個階段的順序是固定的,而解析階段則不一定,它有時候可能會在初始化之後開始,這是為了支持Java的運行時綁定。需要特別注意的是,這裡邊的順序指的是按順序開始,而不是按順序進行或完成,因為這些階段通常會互相交叉的混合進行。

了解類的加載機制非常有必要,下面將逐個解釋說明類加載的全過程(即加載,驗證,準備,解析,初始化五個階段)。相信看完之後,你會對Java類某些問題有更深刻的理解(例如,為什么子類可以覆蓋父類的欄位和方法?餓漢式單例為什麼天生是線程安全的?)

加載

加載過程分為三步:

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

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

3)在內存中生成一個代表這個類的Class對象,作為方法區這個類的各種數據的訪問入口。

上面的第一步獲取二進位字節流,並沒有限定只能從編譯好的.class文件中獲取,也可以是zip包,jar,war,網絡流(Applet),運行時計算生成(如動態代理,通過反射在運行時動態生成代理類),其他文件(如jsp,因jsp最終會編譯成class),資料庫(用的場景較少)。

對於數組類的加載,和普通類的加載有所不同。數組類本身不通過類加載器加載,而是由虛擬機直接完成。但是數組類的元素類型(指數組類去除維度之後的類型,如String[] 數組的元素類型就是 String)是靠類加載器加載的。

加載階段完成之後,虛擬機就會把外部的二進位字節流(不論從何處獲取的)按照一定的數據格式存儲在運行時數據區中的方法區。然後在內存中實例化一個java.lang.Class對象(Class這個對象比較特殊,它存放在方法區中而不是堆中),這個對象將作為程序訪問方法區中的這些數據的外部接口。

驗證

驗證是連接階段的第一步,這一階段的主要目的就是確保Class文件流中的信息符合虛擬機的規範,並且不會危害虛擬機的安全。驗證階段一般分為四個階段:文件格式驗證,元數據驗證,字節碼驗證和符號引用驗證。

1)文件格式驗證

第一階段要驗證二進位字節流是否符合Class文件格式的規範,確保能被虛擬機處理。主要包括以下驗證點:

是否以魔數 0xCAFEBABE 開頭。(每個Class文件的頭4個字節稱為魔數,是一個16進位的固定值,它的作用就是確保這個Class文件能被虛擬機接受)主、次版本號是否在當前虛擬機的處理範圍中(緊接著魔數後面的第5,6位元組代表次版本號,第7,8位元組代表主版本號)。常量池中的常量是否有不被支持的常量類型(依據常量的tag值)。等等,還有其他很多驗證,不再一一說明。這一階段的驗證主要是針對二進位字節流進行的,驗證完成之後,字節流會進入內存中的方法區進行存儲。所以後面的三個驗證階段不再直接操作二進位字節流。

2)元數據驗證

第二階段是對字節碼描述的信息進行語義分析,保證其描述的信息符合Java語言規範。主要包括以下驗證點:

這個類是否有父類(除了Object類,所有類都應該有父類)。這個類是否繼承了不允許被繼承的類(被final修飾的類不可被繼承)。是否實現了其父類或接口要求實現的所有方法。類中的欄位、方法是否與父類產生矛盾(如覆蓋了父類的final欄位,或者重寫、重載不符合規範)。3)字節碼驗證

第三階段主要是對類的方法體進行驗證,確保程序語義是合法的、符合邏輯的。

保證數據的定義和使用相匹配,如定義int類型數據,使用時不能以long型操作。保證跳轉指令不會跳轉到方法體以外的字節碼指令上。保證方法體中的類型轉換是有效的。如可以把子類對象賦值給父類引用,但是父類不可以直接賦值給子類(必須強轉)或其他不相干的類型。4)符號引用驗證

最後一個階段的驗證發生在符號引用轉換為直接引用的時候。實際的轉換動作,發生在後面的解析階段。主要對類自身以外的信息(常量池中的各種符號引用)進行匹配性的校驗。

驗證階段是非常重要但是非必要的一個階段。如果確保代碼對程序運行期沒有影響,則可以通過 -Xverify:node 參數關閉大部分的驗證,以縮短類加載的總時間。

準備

準備階段是類變量分配內存並設置初始值的階段。這裡的類變量指的是被static修飾的變量,而不包括實例變量。類變量被分配到方法區中,而實例變量存放在堆中。

這裡的初始值指的是數據類型的默認值,而不是代碼中所賦的值。例如

publicstaticintvalue = 1 ;

在準備階段之後,value值為0,而不是1。賦值為1的動作發生在初始化階段。

但是,也要特殊情況,如果變量被static 和 final同時修飾,則準備階段直接賦值為指定值。如

public finallystaticintvalue = 1 ;

在準備階段之後,value的值即為1.

各數據類型的初始默認值如下:

解析

解析階段是將常量池中的符號引用轉換為直接引用的過程。那什麼是符號引用和直接引用呢?

符號引用是用一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義的定位到目標即可(前面JVM的模型中,也提到了符號引用,它存在於常量池中,包括類和接口的全限定名、欄位的名稱和描述符、方法的名稱和描述符)。看概念可能比較抽象,可以理解為它就是一個代號,就像你有一個大名,同時也有一個小名,但是不管怎麼叫指代的都是你本人。

直接引用可以是直接指向目標的指針、相對偏移量或是一個能間接定位到目標的句柄。

解析動作主要針對類或接口、欄位、類方法、接口方法、方法屬性、方法句柄、調用點限定符7類符號引用。此處分別介紹一下前四種的解析過程。

1)類或接口的解析

如果類C不是數組類型,那麼虛擬機會把類C直接傳給類加載器。如果類C是數組類型並且元素類型是對象(如String[]),那麼先用類加載器加載元素類型(String類型),再由虛擬機創建代表此數組維度和元素的數組對象。判斷調用類是否有權限訪問被加載類,如果不允許的話,就拋出IllegalAccessError異常。

2)欄位的解析

首先解析欄位所屬的類或接口的符號引用。如果類中有欄位的符號引用(欄位的名稱和描述符)和目標欄位相匹配,則返回這個欄位的直接引用。如果沒有,則自下而上查找其實現的接口和父接口,若匹配到,則返回這個欄位的直接引用。如果還沒有,就自下而上查找其繼承的父類,若匹配到,則返回這個欄位的直接引用。否則,查找失敗,拋出NoSuchFieldError異常。最後如果查找成功的話,會判斷欄位訪問權限,如果該欄位不允許訪問,則拋出 IllegalAccessError異常。

這麼一大段,如果乍看沒明白,下面用代碼解釋一下就懂了。

比如,我去查找類Child中的a欄位,目前來看可以直接查到,就是a=2。如果我把①所包圍的代碼修改為

則表示在本類中找不到a欄位,因此去Child類實現的接口Interface0中查找,於是,成功找到 a=0。再次把①代碼修改為

本類找不到a,則去它的父類查找,於是查找成功,a=1。

那麼聰明的同學可能想到了,如果我修改代碼為既繼承父類又實現接口會怎麼樣呢?

這樣是不行的,編譯器會拒絕編譯。其實,想一下,就能明白,這個時候Child應該取父類中欄位的值還是接口中欄位的值呢,編譯器是不知道的,所以不能編譯。其實,如果是在編譯期,代碼開發工具會給一條這樣的報錯信息:Reference to 'a' is ambiguous, both 'Parent.a' and 'Interface0.a' match.

如果強制執行這段代碼,控制臺則會報錯如下信息:

思考一下,如果,我非要既繼承父類又實現接口,應該怎樣修改代碼才能編譯通過呢?

3)類方法解析

類方法解析第一步同欄位解析一樣,也需要先解析方法所屬的類或接口的符號引用。類方法和接口方法符號引用的常量類型是分開的。如果,在類方法中解析出來的是一個接口,則會拋出 IncompatibleClassChangeError 異常。如果在類中有方法的符號引用(方法的名稱和描述符)和目標方法相匹配,則返回這個方法的直接引用,查找結束。否則,在類的父類中遞歸查找,若找到則返回,查找結束。否則,查找它實現的接口和父接口,如果找到,說明此類是一個抽象類,拋出 AbstractMethodError異常。若都找不到,就拋出NoSuchMethodError 異常。最後,如果查找成功,會判斷此方法是否有訪問權限,若沒有,則拋出 IllegalAccessError異常。

下面通過代碼解釋:

②中,如果當前類Child中有method0方法,則直接返回此方法,列印結果child method0。若把Child中的method0方法注釋掉,則會去找父類Parent的method0,列印結果 parent method0 。最後一點,如果類是實現了接口Interface0,並在接口中找到了method0方法,則說明Child類一定是抽象類。因為,只有抽象類才可以選擇不重寫接口的抽象方法。如果不是抽象類,則需要實現接口的全部方法,此時就可以直接在當前Child類中找到method0方法,而不必去接口中查找方法了。

4)接口方法的解析

首先解析方法所屬的類或接口的符號引用,和類方法解析同理,如果發現解析出來是一個類方法,則會拋出 IncompatibleClassChangeError 異常。如果所屬接口中匹配到目標方法,則返回此方法的直接引用。否則,在父接口中查找,若找到,則返回。否則,查找失敗,拋出 NoSuchMethodError 異常。由於接口的方法都是public的,所以不存在訪問權限的問題。

初始化

這是類加載的最後一步,到這才真正開始執行Java代碼。在準備階段,已經為類變量分配內存,並賦值了默認值。在初始階段,則可以根據需要來賦值了。可以說,初始化階段是執行類構造器 < clinit > 方法的過程。

首先說下類構造器 < clinit > 方法和實例構造器 < init > 方法有什麼區別。< clinit > 方法是在類加載的初始化階段執行,是對靜態變量、靜態代碼塊進行的初始化。而< init > 方法是new一個對象,即調用類的 constructor方法時才會執行,是對非靜態變量進行的初始化。

類構造器方法有如下特點:

保證父類的 < clinit > 方法執行完畢,再執行子類的 < clinit > 方法。由於父類的 < clinit > 方法先執行,所以父類的靜態代碼塊也優於子類執行。如果類中沒有靜態代碼塊,也沒有為變量賦值,則可以不生成 < clinit > 方法。執行接口的 < clinit > 方法時,不需要先執行父接口的 < clinit > 方法。只有父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也不執行接口的 < clinit > 方法。虛擬機會保證在多線程環境下 < clinit > 方法能被正確的加鎖、同步。如果有多個線程同時請求加載一個類,那麼只會有一個線程去執行這個類的 < clinit > 方法,其他線程都會阻塞,直到方法執行完畢。同時,其他線程也不會再去執行 < clinit > 方法了。這就保證了同一個類加載器下,一個類只會初始化一次。(這也是為什麼說餓漢式單例模式是線程安全的,因為類只會加載一次。)類的初始化時機:只有對類主動使用的時候才會觸發初始化,主動使用的場景如下:

使用new關鍵詞創建對象時,訪問某個類的靜態變量或給靜態變量賦值時,調用類的靜態方法時。反射調用時,會觸發類的初始化(如Class.forName())初始化一個類的時候,如其父類未初始化,則會先觸發父類的初始化。虛擬機啟動時,會先初始化主類(即包含main方法的類)。另外,也有些場景並不會觸發類的初始化:

通過子類調用父類的靜態變量,只會觸發父類的初始化,而不會觸發子類的初始化(因為,對於靜態變量,只有直接定義這個變量的類才會初始化)。通過數組來創建對象不會觸發此類的初始化。(如定義一個自定義的Person[] 數組,不會觸發Person類的初始化)通過調用靜態常量(即static final修飾的變量),並不會觸發此類的初始化。因為,在編譯階段,就已經把final修飾的變量放到常量池中了,本質上並沒有直接引用到定義常量的類,因此不會觸發類的初始化。

相關焦點

  • JVM 第三篇:Java 類加載機制
    什麼是類的加載?類的加載指的是將類的 .class 文件中的二進位數據讀入到內存中,將其放在運行時數據區的方法區內,然後在堆區創建一個 java.lang.Class 對象,用來封裝類在方法區內的數據結構。
  • 深入理解Java虛擬機:類加載機制
    虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗,解析和初始化,最終形成可以被虛擬機直接使用的Java類型。一、類的生命周期二、類加載時機必須對類進行"初始化"的情況:使用new關鍵字實例化對象的時候讀取或設置一個類型的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候。
  • 你有真正理解 Java 的類加載機制嗎?|原力計劃
    作者 | 宜春責編 | Elle出品 | CSDN 博客你是否真的理解Java的類加載機制?點進文章的盆友不如先來做一道非常常見的面試題,如果你能做出來,可能你早已掌握並理解了Java的類加載機制,若結果出乎你的意料,那就很有必要來了解了解Java的類加載機制了。
  • java類加載的過程概述
    本文為看雪論壇優秀文章看雪論壇作者ID:上火喝王老吉當程序要使用某個類時,如果該類還未被加載到內存中,則系統會通過加載
  • 乾貨 | 高級Java工程師面試必備jvm類加載機制
    回復「新人」領取關注福利這篇文章不聊別的,專門來侃侃JVM的類加載機制。
  • 【143期】Java 類是如何被加載的?
    我在向朋友解釋的時候是這麼說的:雙親委派模型中,ClassLoader在加載類的時候,會先交由它的父ClassLoader加載,只有當父ClassLoader加載失敗的情況下,才會嘗試自己去加載。這樣可以實現部分類的復用,又可以實現部分類的隔離,因為不同ClassLoader加載的類是互相隔離的。
  • 兩道面試題,帶你解析Java類加載機制
    其實這種面試題考察的就是你對Java類加載機制的理解。如果你對Java加載機制不理解,那麼你是無法解答這道題目的。這篇文章,我將通過對Java類加載機制的講解,讓你掌握解答此類題目的方法。Java類加載機制的七個階段當我們的Java代碼編譯完成後,會生成對應的 class 文件。
  • Java反射機制深入詳解
    一.概念反射就是把Java的各種成分映射成相應的Java類。Class類的構造方法是private,由JVM創建。反射是java語言的一個特性,它允程序在運行時(注意不是編譯的時候)來進行自我檢查並且對內部的成員進行操作。例如它允許一個java的類獲取他所有的成員變量和方法並且顯示出來。
  • Java 的類加載過程
    Java 的類加載過程分為三個主要步驟:加載、連結、初始化,其中連結又分為驗證,準備和解析。
  • Java類隔離加載實現原理是什麼?
    Java類隔離加載實現原理是什麼? JVM 提供一個全局類加載器的設置接口,直接替換全局類加載器,但無法解決多個自定義類加載器同時存在的問題。然而JVM會選擇當前類的類加載器來加載所有該類的引用的類。類隔離技術是什麼?
  • 常見的類加載異常
    上篇文章回顧了一下類加載的委派模型和類加載的三個階段所做的一些事情,本篇文章準備介紹一下 Java 程序運行中常見的類加載異常。
  • 別翻了,這篇文章絕對讓你深刻理解java類的加載以及ClassLoader源碼分析
    點進文章的盆友不如先來做一道非常常見的面試題,如果你能做出來,可能你早已掌握並理解了java的類加載機制,若結果出乎你的意料,那就很有必要來了解了解java的類加載機制了。類的加載問題,如果你對Java加載機制不理解,那麼你可能就錯了上面兩道題目的。
  • Java中加載資料庫驅動的方式有幾種?背後的原理是什麼?
    在運行時,類加載器從CLASSPATH路徑中定位和加載JDBC驅動類。在加載驅動程序類後,需要註冊驅動程序類的一個實例。DriverManager類是驅動程序管理器類,負責管理驅動程序,這個類中所有的方法都是靜態的。
  • 淺析java內存管理機制
    不同的程式語言有不同的內存管理機制,本文在對比C++和Java語言內存管理機制的不同的基礎上,淺析java中的內存分配和內存回收機制,包括java對象初始化及其內存分配,內存回收方法及其注意事項等……1、首先Java原始碼文件(.java後綴)會被Java編譯器編譯為字節碼文件(.class後綴),然後由JVM中的類加載器加載各個類的字節碼文件,加載完畢之後
  • java創建對象的過程詳解(從內存角度分析)
    java對象的創建操作其實我在《JVM系列之類的加載機制》一文曾經提到過,包含兩個過程:類的初始化和實例化。為此為了理解的深入,我們還需要再來看一下類的生命周期。一張圖表示:從上面我們可以看到,對象的創建其實包含了初始化和使用兩個階段。有了這個印象之後,我們就能開始今天的文章了。
  • 你知道java反射機制中class.forName和classloader的區別嗎?
    前兩天頭條有朋友留言說使用class.forName找不到類,可以使用classloader加載。趁此機會總結一下,正好看到面試中還經常問到。一、類加載機制上面兩種加載類的方式說到底還是為了加載一個java類,因此需要先對類加載的過程進行一個簡單的了解。
  • Java 反射機制你還不會?那怎麼看 Spring 源碼?
    ),它假定我們在編譯時已經知道了所有的類型信息;另一種是反射機制,它允許我們在運行時發現和使用類的信息。使用的前提條件:必須先得到代表的字節碼的Class,Class類用於表示.class文件(字節碼)Java的反射(reflection)機制是指在程序的運行狀態中,可以構造任意一個類的對象,可以了解任意一個對象所屬的類,可以了解任意一個類的成員變量和方法,可以調用任意一個對象的屬性和方法。這種動態獲取程序信息以及動態調用對象的功能稱為Java語言的反射機制。
  • 重試機制!java retry(重試) spring retry, guava retrying 詳解
    作者:葉止水https://juejin.im/post/5b6ac0a06fb9a04f8a21b192系列說明java retry 的一步步實現機制。情景導入簡單的需求產品經理:實現一個按條件,查詢用戶信息的服務。小明:好的。沒問題。
  • 你真的了解java類加載器嗎?
    主要保證避免重複加載 + 避免核心類篡改Java類隨著它的類加載器一起具備了一種帶有優先級的層次關係,通過這種層級關可以避免類的重複加載,當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次。
  • 「JAVA」萬字長篇詳述字節碼對象與反射機制完成動態編程
    Java 反射在Java的開發環境中,運行java文件需要使用:java xx.java 命令,運行java命令後,便會啟動JVM,將字節碼文件加載到JVM中,然後開始運行;當運行java命令時,該命令將會啟動一個JVM