jvm的類加載機制-類的加載過程(一)

jvm的類加載機制

jvm加載機制對于大多數(shù)人來說都是比較陌生的池户,但是當你作為一個擁有幾年工作經驗的開發(fā)來說动羽,了解內部機制的是極其重要的墨闲,下面先看下代碼佩迟!

public class SSClass
{
    static
    {
        System.out.println("SSClass");
    }
}    
public class SuperClass extends SSClass
{
    static
    {
        System.out.println("SuperClass init!");
    }

    public static int value = 123;

    public SuperClass()
    {
        System.out.println("init SuperClass");
    }
}
public class SubClass extends SuperClass
{
    static 
    {
        System.out.println("SubClass init");
    }

    static int a;

    public SubClass()
    {
        System.out.println("init SubClass");
    }
}
public class NotInitialization
{
    public static void main(String[] args)
    {
        System.out.println(SubClass.value);
    }
}

運行結果

SSClass
SuperClass init!
123

類加載過程


類從被加載到虛擬機內存中開始,到卸載出內存為止昏兆,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準備(Preparation)爬虱、解析(Resolution)隶债、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段跑筝。其中準備死讹、驗證、解析3個部分統(tǒng)稱為連接(Linking)曲梗。如圖所示赞警。

image.png

加載、驗證虏两、準備愧旦、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始定罢,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始笤虫,這是為了支持Java語言的運行時綁定(也稱為動態(tài)綁定或晚期綁定)。以下陳述的內容都已HotSpot為基準祖凫。

一琼蚯、 加載

在加載階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機需要完成以下3件事情:

image.png
  1. 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流(并沒有指明要從一個Class文件中獲取惠况,可以從其他渠道遭庶,譬如:網絡、動態(tài)生成稠屠、數(shù)據庫等)峦睡;
  2. 將這個字節(jié)流所代表的靜態(tài)存儲結構轉化為方法區(qū)的運行時數(shù)據結構;
    在內存中生成一個代表這個類的java.lang.Class對象完箩,作為方法區(qū)這個類的各種數(shù)據的訪問入口赐俗;
  3. 加載階段和連接階段(Linking)的部分內容(如一部分字節(jié)碼文件格式驗證動作)是交叉進行的,加載階段尚未完成弊知,連接階段可能已經開始阻逮,但這些夾在加載階段之中進行的動作,仍然屬于連接階段的內容秩彤,這兩個階段的開始時間仍然保持著固定的先后順序叔扼。

其實通俗來講就是講類的.class文件中二進制數(shù)據讀入到內存中,將其放在運行時數(shù)據區(qū)的方法區(qū)內漫雷,然后在堆區(qū)創(chuàng)建一個
java.lang.Class對象瓜富,用來封裝類在方法區(qū)內的數(shù)據結構。

注:這一塊原先說了在加載過程中會執(zhí)行靜態(tài)代碼塊降盹,后來經沐小晨曦私信我与柑,該地方與后面再初始化過程執(zhí)行靜態(tài)代碼塊說法自相矛盾,后來查了一番,確實如此价捧,多謝提醒丑念。可以確定的說:

靜態(tài)代碼塊是在初始化的時候執(zhí)行的

二结蟋、連接

類加載完成后就進入了類的連接階段脯倚,連接階段主要分為三個過程分別是:驗證,準備和解析嵌屎。在連接階段推正,主要是將已經讀到內存的類的二進制數(shù)據合并到虛擬機的運行時環(huán)境中去。

驗證

驗證是連接階段的第一步宝惰,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求植榕,并且不會危害虛擬機自身的安全。
驗證階段大致會完成4個階段的檢驗動作:

這個階段主要目的是保證Class流的格式是正確的掌测。主要驗證的內容包括:

  1. 文件格式的驗證

是否以0xCAFEBABE開頭
版本號是否合理

  1. 元數(shù)據的驗證

是否有父類
是否繼承了final類
非抽象類實現(xiàn)了所有抽象方法

  1. 字節(jié)碼驗證

運行檢查
棧數(shù)據類型和操作碼數(shù)據參數(shù)吻合
跳轉指令指定到合理的位置

  1. 符號引用驗證

常量池中描述類是否存在
訪問的方法或字段是否存在且有足夠的權限

準備

這個階段主要是為對象和變量分配內存内贮,并為類設置初始值(方法區(qū)中)

對于static類型變量在這個階段會為其賦值為默認值,比如

public static int v=5,

在這個階段會為其賦值為v=0汞斧,

public static final int v=5,

而對于static final類型的變量夜郁,在準備階段就會被賦值為正確的值

解析

解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或接口粘勒、字段竞端、類方法、接口方法庙睡、方法類型事富、方法句柄和調用點限定符7類符號引用進行。

初始化

在這個階段主要執(zhí)行類的構造方法乘陪。并且為靜態(tài)變量賦值為初始值统台,執(zhí)行靜態(tài)塊代碼。

