深入理解Java類加載機(jī)制(一)

1 前言:

在上一篇文章一文讓你明白Java字節(jié)碼中,
我們了解了java字節(jié)碼的解析過程昂验,那么在接下來的內(nèi)容中逛艰,我們來了解一下類的加載機(jī)制蘑志。

2 題外話

Java的核心是什么金砍?當(dāng)然是JVM了址否,所以說了解并熟悉JVM對于我們理解Java語言非常重要餐蔬,不管你是做Java還是Android,熟悉JVM是我們每個(gè)Java在张、Android開發(fā)者必不可少的技能用含。如果你現(xiàn)在覺得Android的開發(fā)到了天花板的地步,那不妨往下走走帮匾,一起探索JAVA層面的內(nèi)容啄骇。如果我們不了解自己寫的代碼是如何被執(zhí)行的,那么我們只是一個(gè)會寫代碼的程序員瘟斜,我們知其然不知其所以然缸夹。看到很多人說現(xiàn)在工作難找螺句,真是這樣嗎虽惭?如果我們足夠優(yōu)秀,工作還難找嗎蛇尚?如果我們底子足夠深芽唇,需要找工作嗎?找不到工作多想想自己的原因取劫,總是抱怨環(huán)境是沒有用的匆笤,因?yàn)槟銢]辦法去改變壞境。如果我們一直停留在框架層面谱邪,停留在新的功能層面炮捧,那么我們的優(yōu)勢在哪里呢?所以說惦银,我們不僅要學(xué)會寫代碼咆课,還要知道為什么這樣寫代碼末誓,這才是我們的核心競爭力之一。這樣我們的差異化才能夠體現(xiàn)出來书蚪,不信喇澡?我們走著瞧......我們第一個(gè)差異化就是對JVM的掌握,而今天的內(nèi)容類加載機(jī)制是JVM比較核心的部分善炫,如果你想和別人不一樣撩幽,那就一起仔細(xì)研究研究這次的內(nèi)容吧。

3 引子

為了看看自己是否掌握了類加載機(jī)制箩艺,我們看看一道題:

public class Singleton {
  private static Singleton singleton = new Singleton();
  public static int counter1;
  public static int counter2 = 0;
  private Singleton() {
      counter1++;
      counter2++;
  }
  public static Singleton getSingleton() {
      return singleton;
  }
 }

上面是一個(gè)Singleton類窜醉,有3個(gè)靜態(tài)變量,下面是一個(gè)測試類艺谆,打印出靜態(tài)屬性的值榨惰,就是這么簡單。

  public class TestSingleton {
  public static void main(String args[]){
      Singleton singleton = Singleton.getSingleton();
      System.out.println("counter1="+singleton.counter1);
      System.out.println("counter2="+singleton.counter2);
  }
}

在往下看之前静汤,大家先看看這道題的輸出是啥琅催?如果你清楚知道為什么,那么說明你掌握了類的加載機(jī)制虫给,往下看或許有不一樣的收獲藤抡;如果你不懂,那就更要往下看了抹估。我們先不講這道題缠黍,待我們了解了類的加載機(jī)制之后,回過頭看看這道題药蜻,或許有恍然大悟的感覺瓷式,或許講完之后你會懷疑自己是否真正了解Java,或許你寫了這么多年的Java都不了解它的執(zhí)行機(jī)制语泽,是不是很丟人呢贸典?不過沒關(guān)系,馬上你就不丟人了踱卵。

4 正題

下面我們具體了解類的加載機(jī)制廊驼。
1)加載
2)連接(驗(yàn)證-準(zhǔn)備-解析)
3)初始化
JVM就是按照上面的順序一步一步的將字節(jié)碼文件加載到內(nèi)存中并生成相應(yīng)的對象的。首先將字節(jié)碼加載到內(nèi)存中惋砂,然后對字節(jié)碼進(jìn)行連接妒挎,連接階段包括了驗(yàn)證準(zhǔn)備解析這3個(gè)步驟,連接完畢之后再進(jìn)行初始化工作班利。下面我們一一了解:

5 首先我們了解一下加載

5.1 什么是類的加載?

