Java虛擬機(jī)之類(lèi)加載
一、類(lèi)加載流程
類(lèi)加載的流程可以簡(jiǎn)單分為三步:
- 加載
- 連接
- 初始化
而其中的連接又可以細(xì)分為三步:
- 驗(yàn)證
- 準(zhǔn)備
- 解析
下面會(huì)分別對(duì)各個(gè)流程進(jìn)行介紹。
1.1 類(lèi)加載條件
在了解類(lèi)接在流程之前,先來(lái)看一下觸發(fā)類(lèi)加載的條件。
JVM
不會(huì)無(wú)條件加載類(lèi),只有在一個(gè)類(lèi)或接口在初次使用的時(shí)候,必須進(jìn)行初始化。這里的使用是指主動(dòng)使用,主動(dòng)使用包括如下情況:
- 創(chuàng)建一個(gè)類(lèi)的實(shí)例的時(shí)候:比如使用
new
創(chuàng)建,或者使用反射、克隆、反序列化 - 調(diào)用類(lèi)的靜態(tài)方法的時(shí)候:比如使用
invokestatic
指令 - 使用類(lèi)或接口的靜態(tài)字段:比如使用
getstatic
/putstatic
指令 - 使用
java.lang.reflect
中的反射類(lèi)方法時(shí) - 初始化子類(lèi)時(shí),要求先初始化父類(lèi)
- 含有
main()
方法的類(lèi)
除了以上情況外,其他情況屬于被動(dòng)使用,不會(huì)引起類(lèi)的初始化。
比如下面的例子:
public class Main { public static void main(String[] args){ System.out.println(Child.v); } } class Parent{ static{ System.out.println("Parent init"); } public static int v = 100; } class Child extends Parent{ static { System.out.println("Child init"); } }
輸出如下:
Parent init
100
而加上類(lèi)加載參數(shù)-XX:+TraceClassLoading
后,可以看到Child
確實(shí)被加載了:
[0.068s][info ][class,load] com.company.Main [0.069s][info ][class,load] com.company.Parent [0.069s][info ][class,load] com.company.Child Parent init 100
但是并沒(méi)有進(jìn)行初始化。另外一個(gè)例子是關(guān)于final
的,代碼如下:
public class Main { public static void main(String[] args){ System.out.println(Test.STR); } } class Test{ static{ System.out.println("Test init"); } public static final String STR = "Hello"; }
輸出如下:
[0.066s][info ][class,load] com.company.Main
Hello
Test
類(lèi)根本沒(méi)有被加載,因?yàn)?code>final被做了優(yōu)化,編譯后的Main.class
中,并沒(méi)有引用Test
類(lèi):
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #4 // String Hello 5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
在字節(jié)碼偏移3的位置,通過(guò)ldc
將常量池第4項(xiàng)入棧,此時(shí)在字節(jié)碼文件中常量池第4項(xiàng)為:
#3 = Class #24 // com/company/Test #4 = String #25 // Hello #5 = Methodref #26.#27 // java/io/PrintStream.println:(Ljava/lang/String;)V
因此并沒(méi)有對(duì)Test
類(lèi)進(jìn)行加載,只是直接引用常量池中的常量,因此輸出沒(méi)有Test
的加載日志。
1.2 加載
類(lèi)加載的時(shí)候,JVM
必須完成以下操作:
- 通過(guò)類(lèi)的全名獲取二進(jìn)制數(shù)據(jù)流
- 解析類(lèi)的二進(jìn)制數(shù)據(jù)流為方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)
- 創(chuàng)建
java.lang.Class
類(lèi)的實(shí)例,表示該類(lèi)型
第一步獲取二進(jìn)制數(shù)據(jù)流,途徑有很多,包括:
- 字節(jié)碼文件
JAR
/ZIP
壓縮包- 從網(wǎng)絡(luò)加載
等等,獲取到二進(jìn)制數(shù)據(jù)流后,JVM
進(jìn)行處理并轉(zhuǎn)化為一個(gè)java.lang.Class
實(shí)例。
1.3 驗(yàn)證
驗(yàn)證的操作是確保加載的字節(jié)碼是合法、合理并且規(guī)范的。步驟簡(jiǎn)略如下:
- 格式檢查:判斷二進(jìn)制數(shù)據(jù)是否符合格式要求和規(guī)范,比如是否以魔數(shù)開(kāi)頭,主版本號(hào)和小版本號(hào)是否在當(dāng)前
JVM
支持范圍內(nèi)等等 - 語(yǔ)義檢查:比如是否所有類(lèi)都有父類(lèi)存在,一些被定義為
final
的方法或類(lèi)是否被重載了或者繼承了,是否存在不兼容方法等等 - 字節(jié)碼驗(yàn)證:會(huì)試圖通過(guò)對(duì)字節(jié)碼流的分析,判斷字節(jié)碼是否可以正確被執(zhí)行,比如是否會(huì)跳轉(zhuǎn)到一條不存在的指令,函數(shù)調(diào)用是否傳遞了正確的參數(shù)等等,但是卻無(wú)法100%判斷一段字節(jié)碼是否可以被安全執(zhí)行,只是盡可能檢查出可以預(yù)知的明顯問(wèn)題。如果無(wú)法通過(guò)檢查,則不會(huì)加載這個(gè)類(lèi),如果通過(guò)了檢查,也不能說(shuō)明這個(gè)類(lèi)完全沒(méi)有問(wèn)題
- 符號(hào)引用驗(yàn)證:檢查類(lèi)或方法是否確實(shí)存在,并且確定當(dāng)前類(lèi)有沒(méi)有權(quán)限訪問(wèn)這些數(shù)據(jù),比如無(wú)法找到一個(gè)類(lèi)就拋出
NoClassDefFoundError
,無(wú)法找到方法就拋出NoSuchMethodError
1.4 準(zhǔn)備
類(lèi)通過(guò)驗(yàn)證后,就會(huì)進(jìn)入準(zhǔn)備階段,在這個(gè)階段,JVM
為會(huì)類(lèi)分配相應(yīng)的內(nèi)存空間,并設(shè)置初始值,比如:
int
初始化為0
long
初始化為0L
double
初始化為0f
- 引用初始化為
null
如果存在常量字段,那么這個(gè)階段也會(huì)為常量賦值。
1.5 解析
解析就是將類(lèi)、接口、字段和方法的符號(hào)引用轉(zhuǎn)為直接引用。符號(hào)引用就是一些字面量引用,和JVM
的內(nèi)存數(shù)據(jù)結(jié)構(gòu)和內(nèi)存布局無(wú)關(guān),由于在字節(jié)碼文件中,通過(guò)常量池進(jìn)行了大量的符號(hào)引用,這個(gè)階段就是將這些引用轉(zhuǎn)為直接引用,得到類(lèi)、字段、方法在內(nèi)存中的指針或直接偏移量。
另外,由于字符串有著很重要的作用,JVM
對(duì)String
進(jìn)行了特別的處理,直接使用字符串常量時(shí),就會(huì)在類(lèi)中出現(xiàn)CONSTANT_String
,并且會(huì)引用一個(gè)CONSTANT_UTF8
常量項(xiàng)。JVM
運(yùn)行時(shí),內(nèi)部的常量池中會(huì)維護(hù)一張字符串拘留表(intern
),會(huì)保存其中出現(xiàn)過(guò)的所有字符串常量,并且沒(méi)有重復(fù)項(xiàng)。使用String.intern()
可以獲得一個(gè)字符串在拘留表的引用,比如下面代碼:
public static void main(String[] args){ String a = 1 + String.valueOf(2) + 3; String b = "123"; System.out.println(a.equals(b)); System.out.println(a == b); System.out.println(a.intern() == b); }
輸出:
true
false
true
這里b
就是常量本身,因此a.intern()
返回在拘留表的引用后就是b
本身,比較結(jié)果為真。
1.6 初始化
初始化階段會(huì)執(zhí)行類(lèi)的初始化方法<clint>
,<clint>
是由編譯期生成的,由靜態(tài)成員的賦值語(yǔ)句以及static
語(yǔ)句共同產(chǎn)生。
另外,加載一個(gè)類(lèi)的時(shí)候,JVM
總是會(huì)試圖加載該類(lèi)的父類(lèi),因此父類(lèi)的<clint>
方法總是在子類(lèi)的<clint>
方法之前被調(diào)用。另一方面,需要注意的是<clint>
會(huì)確保在多線程環(huán)境下的安全性,也就是多個(gè)線程同時(shí)初始化同一個(gè)類(lèi)時(shí),只有一個(gè)線程可以進(jìn)入<clint>
方法,換句話說(shuō),在多線程下可能會(huì)出現(xiàn)死鎖,比如下面代碼:
package com.company; import java.util.concurrent.TimeUnit; public class Main extends Thread{ private char flag; public Main(char flag){ this.flag = flag; } public static void main(String[] args){ Main a = new Main('A'); a.start(); Main b = new Main('B'); b.start(); } @Override public void run() { try{ Class.forName("com.company.Static"+flag); }catch (ClassNotFoundException e){ e.printStackTrace(); } } } class StaticA{ static { try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); } try{ Class.forName("com.company.StaticB"); }catch (ClassNotFoundException e){ e.printStackTrace(); } System.out.println("StaticA init ok"); } } class StaticB{ static { try { TimeUnit.SECONDS.sleep(1); }catch (InterruptedException e){ e.printStackTrace(); } try{ Class.forName("com.company.StaticA"); }catch (ClassNotFoundException e){ e.printStackTrace(); } System.out.println("StaticB init ok"); } }
在加載StaticA
的時(shí)候嘗試加載StaticB
,但是由于StaticB
已經(jīng)被加載中,因此加載StaticA
的線程會(huì)阻塞在Class.forName("com.company.StaticB")
處,同理加載StaticB
的線程會(huì)阻塞在Class.forName("com.company.StaticA")
處,這樣就出現(xiàn)死鎖了。
二、ClassLoader
2.1 ClassLoader簡(jiǎn)介
ClassLoader
是類(lèi)加載的核心組件,所有的Class
都是由ClassLoader
加載的,ClassLoader
通過(guò)各種各樣的方式將Class
信息的二進(jìn)制數(shù)據(jù)流讀入系統(tǒng),然后交給JVM
進(jìn)行連接、初始化等操作。因此ClassLoader
負(fù)責(zé)類(lèi)的加載流程,無(wú)法通過(guò)ClassLoader
改變類(lèi)的連接和初始化行為。
ClassLoader
是一個(gè)抽象類(lèi),提供了一些重要接口定義加載流程和加載方式,主要方法如下:
public Class<?> loadClass(String name) throws ClassNotFoundException
:給定一個(gè)類(lèi)名,加載一個(gè)類(lèi),返回這個(gè)類(lèi)的Class
實(shí)例,找不到拋出異常
protected final Class<?> defineClass(byte[] b, int off, int len)
:根據(jù)給定字節(jié)流定義一個(gè)類(lèi),off
和len
表示在字節(jié)數(shù)組中的偏移和長(zhǎng)度,這是一個(gè)protected
方法,在自定義子類(lèi)中才能使用
protected Class<?> findClass(String name) throws ClassNotFoundException
:查找一個(gè)類(lèi),會(huì)在loadClass
中被調(diào)用,用于自定義查找類(lèi)的邏輯
protected Class<?> findLoadedClass(String name)
:尋找一個(gè)已經(jīng)加載的類(lèi)
2.2 類(lèi)加載器分類(lèi)
在標(biāo)準(zhǔn)的Java
程序中,JVM
會(huì)創(chuàng)建3類(lèi)加載器為整個(gè)應(yīng)用程序服務(wù),分別是:
- 啟動(dòng)類(lèi)加載器:
Bootstrap ClassLoader
- 擴(kuò)展類(lèi)加載器:
Extension ClassLoader
- 應(yīng)用類(lèi)加載器(也叫系統(tǒng)類(lèi)加載器):
App ClassLoader
另外,在程序中還可以定義自己的類(lèi)加載器,從總體看,層次結(jié)構(gòu)如下:
一般來(lái)說(shuō)各個(gè)加載器負(fù)責(zé)的范圍如下:
- 啟動(dòng)類(lèi)加載器:負(fù)責(zé)加載系統(tǒng)的核心類(lèi),比如
rt.jar
包中的類(lèi) - 擴(kuò)展類(lèi)加載器:負(fù)責(zé)加載
lib/ext/*.jar
下的類(lèi) - 應(yīng)用類(lèi)加載器:負(fù)責(zé)加載用戶程序的類(lèi)
- 自定義加載器:加載一些特殊途徑的類(lèi),一般是用戶程序的類(lèi)
2.3 雙親委派
默認(rèn)情況下,類(lèi)加載使用雙親委派加載的模式,具體來(lái)說(shuō),就是類(lèi)在加載的時(shí)候,會(huì)判斷當(dāng)前類(lèi)是否已經(jīng)被加載,如果已經(jīng)被加載,那么直接返回已加載的類(lèi),如果沒(méi)有,會(huì)先請(qǐng)求雙親加載,雙親也是按照一樣的流程先判斷是否已加載,如果沒(méi)有在此委托雙親加載,如果雙親加載失敗,則會(huì)自己加載。
在上圖中,應(yīng)用類(lèi)加載器的雙親為擴(kuò)展類(lèi)加載器,擴(kuò)展類(lèi)加載器的雙親為啟動(dòng)類(lèi)加載器,當(dāng)系統(tǒng)需要加載一個(gè)類(lèi)的時(shí)候,會(huì)先從底層類(lèi)加載器開(kāi)始進(jìn)行判斷,當(dāng)需要加載的時(shí)候會(huì)從頂層開(kāi)始加載,依次向下嘗試直到加載成功。
在所有加載器中,啟動(dòng)類(lèi)加載器是最特別的,并不是使用Java
語(yǔ)言實(shí)現(xiàn),在Java
中沒(méi)有對(duì)象與之相對(duì)應(yīng),系統(tǒng)核心類(lèi)就是由啟動(dòng)類(lèi)加載器進(jìn)行加載的。換句話說(shuō),如果嘗試在程序中獲取啟動(dòng)類(lèi)加載器,得到的值是null
:
System.out.println(String.class.getClassLoader() == null);
輸出結(jié)果為真。
到此這篇關(guān)于Java虛擬機(jī)之類(lèi)加載的文章就介紹到這了,更多相關(guān)JVM類(lèi)加載內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java理論基礎(chǔ)Stream性能論證測(cè)試示例
這篇文章主要為大家介紹了java理論基礎(chǔ)Stream性能論證的測(cè)試示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2022-03-03springboot下mybatis-plus開(kāi)啟打印sql日志的配置指南
這篇文章主要給大家介紹了關(guān)于springboot下mybatis-plus開(kāi)啟打印sql日志的配置指南的相關(guān)資料,還介紹了關(guān)閉打印的方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03淺析Spring容器原始Bean是如何創(chuàng)建的
這篇文章主要是想和小伙伴們一起聊聊?Spring?容器創(chuàng)建?Bean?最最核心的?createBeanInstance?方法,文中的示例代碼講解詳細(xì),需要的可以參考一下2023-08-08Java在并發(fā)環(huán)境中SimpleDateFormat多種解決方案
這篇文章主要介紹了Java在并發(fā)環(huán)境中SimpleDateFormat多種解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07SpringBoot項(xiàng)目整合jasypt實(shí)現(xiàn)過(guò)程詳解
這篇文章主要介紹了SpringBoot項(xiàng)目整合jasypt實(shí)現(xiàn)過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-08-08Java中DecimalFormat用法及符號(hào)含義
DecimalFormat是NumberFormat的一個(gè)具體子類(lèi),用于格式化十進(jìn)制數(shù)字。這篇文章介紹了DecimalFormat的用法及符號(hào)含義,需要的朋友可以收藏下,方便下次瀏覽觀看2021-12-12詳解Kotlin中如何實(shí)現(xiàn)類(lèi)似Java或C#中的靜態(tài)方法
Kotlin中如何實(shí)現(xiàn)類(lèi)似Java或C#中的靜態(tài)方法,本文總結(jié)了幾種方法,分別是:包級(jí)函數(shù)、伴生對(duì)象、擴(kuò)展函數(shù)和對(duì)象聲明。這需要大家根據(jù)不同的情況進(jìn)行選擇。2017-05-05