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

1 前言:

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

2 題外話

Java的核心是什么?當然是JVM了,所以說了解并熟悉JVM對于我們理解Java語言非常重要,不管你是做Java還是Android,熟悉JVM是我們每個Java、Android開發(fā)者必不可少的技能。如果你現(xiàn)在覺得Android的開發(fā)到了天花板的地步,那不妨往下走走,一起探索JAVA層面的內(nèi)容。如果我們不了解自己寫的代碼是如何被執(zhí)行的,那么我們只是一個會寫代碼的程序員,我們知其然不知其所以然。看到很多人說現(xiàn)在工作難找,真是這樣嗎?如果我們足夠優(yōu)秀,工作還難找嗎?如果我們底子足夠深,需要找工作嗎?找不到工作多想想自己的原因,總是抱怨環(huán)境是沒有用的,因為你沒辦法去改變壞境。如果我們一直停留在框架層面,停留在新的功能層面,那么我們的優(yōu)勢在哪里呢?所以說,我們不僅要學(xué)會寫代碼,還要知道為什么這樣寫代碼,這才是我們的核心競爭力之一。這樣我們的差異化才能夠體現(xiàn)出來,不信?我們走著瞧......我們第一個差異化就是對JVM的掌握,而今天的內(nèi)容類加載機制是JVM比較核心的部分,如果你想和別人不一樣,那就一起仔細研究研究這次的內(nèi)容吧。

3 引子

為了看看自己是否掌握了類加載機制,我們看看一道題:

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;
  }
 }

上面是一個Singleton類,有3個靜態(tài)變量,下面是一個測試類,打印出靜態(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);
  }
}

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

4 正題

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

5 首先我們了解一下加載

5.1 什么是類的加載?

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

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

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

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

5.2 什么時候?qū)︻愡M行加載呢?

Java虛擬機有預(yù)加載功能。類加載器并不需要等到某個類被"首次主動使用"時再加載它,JVM規(guī)范規(guī)定JVM可以預(yù)測加載某一個類,如果這個類出錯,但是應(yīng)用程序沒有調(diào)用這個類, JVM也不會報錯;如果調(diào)用這個類的話,JVM才會報錯,(LinkAgeError錯誤)。其實就是一句話,Java虛擬機有預(yù)加載功能。

6 類加載器

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

6.1 什么是類加載器?

類加載器負責對類的加載。

6.2 Java自帶有3種類加載器

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

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

static class ExtClassLoader extends URLClassLoader

同時我們看看AppClassLoader,它也是在Launcher中,

static class AppClassLoader extends URLClassLoader

他們同時繼承一個類URLClassLoader。
關(guān)于這種層次關(guān)系,看起來像繼承,其實不是的。我們看到上面的代碼就知道ExtClassLoader和AppClassLoader同時繼承同一個類。同時我們來看下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)加載那就直接拿出,否則再進行加載。其中有一個parent屬性,就是表示父加載器。這點正好說明了加載器之間的關(guān)系并不是繼承關(guān)系。

6.3 雙親委派機制

關(guān)于類加載器,我們不得不說一下雙親委派機制。聽著很高大上,其實很簡單。比如A類的加載器是AppClassLoader(其實我們自己寫的類的加載器都是AppClassLoader),AppClassLoader不會自己去加載類,而會委ExtClassLoader進行加載,那么到了ExtClassLoader類加載器的時候,它也不會自己去加載,而是委托BootStrap類加載器進行加載,就這樣一層一層往上委托,如果Bootstrap類加載器無法進行加載的話,再一層層往下走。
上面的源碼也說明了這點。

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

6.4 為何要雙親委派機制

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

我們看一個例子:

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);

  }
}

大家可以看看輸出的是什么?我們自己定義了一個類加載器,讓它去加載我們自己寫的一個類,然后判斷由我們寫的類加載器加載的類是否是MyClassLoader的一個實例。
答案是否定的。為什么?因為jvm.classloader.MyClassLoader是在classpath下面,是由AppClassLoader加載器加載的,而我們卻指定了自己的加載器,當然加載出來的類就不相同了。不信,我們將他的父類加載器都打印出來。在上面代碼中加入下面代碼:

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

第一個是我們自己加載器加載的類,第二個是直接new的一個對象,是由App類加載器進行加載的,我們把它們的父類加載器打印出來了,可以看出他們的加載器是不一樣的。很奇怪為何會執(zhí)行classloader==null這句話。其實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 類的連接

講完了類的加載之后,我們需要了解一下類的連接。類的連接有三步,分別是驗證,準備,解析。下面讓我們一一了解

7.1 首先我們看看驗證階段。

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

7.2 準備階段

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

7.3 解析階段

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,908評論 2 372

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