所有的Java類只有在對類的首次主動使用時才會被初始化啡邑。主動使用的情況有六中贱勃,其他情況都屬于被動使用:
1、 創(chuàng)建類的實例
2谤逼、訪問某個類或接口的靜態(tài)變量贵扰,或者對該靜態(tài)變量賦值
3、調用類的靜態(tài)方法
4流部、反射(Class.fotName)
5戚绕、初始化一個類的子類
6、Java虛擬機啟動時被標明為啟動類的類(面方法所在的類)

注意:
1枝冀、當Java虛擬機初始化一個類時舞丛,要求他的所有父類都已經被初始化耘子,但是這條規(guī)則并不適合接口。在初始化一個類或接口時瓷马,并不會先初始化它所實現(xiàn)的接口拴还。
2、只有當程序訪問的靜態(tài)變量或靜態(tài)方法確實在當前類或當前接口中定義時欧聘,才可以認為是對類或接口的主動使用。如果靜態(tài)方法或變量在parent中定義端盆,從子類進行調用怀骤,則不會初始化子類。

public class Test
{
    static
    {
        i=0;
        System.out.println(i);//這句編譯器會報錯:Cannot reference a field before it is defined(非法向前應用)
    }
    static int i=1;
}

那么去掉報錯的那句焕妙,改成下面:

public class Test
{
    static
    {
        i=0;
//      System.out.println(i);
    }
    static int i=1;

    public static void main(String args[])
    {
        System.out.println(i);
    }
}

輸出結果是什么呢蒋伦?當然是1啦~在準備階段我們知道i=0,然后類初始化階段按照順序執(zhí)行焚鹊,首先執(zhí)行static塊中的i=0,接著執(zhí)行static賦值操作i=1,最后在main方法中獲取i的值為1痕届。

<clinit>():虛擬機在裝載一個類初始化的時候調用的
<init>():虛擬機類實例類化的時候調用的

<clinit>()方法對于類或者接口來說并不是必需的研叫,如果一個類中沒有靜態(tài)語句塊,也沒有對變量的賦值操作璧针,那么編譯器可以不為這個類生產<clinit>()方法嚷炉。 

接口中不能使用靜態(tài)語句塊化撕,但仍然有變量初始化的賦值操作帆竹,因此接口與類一樣都會生成<clinit>()方法屁柏。但接口與類不同的是靡菇,執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口的<clinit>()方法众雷。只有當父接口中定義的變量使用時斗幼,父接口才會初始化辙芍。另外瞻想,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<clinit>()方法胞枕。 

虛擬機會保證一個類的<clinit>()方法在多線程環(huán)境中被正確的加鎖杆煞、同步,如果多個線程同時去初始化一個類曲稼,那么只會有一個線程去執(zhí)行這個類的<clinit>()方法索绪,其他線程都需要阻塞等待,直到活動線程執(zhí)行<clinit>()方法完畢贫悄。如果在一個類的<clinit>()方法中有耗時很長的操作瑞驱,就可能造成多個線程阻塞,在實際應用中這種阻塞往往是隱藏的窄坦。

虛擬機規(guī)范嚴格規(guī)定了有且只有5中情況(jdk1.7)必須對類進行“初始化”(而加載唤反、驗證凳寺、準備自然需要在此之前開始):

  1. 遇到new,getstatic,putstatic,invokestatic這失調字節(jié)碼指令時,如果類沒有進行過初始化彤侍,則需要先觸發(fā)其初始化肠缨。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態(tài)字段(被final修飾盏阶、已在編譯器把結果放入常量池的靜態(tài)字段除外)的時候晒奕,以及調用一個類的靜態(tài)方法的時候。
  1. 使用java.lang.reflect包的方法對類進行反射調用的時候名斟,如果類沒有進行過初始化脑慧,則需要先觸發(fā)其初始化。
  1. 當初始化一個類的時候砰盐,如果發(fā)現(xiàn)其父類還沒有進行過初始化闷袒,則需要先觸發(fā)其父類的初始化。
  2. 當虛擬機啟動時岩梳,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類)囊骤,虛擬機會先初始化這個主類。
  3. 當使用jdk1.7動態(tài)語言支持時冀值,如果一個java.lang.invoke.MethodHandle實例最后的解析結果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄也物,并且這個方法句柄所對應的類沒有進行初始化,則需要先出觸發(fā)其初始化池摧。

案例分析


package jvm.classload;

public class StaticTest
{
    public static void main(String[] args)
    {
        staticFunction();
    }

    static StaticTest st = new StaticTest();

    static
    {
        System.out.println("1");
    }

    {
        System.out.println("2");
    }

    StaticTest()
    {
        System.out.println("3");
        System.out.println("a="+a+",b="+b);
    }

    public static void staticFunction(){
        System.out.println("4");
    }

    int a=110;
    static int b =112;
}

結果為什么呢焦除??作彤?

如果按照下面的正常邏輯:

java中賦值順序:

  1. 父類的靜態(tài)變量賦值
  2. 自身的靜態(tài)變量賦值
  3. 父類成員變量賦值和父類塊賦值
  4. 父類構造函數(shù)賦值
  5. 自身成員變量賦值和自身塊賦值
  6. 自身構造函數(shù)賦值

