Java中什么是類加載?類加載的過程?
類加載指的是把類加載到 JVM 中。把二進(jìn)制流存儲到內(nèi)存中,之后經(jīng)過一番解析、處理轉(zhuǎn)化成可用的 class 類。
二進(jìn)制流可以來源于 class 文件,或通過字節(jié)碼工具生成的字節(jié)碼或來自于網(wǎng)絡(luò)。只要符合格式的二進(jìn)制流,JVM 來者不拒。
虛擬機(jī)遇到?條 new 指令時(shí),?先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到這個(gè)類的符號引?,并且檢查這個(gè)符號引?代表的類是否已被加載過、解析和初始化過。如果沒有,那必須先執(zhí)?相應(yīng)的類加載過程。類加載過程包括了加載、連接、初始化三個(gè)階段,其中連接還可以分為驗(yàn)證、準(zhǔn)備、解析。
圖片
加載
將二進(jìn)制流讀入內(nèi)存中,生成一個(gè) Class 對象。
在加載階段,虛擬機(jī)需要完成以下三件事情:
- 通過一個(gè)類的全限定名來獲取其定義的二進(jìn)制字節(jié)流。
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
- 在Java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對象,作為對方法區(qū)中這些數(shù)據(jù)的訪問入口。
圖片
這個(gè)階段既可以使用系統(tǒng)提供的類加載器來完成加載,也可以自定義自己的類加載器來完成加載。
驗(yàn)證
確保Class文件的字節(jié)流中包含的信息符合JVM規(guī)范,保證在運(yùn)行后不會危害虛擬機(jī)自身的安全。即安全性檢查,主要包括四種驗(yàn)證:
- 文件格式驗(yàn)證: 驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范;例如: 是否以0xCAFEBABE開頭、主次版本號是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型。
- 元數(shù)據(jù)驗(yàn)證:: 對字節(jié)碼描述的信息進(jìn)行語義分析(注意: 對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規(guī)范的要求;例如: 這個(gè)類是否有父類,除了java.lang.Object之外。
- 字節(jié)碼驗(yàn)證:通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。
- 符號引用驗(yàn)證:確保解析動(dòng)作能正確執(zhí)行
驗(yàn)證階段是非常重要的,但不是必須的,它對程序運(yùn)行期沒有影響,如果所引用的類經(jīng)過反復(fù)驗(yàn)證,那么可以考慮采用-Xverifynone參數(shù)來關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載的時(shí)間。
準(zhǔn)備
準(zhǔn)備階段是正式為static 變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中分配。
static變量在分配空間和賦值是在兩個(gè)階段完成的。分配空間在準(zhǔn)備階段完成,賦值在初始化階段完成。也就是說這里給類變量設(shè)置初始值,設(shè)置的是數(shù)據(jù)類型默認(rèn)的零值(如0、0L、null、false等)。
- 如果 static 變量是 ?nal 的基本類型,以及字符串常量,那么編譯階段值就確定了,賦值在準(zhǔn)備階段完成。
- 如果 static 變量是 ?nal 的,但屬于引用類型,那么賦值會在初始化階段完成。
解析
將常量池內(nèi)的符號引用替換為直接引用的過程。符號引用用于描述目標(biāo),直接引用直接指向目標(biāo)的地址。
- 未解析時(shí),常量池中的看到的對象僅是符號,未真正的存在于內(nèi)存中。
- 解析以后,會將常量池中的符號引用解析為直接引用。
初始化
初始化階段會執(zhí)行cinit方法來為 類變量static變量 賦上定義的值并執(zhí)行類中的靜態(tài)代碼塊;這里的賦值才是代碼里面的賦值,準(zhǔn)備階段只是設(shè)置初始值占個(gè)坑。
在Java中對類變量進(jìn)行初始值設(shè)定有兩種方式:
- 聲明類變量是指定初始值
- 使用靜態(tài)代碼塊為類變量指定初始值
何時(shí)進(jìn)行類加載?
- 定義了main的類,啟動(dòng)main方法時(shí)該類會被加載
- 創(chuàng)建類的實(shí)例,即new對象的時(shí)候
- 訪問類的靜態(tài)方法
- 訪問類的靜態(tài)變量
- 反射 Class.forName()
JVM初始化步驟?
- 假如這個(gè)類還沒有被加載和連接,則程序先加載并連接該類
- 假如該類的直接父類還沒有被初始化,則先初始化其直接父類
- 假如類中有初始化語句,則系統(tǒng)依次執(zhí)行這些初始化語句
初始化發(fā)生的時(shí)機(jī)?
概括得說,類初始化是【懶惰的】,只有當(dāng)對類的主動(dòng)使用的時(shí)候才會導(dǎo)致類的初始化。
- main 方法所在的類,總會被首先初始化
- 首次訪問這個(gè)類的靜態(tài)變量或靜態(tài)方法時(shí)
- 子類初始化,如果父類還沒初始化,會引發(fā)父類初始化
- 子類訪問父類的靜態(tài)變量,只會觸發(fā)父類的初始化
- Class.forName new 會導(dǎo)致初始化
不會導(dǎo)致類初始化的情況?
- 訪問類的 static final 靜態(tài)常量(基本類型和字符串)不會觸發(fā)初始化。
- 類對象.class 不會觸發(fā)初始化
- 創(chuàng)建該類的數(shù)組不會觸發(fā)初始化
- 類加載器的 loadClass 方法
- Class.forName 的參數(shù) 2 為 false 時(shí)
cinit方法如果執(zhí)行失敗了怎么辦,這個(gè)類還能用嗎?
- 在Java類加載的過程中,cinit 方法實(shí)際上指的是類的靜態(tài)初始化方法,也就是類的靜態(tài)代碼塊或者靜態(tài)變量的初始化代碼。如果類的靜態(tài)初始化方法執(zhí)行失敗,通常會導(dǎo)致類的初始化失敗,這意味著這個(gè)類不能被正常使用。會拋出異常,如 ExceptionInInitializerError
- 在Java中,類的靜態(tài)初始化方法只會執(zhí)行一次,無論類被加載多少次,靜態(tài)初始化方法只會在首次加載類的時(shí)候執(zhí)行。因此,cinit 方法不會多次執(zhí)行。一旦類的靜態(tài)初始化方法執(zhí)行過,后續(xù)對同一個(gè)類的加載都不會再次觸發(fā)靜態(tài)初始化方法的執(zhí)行。這種機(jī)制確保了類的靜態(tài)初始化只會在需要的時(shí)候執(zhí)行一次,避免了不必要的開銷和重復(fù)操作。
分配內(nèi)存
在類加載后,接下來虛擬機(jī)將為新?對象分配內(nèi)存。
分配在哪?
主要就是根據(jù)JVM的分配機(jī)制:對象優(yōu)先分配Eden
- 先TLAB分配
- 再通過CAS在Eden區(qū)分配
- 大對象直接分配到老年代
TLAB:線程本地分配緩沖區(qū),為每?個(gè)線程預(yù)先在 Eden 區(qū)分配?塊?私有的緩存區(qū)域,JVM 在給線程中的對象分配內(nèi)存時(shí),?先在 TLAB 分配,當(dāng)對象?于 TLAB 中的剩余內(nèi)存或 TLAB 的內(nèi)存已?盡時(shí)(或者未開啟TLAB),再采?上述的 CAS 進(jìn)?內(nèi)存分配。默認(rèn)情況TLAB僅占每個(gè)Eden區(qū)域的1%。它的主要目的是在多線程并發(fā)環(huán)境下需要進(jìn)行內(nèi)存分配的時(shí)候,減少線程之間對于內(nèi)存分配區(qū)域的競爭,加速內(nèi)存分配的速度。
為什么要CAS分配內(nèi)存?
多個(gè)并發(fā)執(zhí)行的線程需要?jiǎng)?chuàng)建對象、申請分配內(nèi)存的時(shí)候,有可能在 Java 堆的同一個(gè)位置申請,這時(shí)就需要對擬分配的內(nèi)存區(qū)域進(jìn)行加鎖或者采用 CAS 等操作,保證這個(gè)區(qū)域只能分配給一個(gè)線程。
JVM對象分配內(nèi)存如何保證線程安全
在JVM中,為對象分配內(nèi)存的過程需要確保線程安全,因?yàn)樵诙嗑€程環(huán)境下,多個(gè)線程可能會同時(shí)嘗試創(chuàng)建對象。為了保證內(nèi)存分配的線程安全性,JVM采用了以下幾種機(jī)制和技術(shù):
- TLAB(Thread Local Allocation Buffer):
當(dāng)一個(gè)線程需要分配對象時(shí),首先會嘗試在TLAB中進(jìn)行分配。如果TLAB有足夠的空間,分配過程就是線程安全的,因?yàn)闆]有其他線程訪問這個(gè)內(nèi)存塊。
不足:當(dāng)TLAB空間不足時(shí),線程需要請求一個(gè)新的TLAB或者直接從共享堆中分配,這個(gè)過程需要一定的同步機(jī)制。
- CAS(Compare-And-Swap)機(jī)制: 當(dāng)TLAB耗盡或在涉及到跨線程的堆內(nèi)存分配時(shí),CAS有效避免了競爭條件。
- 分代收集: 雖然不是直接用于線程安全,但分代收集(年輕代、老年代、永久代/元空間)使得內(nèi)存管理更高效,減少了直接競爭的機(jī)會。
結(jié)合:TLAB一般對年輕代的內(nèi)存分配進(jìn)行優(yōu)化,更加局部化的內(nèi)存管理有助于線程安全。
通過運(yùn)用這些機(jī)制,JVM能夠在多線程環(huán)境下高效而安全地進(jìn)行內(nèi)存分配,并最大限度地減少同步操作帶來的性能損耗。這樣設(shè)計(jì)不僅提升了性能,也保證了對象內(nèi)存分配的安全性和一致性。
說說對象分配規(guī)則
在Java中,對象分配規(guī)則是關(guān)于如何為新對象分配內(nèi)存的一套規(guī)則,以確保內(nèi)存的有效使用和對象的正確初始化。以下是關(guān)于對象分配的主要規(guī)則:
- 內(nèi)存分配:新對象通常在堆內(nèi)存中分配內(nèi)存空間。
- 對象頭:在為對象分配內(nèi)存空間后,Java虛擬機(jī)會為對象分配一個(gè)對象頭。對象頭包含了一些關(guān)于對象的元信息,如對象的哈希碼、鎖狀態(tài)、垃圾回收信息等。
- 零值初始化:在對象內(nèi)存分配后,所有的成員變量會被初始化為零值。具體的零值取決于變量的數(shù)據(jù)類型。例如,整數(shù)類型會初始化為0,布爾類型會初始化為false,對象引用會初始化為null。
- 構(gòu)造函數(shù)調(diào)用:一旦對象內(nèi)存分配和零值初始化完成,Java虛擬機(jī)會調(diào)用對象的構(gòu)造函數(shù)。
- 對象引用:最后,new 關(guān)鍵字會返回對象的引用,將這個(gè)引用分配給一個(gè)變量,以便后續(xù)可以通過該變量訪問對象的屬性和方法。
- 垃圾回收管理:Java虛擬機(jī)會自動(dòng)管理對象的內(nèi)存。如果對象不再被引用,它會被標(biāo)記為垃圾,并在適當(dāng)?shù)臅r(shí)機(jī)由垃圾回收器回收,釋放占用的內(nèi)存。
圖片
這些規(guī)則確保了對象在創(chuàng)建時(shí)的正確初始化和內(nèi)存管理。對于程序員來說,最重要的是編寫好構(gòu)造函數(shù)以確保對象在創(chuàng)建后具有合適的初始狀態(tài),并且不忘記在不再需要對象時(shí)將引用置為null,以便垃圾回收器能夠回收不再使用的對象。
何時(shí)進(jìn)行類卸載?
類的卸載條件很多,需要滿足以下三個(gè)條件,并且滿足了也不一定會被卸載:
- 該類所有的實(shí)例都已經(jīng)被回收,也就是堆中不存在該類的任何實(shí)例。
- 加載該類的 ClassLoader 已經(jīng)被回收。
- 該類對應(yīng)的 Class 對象沒有在任何地方被引用,也就無法在任何地方通過反射訪問該類方法。
可以通過 -Xnoclassgc 參數(shù)來控制是否對類進(jìn)行卸載。
Java虛擬機(jī)將結(jié)束生命周期的幾種情況?(什么情況會導(dǎo)致JVM退出)
- 正常程序終止: 當(dāng)程序執(zhí)行完main方法,包括所有非守護(hù)線程都終止時(shí),JVM將正常退出。
- 調(diào)用System.exit(int status): 顯式調(diào)用System.exit()方法,以指定的狀態(tài)碼終止當(dāng)前運(yùn)行的Java虛擬機(jī)。
- 未捕獲的異常或錯(cuò)誤: 如果某個(gè)線程拋出的異常沒有被捕獲,并且此異常傳播到了主線程,JVM可能會終止。
- Runtime.halt(int)或崩潰:
直接調(diào)用Runtime.halt()會立即停止Java進(jìn)程,類似于突然終止程序而不調(diào)用任何鉤子。
JVM的致命錯(cuò)誤(如內(nèi)存訪問違規(guī))也可能導(dǎo)致崩潰并退出。
- 外部命令強(qiáng)制關(guān)閉: 例如通過操作系統(tǒng)的任務(wù)管理器或者控制臺命令,如kill命令。或者操作系統(tǒng)出現(xiàn)錯(cuò)誤而導(dǎo)致Java虛擬機(jī)進(jìn)程終止