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