類的加載指的是將類的.class文件中的二進(jìn)制數(shù)據(jù)讀入內(nèi)存中,將其放在運(yùn)行時(shí)數(shù)據(jù)區(qū)域的方法去內(nèi),然后在堆中創(chuàng)建java.lang.Class對象,用來封裝類在方法區(qū)的數(shù)據(jù)結(jié)構(gòu).只有java虛擬機(jī)才會創(chuàng)建class對象,并且是一一對應(yīng)關(guān)系.這樣才能通過反射找到相應(yīng)的類信息.

我們上面提到過Class這個(gè)類榨呆,這個(gè)類我們并沒有new過罗标,這個(gè)類是由java虛擬機(jī)創(chuàng)建的庸队。通過它可以找到類的信息,我們來看下源碼:

 /*
     * Constructor. Only the Java Virtual Machine creates Class
     * objects.
     */
 private Class() {}

從上面貼出的Class類的構(gòu)造方法源碼中闯割,我們知道這個(gè)構(gòu)造器是私有的彻消,并且只有虛擬機(jī)才能創(chuàng)建這個(gè)類的對象。

5.2 什么時(shí)候?qū)︻愡M(jìn)行加載呢宙拉?

Java虛擬機(jī)有預(yù)加載功能宾尚。類加載器并不需要等到某個(gè)類被"首次主動使用"時(shí)再加載它,JVM規(guī)范規(guī)定JVM可以預(yù)測加載某一個(gè)類,如果這個(gè)類出錯(cuò)谢澈,但是應(yīng)用程序沒有調(diào)用這個(gè)類煌贴, JVM也不會報(bào)錯(cuò);如果調(diào)用這個(gè)類的話锥忿,JVM才會報(bào)錯(cuò)牛郑,(LinkAgeError錯(cuò)誤)。其實(shí)就是一句話敬鬓,Java虛擬機(jī)有預(yù)加載功能淹朋。

6 類加載器

講到類加載,我們不得不了解類加載器.

6.1 什么是類加載器钉答?

類加載器負(fù)責(zé)對類的加載础芍。

6.2 Java自帶有3種類加載器

classloader.png
    1)根類加載器,使用c++編寫(BootStrap),負(fù)責(zé)加載rt.jar
    2)擴(kuò)展類加載器,java實(shí)現(xiàn)(ExtClassLoader)
    3)應(yīng)用加載器,java實(shí)現(xiàn)(AppClassLoader) classpath

根類加載器,是用c++實(shí)現(xiàn)的数尿,我們沒有辦法在java層面看到仑性;我們接下來看看ExtClassLoader的代碼,它是在Launcher類中,

static class ExtClassLoader extends URLClassLoader

同時(shí)我們看看AppClassLoader砌创,它也是在Launcher中虏缸,

static class AppClassLoader extends URLClassLoader

