虛擬機類加載機制

包機制:

使用package語句,每個源文件只能有一個package語句,屬只於一個包。

一個包中可在底下包含其他包,用“.”的形式表示,在路徑中代表”/”,如package lib.hello,說明lib包內包含hello包,其中lib在根目錄下,要使用hello必須從根目錄開始。

該包機制能夠實施對訪問權限的控制。

類文件結構(包訪問機制就定義在其中的access_flags中)

.class描述的類文件結構中包含:頭4個字節爲魔數0XCAFEBABE,用來判斷是否被虛擬機接受的class文件;而後是常量池,常量數量不固定,起初會放入常量池容量計數值,然後放入常量,包含字面量和符號引用;常量池結束後緊接着兩個字節代表訪問標識(access_flags),用於識別一些類或接口層次的訪問信息,包括:該Class是類還是接口,是否定義爲public,是否定義爲abstract,若是類,是否被定義爲final等;類索引、父類索引、接口索引等;字段表集合(field_info)包含字段作用域如public,是否爲static,可變性(final),併發可見性(volatile),可否被序列化(transient修飾符),字段數據類型(基本類型、對象、數組等);方法表;屬性表;歸根到底一張圖:


類加載的過程:

類從加載到虛擬機內存中開始到卸載出內存,整個生命週期包括:


加載、連接(驗證、準備)、初始化、使用、卸載這五個階段順序是確定的,類加載過程按這種順序開始執行,但解析階段就不一定:某些情況下,解析可能會在初始化之後,以支持java的動態綁定(運行期綁定)。這裏說的開始是說每個階段開始的順序,不一定是完成的先後順序,一般來說,這些階段都是交叉混合進行,在一個階段運行過程中開始調用激活下一個階段。

類初始化時機只要滿足以下5個條件之一即可:




只有這四種會觸發類初始化、稱爲對一個類進行主動引用,除此之外的所有引用類的的方式,都不會觸發初始化,稱爲被動引用。三個栗子:

Ex1. 被動引用之一——調用子類中繼承的父類靜態變量


輸出:


對靜態字段,只有直接定義該字段的類纔會被初始化。


Ex2. 被動引用之二——定義數組來引用類,不會觸發類的初始化


輸出:

空白

因爲對數組引用類的情況,不會觸發該類的初始化。


EX3.被動引用三——常量在編譯階段存入調用類的常量池,本質上沒有直接引用到定義常量的類,不會觸發定義常量的類的初始化。


結果:


hello world

沒輸出SuperClass init,因爲雖在代碼中引用了TestP中的靜態常量HELLOWORLD,但在編譯期該常量值已被存儲到了Test類的常量池中,對常量TestP.HELLOWORLD的引用實際上都轉爲Test類對自身常量池的引用了。

接口加載與類加載有所不同,接口中不能使用static{}語句塊,但也有<clinit>()類構造器,真正和類有區別的是前面所述的“有且僅有”需要開始初始化場景的第三種:當一個類在初始化時,要求父類全部都已經初始化過了,但是接口在初始化時,並不要其父類接口全部完成初始化,只有真正用到父類接口(如引用接口中定義的常量)纔會初始化


類加載(在初始化之前)具體過程:


注意:“準備”部分有問題。“準備”中只是爲類變量進行內存分配,而不是實例變量,且如

Public static int value = 123,經過準備階段,其值僅爲初始值0,而不是123,賦值會在“初始化(<clinit>(),這不是在準備階段做的)”進行;但若該value爲final,那麼在準備階段就會賦值,並帶有ConstantValue屬性


“初始化”階段:

l  <clinit>()由編譯器自動收集類中所有類變量的賦值動作和靜態語句塊中的語句合併產生,收集順序與語句定義順序有關,靜態語句塊只能訪問定義在該語句塊之前的變量,定義在它之後的變量,在前面的靜態語句塊可以賦值,但不能訪問

l  <clinit>()方法與類的構造器不同或者實例構造器<init>()不同,它不需要顯式調用父類構造器、虛擬機會保證在子類<clinit>()執行前、父類的<clinit>()已執行完畢,因此在虛擬機中第一個被執行的<clinit>()肯定是java.lang.Object

l  父類<clinit>()先執行,所以父類中定義的靜態語句塊要優先子類的變量賦值操作。

l  <clinit>()對於接口和類不是必須的,若類中沒有靜態語句塊也沒有對變量的賦值操作,那麼可以不爲該類生成該<clinit>()

l  接口中不能用static語句塊,但仍有變量初始化操作,因爲接口也會有該<clinit>(),但不同的是:執行接口的該方法時不需要先執行父接口的該方法。只有當父接口中定義的變量被使用時,父接口才會初始化。另外接口的實現類在初始化時也一樣不會執行接口的<clinit>()。

l  虛擬機會保證一個類的<clinit>()在多線程環境下正確加鎖和同步。

類加載器

執行上面這段類加載過程的是ClassLoader,在JVM中是通過ClassLoader和類本身共同判斷兩個Class是否相同,換句話說,不同的ClassLoader加載同名Class文件會被認爲是不同的類。

 

類加載器之間的關係:

有時不會從Class文件加載流(如java applet是從網絡中加載),那麼這個ClassLoader和普通的實現邏輯就不同,需要使用不同的ClassLoader。但允許使用不同的classLoader引發新的問題,如果我也聲明瞭一個java.lang.Object,但內部代碼非常危險,這裏就引入了雙親委派模式:


l  Bootstrap ClassLoader引導類加載器:負責Java核心類的加載,如String等,在JRE的lib下的rt.jar中

l  Extension ClassLoader擴展類加載器:加載JRE中ext目錄下的jar包類

l  Application ClassLoader:負責在JVM啓動時加載來自java命令的class文件(自定義的)ClassPath環境變量所指定的jar包和類路徑。

雙親委派模型要求除頂層的啓動類加載器外,其餘的類加載器應該有父類加載器(一般不以繼承方式,而是以組合方式複用父類加載器的代碼),它在接到加載類的請求時優先委派給父類加載器完成。

其工作過程爲:若一個類加載器收到了類加載請求,首先不會加載該類,而是交給父類加載器完成,只有當父類加載器反饋自己無法完成該加載請求,子加載器纔會嘗試自己加載。

其好處是:Java類隨它的類加載器一起具備了優先級的層次關係,如java.lang.Object類,無論哪一個類加載器要加載該類,最終都是委派給處於最頂端的啓動類加載器完成,因此Object類在程序的各種類加載器環境中都是相同的,相反若沒有雙親委派模式,自己編寫的類加載器去加載該Object類就會使代碼異常混亂。(實現方式遞歸一直到最頂層的bootstrap ClassLoader來加載當前類,加載方式由上而下)。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章