最終得出的結果是

1,4

但是這肯定是錯的膘魄,正確的打印的結果是

2

3
a=110,b=0
1
4

是不是有點不可思議,那么聽我慢慢分析=呋洹创葡!
首先類的生命周期:

加載->驗證->準備->解析->初始化->使用->卸載

在這些過程中,只有在準備階段和初始化階段會涉及到類和變量的賦值和初始化绢慢;

準備階段

該階段需要做的就是類變量的內存分配和設置默認值
static int b =112;
這里對b進行初始化灿渴,但是初始化值b為0,如果添加了 final的話胰舆,則b=112

初始化階段

初始化階段需要做的是執(zhí)行類構造器骚露,

所以首先執(zhí)行的是:
類的初始化階段需要做是執(zhí)行類構造器(類構造器是編譯器收集所有靜態(tài)語句塊和類變量的賦值語句按語句在源碼中的順序合并生成類構造器,對象的構造方法是<init>()缚窿,類的構造方法是<clinit>()棘幸,在代碼中我們可以看到第一個靜態(tài)變量的是: static StaticTest st = new StaticTest();

因此先執(zhí)行第一條靜態(tài)變量的賦值語句即st = new StaticTest (),
然后此時會進行對象的初始化:-->初始化成員變量-->執(zhí)行構造方法

因此設置a為110->打印2->執(zhí)行構造方法(打印3,此時a已經賦值為110倦零,但是b只是設置了默認值0误续,并未完成賦值動作)吨悍,等對象的初始化完成后繼續(xù)執(zhí)行之前的類構造器的語句,接下來就不詳細說了蹋嵌,按照語句在源碼中的順序執(zhí)行即可育瓜。
?
這里面還牽涉到一個冷知識,就是在嵌套初始化時有一個特別的邏輯栽烂。特別是內嵌的這個變量恰好是個靜態(tài)成員躏仇,而且是本類的實例。

這會導致一個有趣的現(xiàn)象:“實例初始化竟然出現(xiàn)在靜態(tài)初始化之前”愕鼓。
其實并沒有提前钙态,你要知道java記錄初始化與否的時機。

將上訴案例代碼簡化一下

public class Test {
    public static void main(String[] args) {
        func();
    }
    static Test st = new Test();
    static void func(){}
}
  1. 首先在執(zhí)行此段代碼時菇晃,首先由main方法的調用觸發(fā)靜態(tài)初始化。
  1. 在初始化Test 類的靜態(tài)部分時蚓挤,遇到st這個成員磺送。
  2. 但湊巧這個變量引用的是本類的實例。
  3. 那么問題來了灿意,此時靜態(tài)初始化過程還沒完成就要初始化實例部分了估灿。是這樣么?
  4. 從人的角度是的缤剧。但從java的角度馅袁,一旦開始初始化靜態(tài)部分,無論是否完成荒辕,后續(xù)都不會再重新觸發(fā)靜態(tài)初始化流程了汗销。
  5. 因此在實例化st變量時,實際上是把實例初始化嵌入到了靜態(tài)初始化流程中抵窒,并且在樓主的問題中弛针,嵌入到了靜態(tài)初始化的起始位置。這就導致了實例初始化完全至于靜態(tài)初始化之前李皇。這也是導致a有值b沒值的原因削茁。
    最后再考慮到文本順序,結果就顯而易見了掉房。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末茧跋,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子卓囚,更是在濱河造成了極大的恐慌瘾杭,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捍岳,死亡現(xiàn)場離奇詭異富寿,居然都是意外死亡睬隶,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門页徐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苏潜,“玉大人,你說我怎么就攤上這事变勇⌒糇螅” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵搀绣,是天一觀的道長飞袋。 經常有香客問我,道長链患,這世上最難降的妖魔是什么巧鸭? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮麻捻,結果婚禮上纲仍,老公的妹妹穿的比我還像新娘。我一直安慰自己贸毕,他們只是感情好郑叠,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著明棍,像睡著了一般乡革。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上摊腋,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天沸版,我揣著相機與錄音,去河邊找鬼歌豺。 笑死推穷,一個胖子當著我的面吹牛,可吹牛的內容都是我干的类咧。 我是一名探鬼主播馒铃,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼痕惋!你這毒婦竟也來了区宇?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤值戳,失蹤者是張志新(化名)和其女友劉穎议谷,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體堕虹,經...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡卧晓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年芬首,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片逼裆。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡郁稍,死狀恐怖,靈堂內的尸體忽然破棺而出胜宇,到底是詐尸還是另有隱情耀怜,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布桐愉,位于F島的核電站财破,受9級特大地震影響,放射性物質發(fā)生泄漏从诲。R本人自食惡果不足惜左痢,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望系洛。 院中可真熱鬧抖锥,春花似錦、人聲如沸碎罚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽荆烈。三九已至,卻和暖如春竟趾,著一層夾襖步出監(jiān)牢的瞬間憔购,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工岔帽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留玫鸟,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓犀勒,卻偏偏與公主長得像屎飘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子贾费,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345