1. 程式人生 > >深入理解Java類載入機制(一)

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

1 前言:

在上一篇文章一文讓你明白 Java 位元組碼中,
我們瞭解了java位元組碼的解析過程,那麼在接下來的內容中,我們來了解一下類的載入機制。

2 題外話

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

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個靜態變數,下面是一個測試類,打印出靜態屬性的值,就是這麼簡單。

  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都不瞭解它的執行機制,是不是很丟人呢?不過沒關係,馬上你就不丟人了。

4 正題

下面我們具體瞭解類的載入機制。
1)載入
2)連線(驗證-準備-解析)
3)初始化
JVM就是按照上面的順序一步一步的將位元組碼檔案載入到記憶體中並生成相應的物件的。首先將位元組碼載入到記憶體中,然後對位元組碼進行連線,連線階段包括了驗證準備解析這3個步驟,連線完畢之後再進行初始化工作。下面我們一一瞭解:

5 首先我們瞭解一下載入

5.1 什麼是類的載入?

類的載入指的是將類的.class檔案中的二進位制資料讀入記憶體中,將其放在執行時資料區域的方法去內,然後在堆中建立java.lang.Class物件,用來封裝類在方法區的資料結構.只有java虛擬機器才會建立class物件,並且是一一對應關係.這樣才能通過反射找到相應的類資訊.

我們上面提到過Class這個類,這個類我們並沒有new過,這個類是由java虛擬機器建立的。通過它可以找到類的資訊,我們來看下原始碼:

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

從上面貼出的Class類的構造方法原始碼中,我們知道這個構造器是私有的,並且只有虛擬機器才能建立這個類的物件。

5.2 什麼時候對類進行載入呢?

Java虛擬機器有預載入功能。類載入器並不需要等到某個類被"首次主動使用"時再載入它,JVM規範規定JVM可以預測載入某一個類,如果這個類出錯,但是應用程式沒有呼叫這個類, JVM也不會報錯;如果呼叫這個類的話,JVM才會報錯,(LinkAgeError錯誤)。其實就是一句話,Java虛擬機器有預載入功能。

6 類載入器

講到類載入,我們不得不瞭解類載入器.

6.1 什麼是類載入器?

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

6.2 Java自帶有3種類載入器

classloader.png

    1)根類載入器,使用c++編寫(BootStrap),負責載入rt.jar
    2)擴充套件類載入器,java實現(ExtClassLoader)
    3)應用載入器,java實現(AppClassLoader) classpath

根類載入器,是用c++實現的,我們沒有辦法在java層面看到;我們接下來看看ExtClassLoader的程式碼,它是在Launcher類中,

static class ExtClassLoader extends URLClassLoader

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

static class AppClassLoader extends URLClassLoader

他們同時繼承一個類URLClassLoader。
關於這種層次關係,看起來像繼承,其實不是的。我們看到上面的程式碼就知道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;
                   }
              }

原始碼沒有全部貼出,只是貼出關鍵程式碼。從上面程式碼我們知道首先會檢查class是否已經載入了,如果已經載入那就直接拿出,否則再進行載入。其中有一個parent屬性,就是表示父載入器。這點正好說明了載入器之間的關係並不是繼承關係。

6.3 雙親委派機制

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

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

6.4 為何要雙親委派機制

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

我們看一個例子:

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");
    }
輸出內容 :
[email protected]
[email protected]
[email protected]
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");
    }
輸出內容:
[email protected]
[email protected]
classLoader == null

第一個是我們自己載入器載入的類,第二個是直接new的一個物件,是由App類載入器進行載入的,我們把它們的父類載入器打印出來了,可以看出他們的載入器是不一樣的。很奇怪為何會執行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 首先我們看看驗證階段。

驗證階段主要做了以下工作
-將已經讀入到記憶體類的二進位制資料合併到虛擬機器執行時環境中去。
-類檔案結構檢查:格式符合jvm規範-語義檢查:符合java語言規範,final類沒有子類,final型別方法沒有被覆蓋
-位元組碼驗證:確保位元組碼可以安全的被java虛擬機器執行.
二進位制相容性檢查:確保互相引用的類的一致性.如A類的a方法會呼叫B類的b方法.那麼java虛擬機器在驗證A類的時候會檢查B類的b方法是否存在並檢查版本相容性.因為有可能A類是由jdk1.7編譯的,而B類是由1.8編譯的。那根據向下相容的性質,A類引用B類可能會出錯,注意是可能。

7.2 準備階段

java虛擬機器為類的靜態變數分配記憶體並賦予預設的初始值.如int分配4個位元組並賦值為0,long分配8位元組並賦值為0;

7.3 解析階段

解析階段主要是將符號引用轉化為直接引用的過程。比如 A類中的a方法引用了B類中的b方法,那麼它會找到B類的b方法的記憶體地址,將符號引用替換為直接引用(記憶體地址)。

到這裡為止,我們知道了類的載入,類載入器,雙親委派機制,類的連線等等操作。那麼接下來需要講的是類的初始化,初始化內容較多

在這裡給大家提供一個學習交流的平臺,java交流群: 558787436

具有1-5工作經驗的,面對目前流行的技術不知從何下手,需要突破技術瓶頸的可以加群。

在公司待久了,過得很安逸,但跳槽時面試碰壁。需要在短時間內進修、跳槽拿高薪的可以加群。

如果沒有工作經驗,但基礎非常紮實,對java工作機制,常用設計思想,常用java開發框架掌握熟練的可以加群。