他們同時(shí)繼承一個(gè)類URLClassLoader。
關(guān)于這種層次關(guān)系嫩实,看起來像繼承刽辙,其實(shí)不是的。我們看到上面的代碼就知道ExtClassLoader和AppClassLoader同時(shí)繼承同一個(gè)類甲献。同時(shí)我們來看下ClassLoader的loadClass方法也可以知道宰缤,下面貼出源代碼:

 private final ClassLoader parent;
 protected Class<?> loadClass(String name, boolean resolve)
                                           throws ClassNotFoundException
   {
      synchronized (getClassLoadingLock(name)) {
          // First, check if the class has already been loaded
             Class c = findLoadedClass(name);
              if (c == null) {
                 long t0 = System.nanoTime();
                    try {
                         if (parent != null) {
                             c = parent.loadClass(name, false);
                          } else {
                             c = findBootstrapClassOrNull(name);
                               }
                           } catch (ClassNotFoundException e) {
                                // ClassNotFoundException thrown if class not found
                                // from the non-null parent class loader
                            }
                return c;
                   }
              }

源碼沒有全部貼出,只是貼出關(guān)鍵代碼晃洒。從上面代碼我們知道首先會檢查class是否已經(jīng)加載了慨灭,如果已經(jīng)加載那就直接拿出,否則再進(jìn)行加載球及。其中有一個(gè)parent屬性氧骤,就是表示父加載器。這點(diǎn)正好說明了加載器之間的關(guān)系并不是繼承關(guān)系吃引。

6.3 雙親委派機(jī)制

關(guān)于類加載器筹陵,我們不得不說一下雙親委派機(jī)制刽锤。聽著很高大上,其實(shí)很簡單朦佩。比如A類的加載器是AppClassLoader(其實(shí)我們自己寫的類的加載器都是AppClassLoader)并思,AppClassLoader不會自己去加載類,而會委ExtClassLoader進(jìn)行加載语稠,那么到了ExtClassLoader類加載器的時(shí)候宋彼,它也不會自己去加載,而是委托BootStrap類加載器進(jìn)行加載仙畦,就這樣一層一層往上委托输涕,如果Bootstrap類加載器無法進(jìn)行加載的話,再一層層往下走议泵。
上面的源碼也說明了這點(diǎn)占贫。

  if (parent != null) {
           c = parent.loadClass(name, false);
    } else {
           c = findBootstrapClassOrNull(name);
                }

6.4 為何要雙親委派機(jī)制

對于我們技術(shù)來講,我們不但要知其然先口,還要知其所以然型奥。為何要采用雙親委派機(jī)制呢?了解為何之前碉京,我們先來說明一個(gè)知識點(diǎn):
判斷兩個(gè)類相同的前提是這兩個(gè)類都是同一個(gè)加載器進(jìn)行加載的厢汹,如果使用不同的類加載器進(jìn)行加載同一個(gè)類,也會有不同的結(jié)果谐宙。
如果沒有雙親委派機(jī)制烫葬,會出現(xiàn)什么樣的結(jié)果呢?比如我們在rt.jar中隨便找一個(gè)類凡蜻,如java.util.HashMap,那么我們同樣也可以寫一個(gè)一樣的類搭综,也叫java.util.HashMap存放在我們自己的路徑下(ClassPath).那樣這兩個(gè)相同的類采用的是不同的類加載器,系統(tǒng)中就會出現(xiàn)兩個(gè)不同的HashMap類划栓,這樣引用程序就會出現(xiàn)一片混亂兑巾。

我們看一個(gè)例子:

public class MyClassLoader {
    public static void main(String args[]) throws ClassNotFoundException,         IllegalAccessException, InstantiationException {
    ClassLoader loader = new ClassLoader() {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {

            String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
            InputStream inputStream = getClass().getResourceAsStream(fileName);
            if (inputStream==null)
                return super.loadClass(name);
            try {
                byte[] bytes = new byte[inputStream.available()];
                inputStream.read(bytes);
                return defineClass(name,bytes,0,bytes.length);

            } catch (IOException e) {
                e.printStackTrace();
                throw new ClassNotFoundException(name);
            }
        }

    };
    Object object = loader.loadClass("jvm.classloader.MyClassLoader").newInstance();
    System.out.println(object instanceof jvm.classloader.MyClassLoader);

  }
}

大家可以看看輸出的是什么?我們自己定義了一個(gè)類加載器忠荞,讓它去加載我們自己寫的一個(gè)類蒋歌,然后判斷由我們寫的類加載器加載的類是否是MyClassLoader的一個(gè)實(shí)例。
答案是否定的委煤。為什么堂油?因?yàn)閖vm.classloader.MyClassLoader是在classpath下面,是由AppClassLoader加載器加載的碧绞,而我們卻指定了自己的加載器府框,當(dāng)然加載出來的類就不相同了。不信讥邻,我們將他的父類加載器都打印出來迫靖。在上面代碼中加入下面代碼:

ClassLoader classLoader = object.getClass().getClassLoader();
    while (classLoader!=null){
        System.out.println(classLoader);
        classLoader = classLoader.getParent();
    }
    if (classLoader==null){
        System.out.println("classLoader == null");
    }
輸出內(nèi)容 :
jvm.classloader.MyClassLoader$1@60172ec6
sun.misc.Launcher$AppClassLoader@338bd37a
sun.misc.Launcher$ExtClassLoader@20e90906
classLoader == null

對比一下下面的代碼:

  Object object2 = new MyClassLoader();
   ClassLoader classLoader2 = object2.getClass().getClassLoader();

    while (classLoader2!=null){
        System.out.println(classLoader2);
        classLoader2 = classLoader2.getParent();
    }
    if (classLoader2==null){
        System.out.println("classLoader2 == null");
    }
輸出內(nèi)容:
sun.misc.Launcher$AppClassLoader@20e90906
sun.misc.Launcher$ExtClassLoader@234f79cb
classLoader == null

