面試阿里字節(jié)99%會被問到Java類加載機制和類加載器

1. 類加載機制
所謂類加載機制就是JVM虛擬機把Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗,轉(zhuǎn)換解析和初始化,形成虛擬機可以直接使用的Jav類型,即Java.lang.Class。
2. 類加載的過程
類加載的過程主要有裝載(Load)、鏈接(Link)、初始化(Initialize)
2.1 裝載(Load)
類的加載指的是將類的.class文件中的二進制數(shù)據(jù)讀入到內(nèi)存中,將其放在運行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個java.lang.Class對象,用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。類的加載的最終產(chǎn)品是位于堆區(qū)中的Class對象,Class對象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向Java程序員提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口。
類加載器并不需要等到某個類被“首次主動使用”時再加載它,JVM規(guī)范允許類加載器在預(yù)料某個類將要被使用時就預(yù)先加載它,如果在預(yù)先加載的過程中遇到了.class文件缺失或存在錯誤,類加載器必須在程序首次主動使用該類時才報告錯誤(LinkageError錯誤)如果這個類一直沒有被程序主動使用,那么類加載器就不會報告錯誤。
加載.class文件的方式
- 從本地系統(tǒng)中直接加載
- 通過網(wǎng)絡(luò)下載.class文件
- 從zip,jar等歸檔文件中加載.class文件
- 從專有數(shù)據(jù)庫中提取.class文件
- 將Java源文件動態(tài)編譯為.class文件
2.2 鏈接(Link)
鏈接這一過程又可以分為驗證(Validate)、準(zhǔn)備(Prepare)、解析(Resolve)三個階段
驗證(Validate)
保證被加載類的正確性。其主要包括四種驗證,文件格式驗證,元數(shù)據(jù)驗證,字節(jié)碼驗證,符號引用驗證。
準(zhǔn)備(Prepare)
為類的靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中分配。對于該階段有以下幾點需要注意
這時候進行內(nèi)存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨著對象一塊分配在Java堆中。
這里所設(shè)置的初始值通常情況下是數(shù)據(jù)類型默認(rèn)的零值(如0、0L、null、false等),而不是被在Java代碼中被顯式地賦予的值。
假設(shè)一個類變量的定義為:public static int value = 3;
那么變量value在準(zhǔn)備階段過后的初始值為0,而不是3,因為這時候尚未開始執(zhí)行任何Java方法,而把value賦值為3的putstatic指令是在程序編譯后,存放于類構(gòu)造器()方法之中的,所以把value賦值為3的動作將在初始化階段才會執(zhí)行。
這里還需要注意以下幾點
對基本數(shù)據(jù)類型來說,對于類變量(static)和全局變量,如果不顯式地對其賦值而直接使用,則系統(tǒng)會為其賦予默認(rèn)的零值,而對于局部變量來說,在使用前必須顯式地為其賦值,否則編譯時不通過。
對于同時被static和final修飾的常量,必須在聲明的時候就為其顯式地賦值,否則編譯時不通過;而只被final修飾的常量則既可以在聲明時顯式地為其賦值,也可以在類初始化時顯式地為其賦值,總之,在使用前必須為其顯式地賦值,系統(tǒng)不會為其賦予默認(rèn)零值。
對于引用數(shù)據(jù)類型reference來說,如數(shù)組引用、對象引用等,如果沒有對其進行顯式地賦值而直接使用,系統(tǒng)都會為其賦予默認(rèn)的零值,即null。
如果在數(shù)組初始化時沒有對數(shù)組中的各元素賦值,那么其中的元素將根據(jù)對應(yīng)的數(shù)據(jù)類型而被賦予默認(rèn)的零值
如果類字段的字段屬性表中存在ConstantValue屬性,即同時被final和static修飾,那么在準(zhǔn)備階段變量value就會被初始化為ConstValue屬性所指定的值。假設(shè)上面的類變量value被定義為: public static final int value = 3;,編譯時Javac將會為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機就會根據(jù)ConstantValue的設(shè)置將value賦值為3。我們可以理解為static final常量在編譯期就將其結(jié)果放入了調(diào)用它的類的常量池中
解析(Resolve)
解析階段是虛擬機將常量池內(nèi)的符號引用替換為直接引用的過程。符號引用就是一組符號來描述目標(biāo),可以是任何字面量。直接引用就是直接指向目標(biāo)的指針、相對偏移量或一個間接定位到目標(biāo)的句柄。
2.3 初始化
對類的靜態(tài)變量,靜態(tài)代碼塊執(zhí)行初始化操作。準(zhǔn)備階段和初始化階段看似有點矛盾,其實是不矛盾的,如果類中有語句:private static int a = 10,它的執(zhí)行過程是這樣的,首先字節(jié)碼文件被加載到內(nèi)存后,先進行鏈接的驗證這一步驟,驗證通過后準(zhǔn)備階段,給a分配內(nèi)存,因為變量a是static的,所以此時a等于int類型的默認(rèn)初始值0,即a=0,然后到解析,到初始化這一步驟時,才把a的真正的值10賦給a,此時a=10。
JVM負(fù)責(zé)對類進行初始化,主要對類變量進行初始化。在Java中對類變量進行初始值設(shè)定有兩種方式:
聲明類變量是指定初始值,也就是直接給類別量一個值
使用靜態(tài)代碼塊為類變量指定初始值
初始化,主要是執(zhí)行類的類構(gòu)造器< clinit>()方法,JVM會將類中的靜態(tài)代碼塊和靜態(tài)變量的賦值語句放在該方法里面。
JVM初始化步驟
1、假如這個類還沒有被加載和鏈接,則程序先加載并鏈接該類
2、假如該類的直接父類還沒有被初始化,則先初始化其直接父類
3、假如類中有初始化語句,則系統(tǒng)依次執(zhí)行這些初始化語句
類初始化時機:只有當(dāng)對類的主動使用的時候才會導(dǎo)致類的初始化,類的主動使用包括以下六種:
– 創(chuàng)建類的實例,也就是new的方式
– 訪問某個類或接口的靜態(tài)變量,或者對該靜態(tài)變量賦值
– 調(diào)用類的靜態(tài)方法
– 反射(如Class.forName(“com.shengsiyuan.Test”))
– 初始化某個類的子類,則其父類也會被初始化
– Java虛擬機啟動時被標(biāo)明為啟動類的類(Java Test),直接使用java.exe命令來運行某個主類
3. clinit方法
類初始化方法clinit:JVM通過Classload進行類型加載時,如果在加載時需要進行類的初始化操作時,則會調(diào)用類型、的初始化方法。 clinit方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊中只能訪問到定義在靜態(tài)語句塊之前的變量,定義在它之后的變量,在前面的靜態(tài)語句中可以賦值,但是不能訪問。
clinit方法對于類或接口來說并不是必須的,如果一個類中沒有靜態(tài)語句塊,也沒有對類變量的賦值操作,那么編譯器可以不為這個類生成clinit方法。
接口中不能使用靜態(tài)語句塊,但仍然有類變量(final static)初始化的賦值操作,因此接口與類一樣會生成clinit方法。但是接口魚類不同的是:執(zhí)行接口的clinit方法不需要先執(zhí)行父接口的clinit方法,只有當(dāng)父接口中定義的變量被使用時,父接口才會被初始化。另外,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的clinit方法。
虛擬機會保證一個類的clinit方法在多線程環(huán)境中被正確地加鎖和同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執(zhí)行這個類的clinit方法,其他線程都需要阻塞等待,直到活動線程執(zhí)行clinit方法完畢。如果在一個類的clinit方法中有耗時很長的操作,那就可能造成多個線程阻塞,在實際應(yīng)用中這種阻塞往往是很隱蔽的。
說到 clinit方法,就不得不說一下對象實例化方法init。
對象實例化方法init:Java對象在被創(chuàng)建時,會進行實例化操作,給成員變量賦值。該部分操作封裝在init方法中,并且子類的init方法中會首先對父類init方法的調(diào)用。
clinit 方法和init 方法的區(qū)別
init和clinit方法執(zhí)行時機不同
init是對象構(gòu)造器方法,也就是說在程序執(zhí)行new 一個對象調(diào)用該對象類的 constructor 方法時才會執(zhí)行init方法,而clinit是類構(gòu)造器方法,也就是在jvm進行類加載—–驗證—-解析—–初始化,中的初始化階段jvm會調(diào)用clinit方法。
init和clinit方法執(zhí)行目的不同
init是instance實例構(gòu)造器,對非靜態(tài)變量解析初始化,而clinit是class類構(gòu)造器對靜態(tài)變量,靜態(tài)代碼塊進行初始化
clinit 和init方法的數(shù)量不同
編譯器最多只為一個類生成一個clinit方法,如果類中沒有靜態(tài)成員或者代碼塊的話,就不有clint方法。而init方法,類中一個構(gòu)造函數(shù)就對應(yīng)一個init方法
4. 類加載器
類加載器負(fù)責(zé)加載所有的類,其為所有被載入內(nèi)存中的類生成一個java.lang.Class實例對象。一旦一個類被加載如JVM中,同一個類就不會被再次載入了。正如一個對象有一個唯一的標(biāo)識一樣,一個載入JVM的類也有一個唯一的標(biāo)識。在Java中,一個類用其全限定類名(包括包名和類名)作為標(biāo)識;但在JVM中,一個類用其全限定類名和其類加載器作為其唯一標(biāo)識。例如,如果在pg的包中有一個名為Person的類,被類加載器ClassLoader的實例kl負(fù)責(zé)加載,則該Person類對應(yīng)的Class對象在JVM中表示為(Person.pg.kl)。這意味著兩個類加載器加載的同名類:(Person.pg.kl)和(Person.pg.kl2)是不同的、它們所加載的類也是完全不同、互不兼容的。
JVM預(yù)定義有三種類加載器,當(dāng)一個 JVM啟動的時候,Java開始使用如下三種類加載器:
啟動類加載器(Bootstrap ClassLoader):負(fù)責(zé)加載存放在JDKjrelib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath參數(shù)指定的路徑中的,并且能被虛擬機識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader加載)。啟動類加載器是由C++實現(xiàn)的,沒有對應(yīng)的Java對象,因此在Java中只能用null代替。
擴展類加載器(Extension ClassLoader):負(fù)責(zé)加載java平臺中擴展功能的一些jar包,包括JDK/jre/lib/*.jar 或 -Djava.ext.dirs指定目錄下的jar包。,開發(fā)者可以直接使用擴展類加載器。
應(yīng)用程序類加載器(Application ClassLoader):負(fù)責(zé)加載用戶類路徑(ClassPath)所指定的類,開發(fā)者可以直接使用該類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。
自定義類加載器 Custom ClassLoader: 通過繼承java.lang.ClassLoader根據(jù)自身需要自定義ClassLoader,如tomcat、jboss都會根據(jù)j2ee規(guī)范自行實現(xiàn)ClassLoader。
5. 雙親委派模型
幾種類加載器的層次關(guān)系如下圖所示
這種層次關(guān)系稱為類加載器的雙親委派模型。我們把每一層上面的類加載器叫做當(dāng)前層類加載器的父加載器,當(dāng)然,它們之間的父子關(guān)系并不是通過繼承關(guān)系來實現(xiàn)的,而是使用組合關(guān)系來復(fù)用父加載器中的代碼。該模型在JDK1.2期間被引入并廣泛應(yīng)用于之后幾乎所有的Java程序中,但它并不是一個強制性的約束模型,而是Java設(shè)計者們推薦給開發(fā)者的一種類的加載器實現(xiàn)方式。
雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成,依次向上,因此,所有的類加載請求最終都應(yīng)該被傳遞到頂層的啟動類加載器中,只有當(dāng)父加載器在它的搜索范圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類。
雙親委派機制的優(yōu)勢:采用雙親委派模式的是好處是Java類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系,通過這種層級關(guān)可以避免類的重復(fù)加載,當(dāng)父親已經(jīng)加載了該類時,就沒有必要子ClassLoader再加載一次。其次是考慮到安全因素,java核心api中定義類型不會被隨意替換,假設(shè)通過網(wǎng)絡(luò)傳遞一個名為java.lang.Integer的類,通過雙親委托模式傳遞到啟動類加載器,而啟動類加載器在核心Java API發(fā)現(xiàn)這個名字的類,發(fā)現(xiàn)該類已被加載,并不會重新加載網(wǎng)絡(luò)傳遞的過來的java.lang.Integer,而直接返回已加載過的Integer.class,這樣便可以防止核心API庫被隨意篡改。
到此這篇關(guān)于面試阿里字節(jié)99%會被問到Java類加載機制和類加載器的文章就介紹到這了,更多相關(guān)Java類加載機制和類加載器內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持腳本之家!
相關(guān)文章
- 這篇文章主要介紹了程序員面試的幾個小技巧,在平時面試的時候,除了實打?qū)嵉募寄苓€需要更多的技巧,雙管齊下才能贏得更大的勝算,技能方面就不多說了,下面來分享幾個面試2023-04-23
- 面試中,問鎖主要是兩方面:鎖的日常使用場景 + 鎖原理,鎖的日常使用場景主要考察對鎖 API 的使用熟練度,看看你是否真的使用過這些 API,而不是紙上談兵,鎖原理主要就是2022-05-19
- 這篇文章主要介紹了Mybatis常見面試題詳細總結(jié),通過總結(jié)列舉大量的mybatis面試常見題目供給大家參考,希望對大家有所幫助2021-08-24
2020Java后端開發(fā)面試題總結(jié)(春招+秋招+社招)
這篇文章主要介紹了2020Java后端開發(fā)面試題總結(jié)(春招+秋招+社招),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2021-02-18- 這篇文章主要介紹了MySQL數(shù)據(jù)庫選擇題小結(jié),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2021-02-07
- 這篇文章主要介紹了30道有趣的JVM面試題(小結(jié)),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2020-11-26
- 這篇文章主要介紹了Python面試題爬蟲篇小結(jié)(附答案),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2020-10-28
- 這篇文章主要介紹了還不理解B樹和B+樹,那就看看這篇文章吧,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一2020-09-10
Java面試通關(guān)要點匯總(備戰(zhàn)秋招)
這篇文章主要介紹了Java面試通關(guān)要點匯總(備戰(zhàn)秋招),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2020-09-08- 這篇文章主要介紹了10道JVM常見面試題解析(附答案),文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)2020-09-04