代碼編譯后產生字節(jié)碼而不是本地機器碼匿乃,是存儲格式的一小步,卻是編程語言的一大步豌汇。
一幢炸、類加載機制概述
虛擬機將描述類的.class文件加載到內存,并對數(shù)據(jù)進行校驗瘤礁,轉換解析和初始化阳懂,最終生成可以被虛擬機直接使用的對象。Java中類型的加載柜思、連接和初始化在程序運行期間完成岩调,會有性能開銷,但為Java程序提供了高度的靈活性赡盘,是天生的可動態(tài)擴展的語言号枕。
二、類加載的時機
- 類在虛擬機中的生命周期
- 加載陨享、驗證葱淳、準備钝腺、解析、初始化赞厕、使用和卸載七個階段且順序是確定的艳狐,驗證、準備皿桑、解析統(tǒng)稱為連接毫目。
- 解析階段某些情況下可以在初始化之后開始,這是為了支持Java語言的運行時綁定(又稱動態(tài)綁定或晚期綁定)诲侮。
- 初始化開始的時機
- 遇到new镀虐、getstatic、putstatic沟绪、invokestatic這4條字節(jié)碼指令時刮便,對應Java代碼場景是:使用new實例化對象、讀取或設置類的靜態(tài)字段(final修飾绽慈,已在編譯期把結果放入常量池的靜態(tài)字段除外)恨旱、調用類的靜態(tài)方法。
- 使用java.lang.reflect包的方法對類進行反射調用的時候久信,如果類未進行過初始化則觸發(fā)其初始化窖杀。
- 初始化類時,其父類未進行初始化則先對父類進行初始化裙士。
- 虛擬機啟動時入客,要執(zhí)行的主類(包含main方法的類)虛擬機會先初始化這個主類。
- 使用動態(tài)語言支持時腿椎,java.lang.invoke.MethodHandle實例的解析結果REF_getStatic桌硫、REF_putStatic、REF_invokeStatic的方法句柄啃炸,并且這個方法句柄對應的類沒有進行過初始化铆隘,需要先觸發(fā)其初始化。
- 對于上述5中場景南用,是“有且只有”的能觸發(fā)初始化的情況膀钠,稱為對一個類進行主動引用,除此之外不會觸發(fā)初始化裹虫,稱為被動引用肿嘲。
- 靜態(tài)塊(static{}修飾的代碼塊)只會在初始化時執(zhí)行,定義數(shù)組時不會觸發(fā)類的初始化筑公,靜態(tài)常量會在編譯時被轉化為對常量池中值的引用雳窟。
- 接口(interface)進行初始化時,不要求父接口全部完成初始化匣屡,只有在真正使用到父接口的時候(如引用接口中定義的常量)才會初始化封救。
- 數(shù)組初始化時拇涤,不會觸發(fā)類的初始化。
三誉结、類加載的過程
- 加載階段
- “加載”是“類加載(class loading)”過程的一個階段鹅士,需要在這個階段完成三件事情。
- 通過類的全限定名來獲取定義類的二進制字節(jié)流搓彻,將字節(jié)流代表的靜態(tài)存儲結構轉化為方法區(qū)的運行時數(shù)據(jù)結構如绸,在內存中生成代表類的java.lang.Class對象作為方法區(qū)中這個類的訪問入口。
- 加載階段完成后旭贬,虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)中,方法區(qū)中數(shù)據(jù)存儲格式又虛擬機實現(xiàn)自行定義搪泳,虛擬機規(guī)范沒有規(guī)定稀轨。
- 驗證階段
- 驗證是連接階段的第一步,確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求岸军,不會危害虛擬機的安全奋刽。
- 大致完成四個階段的檢查動作:文件格式驗證、元數(shù)據(jù)驗證艰赞、字節(jié)碼驗證佣谐、符號引用驗證。
- 文件格式驗證方妖,是否以魔數(shù)0xCAFEBABE開頭狭魂,主、次版本號是否在當前虛擬機處理范圍之內党觅,常量池中是否有不被支持的常量類型等等雌澄。該階段保證輸入的字節(jié)流能正確地解析并存儲于方法區(qū),格式上符合Java類型信息的要求杯瞻,因此后面的三個驗證階段是基于方法區(qū)的存儲結構進行镐牺,不直接操作字節(jié)流。
- 元數(shù)據(jù)驗證魁莉,對字節(jié)碼描述的信息進行語義分析睬涧,保證其描述的信息符合Java語言規(guī)范的要求,如:這個類是否有父類旗唁、是否繼承了被final修飾的父類畦浓、如果不是抽象類是否實現(xiàn)了父類或接口之中要求實現(xiàn)的方法等等。
- 字節(jié)碼驗證逆皮,是驗證階段最復雜的宅粥,通過數(shù)據(jù)流和控制流分析確定程序語義是合法的、符合邏輯的电谣,保證被校驗的類的方法在運行時不會危害虛擬機的安全秽梅,如類型不匹配的情況抹蚀。
- 符號引用驗證,對類自身以外的信息進行匹配性校驗企垦,如符號引用中通過字符串描述的全限定名是否能找到對應的類环壤。
- 準備階段
- 準備階段是正式為類變量分配內存并設置類變量初始值的階段,變量需使用的內存都將在方法區(qū)中進行分配钞诡。需要注意的是郑现,該階段進行內存分配的僅有類變量(被static修飾的變量),不包括實例變量荧降。類變量按類型在內存分配空間后接箫,初始值為零值(int:0,long:0L朵诫,short:(short)0辛友,char:'\u0000',byte:(byte)0剪返,boolean:false废累,float:0.0f,double:0.0d脱盲,reference:null)邑滨。如果是被final修飾的變量,編譯時javac會為變量生成ConstantValue屬性钱反,在該階段虛擬機根據(jù)ConstantValue屬性將變量賦值掖看。
- 解析階段
- 該階段是虛擬機將常量池內的符號引用替換為直接引用的過程。
- 虛擬機規(guī)范沒有規(guī)定解析階段發(fā)生的具體時間诈铛,只要求在執(zhí)行用于操作符號引用的字節(jié)碼指令之前乙各,先對它們使用的符號引用進行解析。
- 初始化
- 該階段根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其他資源幢竹,即該階段是執(zhí)行類構造器<clinit>()方法的過程耳峦。
- <clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊中的語句合并產生,收集的順序是由語句在源文件中出現(xiàn)的順序所決定的焕毫,靜態(tài)語句塊只能訪問到定義在靜態(tài)語句塊之前的變量蹲坷,在其之后定義的變量不能訪問但可以賦值。
四邑飒、類加載器
- 類與類加載器
- 虛擬機把類加載階段中循签,“通過全限定名獲取描述此類的二進制字節(jié)流”交給虛擬機外部去實現(xiàn),以便讓應用程序自己決定如何去獲取所需要的類疙咸,實現(xiàn)這個動作的代碼模塊稱為“類加載器”县匠。
- 每一個類加載器都擁有獨立的類名稱空間,比較兩個類是否“相等”,只有在兩個類是由同一個類加載器加載的前提下才有意義乞旦。
- 雙親委派模式
- 從Java虛擬機角度看贼穆,有兩種不同的類加載器,一種試啟動類加載器(Bootstrap ClassLoader)兰粉,在Hotspot中由C++語言實現(xiàn)故痊;一種是所有其他的類加載器,由Java語言實現(xiàn)玖姑,獨立于虛擬機外部愕秫,并且繼承自java.lang.ClassLoader類。
- 大部分時候程序會用到三種系統(tǒng)提供的類加載器焰络,包括:啟動類加載器戴甩,負責將存在<JAVA_HOME>\lib目錄中的,或被-Xbootclasspath參數(shù)指定的路徑中的類庫加載到虛擬機闪彼;擴展類加載器等恐,它負責加載<JAVA_HOME>\lib\ext目錄中,或java.ext.dirs系統(tǒng)變量指定路徑中的所有類庫备蚓;應用程序類加載器(系統(tǒng)類加載器),由ClassLoader.getSystemClassLoader()方法獲取囱稽,如果程序沒有自定義過類加載器郊尝,一般情況下這個就是程序默認的類加載器。
- 啟動類加載器 <--- 擴展類加載器 <--- 應用程序類加載器 <--- 自定義類加載器战惊,這樣的層次關系稱為類加載器的雙親委派模型(Parents Delegation Model)流昏。
- 雙親委派模型在工作時,如果一個類加載器收到了類加載的請求吞获,它首先不會自己嘗試加載况凉,而是將請求委派給父類加載器去完成,每一個層次的類加載器都是如此各拷。這樣可以保證任何一個類加載請求都是由同一個類加載器完成刁绒。
說明
- 文中內容主要來自于《深入理解Java虛擬機》,更多內容請關注原著烤黍。
- 此文是讀書時對重點知識的記錄知市,如有錯誤請一定指出。