第一個(gè)是我們自己加載器加載的類癣诱,第二個(gè)是直接new的一個(gè)對象,是由App類加載器進(jìn)行加載的袜香,我們把它們的父類加載器打印出來了,可以看出他們的加載器是不一樣的鲫惶。很奇怪為何會執(zhí)行classloader==null這句話蜈首。其實(shí)classloader==null表示的就是根類加載器。我們看看Class.getClassLoader()方法源碼:

/**
 * Returns the class loader for the class.  Some implementations may use
 * null to represent the bootstrap class loader. This method will return
 * null in such implementations if this class was loaded by the bootstrap
 * class loader.
**/
   @CallerSensitive
public ClassLoader getClassLoader() {
    ClassLoader cl = getClassLoader0();
    if (cl == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
    }
    return cl;
}

從注釋中我們知道了欠母,如果返回了null欢策,表示的是bootstrap類加載器。

7 類的連接

講完了類的加載之后赏淌,我們需要了解一下類的連接踩寇。類的連接有三步,分別是驗(yàn)證碳竟,準(zhǔn)備喂柒,解析搬俊。下面讓我們一一了解

7.1 首先我們看看驗(yàn)證階段。

驗(yàn)證階段主要做了以下工作
-將已經(jīng)讀入到內(nèi)存類的二進(jìn)制數(shù)據(jù)合并到虛擬機(jī)運(yùn)行時(shí)環(huán)境中去睛榄。
-類文件結(jié)構(gòu)檢查:格式符合jvm規(guī)范-語義檢查:符合java語言規(guī)范,final類沒有子類,final類型方法沒有被覆蓋
-字節(jié)碼驗(yàn)證:確保字節(jié)碼可以安全的被java虛擬機(jī)執(zhí)行.
二進(jìn)制兼容性檢查:確保互相引用的類的一致性.如A類的a方法會調(diào)用B類的b方法.那么java虛擬機(jī)在驗(yàn)證A類的時(shí)候會檢查B類的b方法是否存在并檢查版本兼容性.因?yàn)橛锌赡蹵類是由jdk1.7編譯的想帅,而B類是由1.8編譯的场靴。那根據(jù)向下兼容的性質(zhì),A類引用B類可能會出錯(cuò)港准,注意是可能旨剥。

7.2 準(zhǔn)備階段

java虛擬機(jī)為類的靜態(tài)變量分配內(nèi)存并賦予默認(rèn)的初始值.如int分配4個(gè)字節(jié)并賦值為0,long分配8字節(jié)并賦值為0;

7.3 解析階段

解析階段主要是將符號引用轉(zhuǎn)化為直接引用的過程。比如 A類中的a方法引用了B類中的b方法浅缸,那么它會找到B類的b方法的內(nèi)存地址轨帜,將符號引用替換為直接引用(內(nèi)存地址)。

到這里為止疗杉,我們知道了類的加載阵谚,類加載器,雙親委派機(jī)制烟具,類的連接等等操作梢什。那么接下來需要講的是類的初始化,初始化內(nèi)容較多朝聋,另開一篇文章深入理解Java類加載機(jī)制(二)講嗡午,這樣大家就不會疲勞和畏懼了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載冀痕,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者荔睹。
  • 序言:七十年代末狸演,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子僻他,更是在濱河造成了極大的恐慌宵距,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吨拗,死亡現(xiàn)場離奇詭異满哪,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)劝篷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門哨鸭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人娇妓,你說我怎么就攤上這事像鸡。” “怎么了哈恰?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵只估,是天一觀的道長。 經(jīng)常有香客問我着绷,道長仅乓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任蓬戚,我火速辦了婚禮夸楣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘子漩。我一直安慰自己豫喧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布幢泼。 她就那樣靜靜地躺著紧显,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缕棵。 梳的紋絲不亂的頭發(fā)上孵班,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音招驴,去河邊找鬼篙程。 笑死,一個(gè)胖子當(dāng)著我的面吹牛别厘,可吹牛的內(nèi)容都是我干的虱饿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼氮发!你這毒婦竟也來了渴肉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤爽冕,失蹤者是張志新(化名)和其女友劉穎仇祭,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體颈畸,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡前塔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了承冰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡食零,死狀恐怖困乒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情贰谣,我是刑警寧澤娜搂,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站吱抚,受9級特大地震影響百宇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜秘豹,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一携御、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧既绕,春花似錦啄刹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至疲扎,卻和暖如春昵时,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背椒丧。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工壹甥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人壶熏。 一個(gè)月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓盹廷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子俄占,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內(nèi)容