1. 程式人生 > 程式設計 >通過例項解析Java class檔案編譯載入過程

通過例項解析Java class檔案編譯載入過程

一、Java從編碼到執行

首先我們來看一下Java是如何從編碼到執行的呢? 我們有一個x.java檔案通過執行javac命令可以變成x.class檔案,當我們呼叫Java命令的時候class檔案會被裝載到記憶體中,這個過程叫做classloader。一般情況下我們自己寫程式碼的時候會用到Java的類庫,所以在載入的時候也會把Java類庫相關的類也載入到記憶體中。裝載完成之後會呼叫位元組碼直譯器和JIT即時編譯器來進行解釋和編譯,編譯完之後由執行引擎開始執行,執行引擎下面對應的就是作業系統硬體了。下圖是大體的流程:

通過例項解析Java class檔案編譯載入過程

Java叫做跨平臺的語言,JVM可以稱之為跨語言的平臺;

有個問題:java是解釋執行還是編譯執行?答:解釋和編譯是可以混合的,特別常用的程式碼或則是程式碼用到的次數特別多的時候,會把一個即時編譯做成本地編譯,這樣會很大程度上的提高效率。

Java虛擬機器是如何做到這麼多語言都可以在上面執行,關鍵在於class檔案,任何語言只要能編譯成class檔案,並且符合class檔案的規範你就可以放在Java虛擬機器上去執行。

二、詳解class檔案的載入過程

接下來主要講的是一個class檔案是怎麼從硬碟上到記憶體中,並開始執行的。

類載入主要有三個過程:loading 、linking 、initializing;其中linking又分為三個步驟:verification 、preparation 、resolution;

通過例項解析Java class檔案編譯載入過程

1、首先Loading是什麼意思呢?是把一個class問價load到記憶體中去;

2、接下來是Linking分為了三小步:

  • verification 是用來校驗載入進來的class檔案是否符合class檔案標準,如果不符合直接就會被拒絕了;
  • preparation 是將class檔案靜態變數賦預設值而不是初始值,例如static int i =8;這個步驟並不是將i賦值為8,而是賦值為預設值0;
  • resolution 是把class檔案常量池中用到的符號引用轉換成直接記憶體地址,可以訪問到的內容;

3、initializing 成為初始化,靜態變數在這個時候才會被賦值為初始值;

下面為類載入過程的簡化圖:

通過例項解析Java class檔案編譯載入過程

類載入器的載入過程是分成不同的層次來載入的,不同的類載入器來載入不同的class檔案, Bootstrap >Extension>Application>Custom(自定義類載入器)

1、第一個類載入器的層次為:Bootstrap 稱為啟動類載入器,是Java類載入層次中最頂層的類載入器,負責載入JDK中的核心類庫。

2、第二個類載入器的層次為:Extension 是用來載入擴充套件類的,主要負責載入Java的擴充套件類庫,預設載入JAVA_HOME/jre/lib/ext/目錄下的所有jar包。

3、第三個類載入器的層次為:Application又稱為系統類載入器,負責在JVM啟動時,載入來自在命令java中的classpath或者java.class.path系統屬性或者CLASSPATH作業系統屬性所指定的JAR類包和類路徑。

4、第三個類載入器的層次為:CustomClassLoader(自定義載入器)

package com.example.demo.classloader;

public class ClassLoaderScope {
  public static void main(String[] args) {
    System.out.println("-------------------Bootstrap載入類-------------------");
    String property = System.getProperty("sun.boot.class.path");
    String s = property.replaceAll(";",System.lineSeparator());
    System.out.println(s);

    System.out.println("-------------------Ext載入類-------------------");

    String property1 = System.getProperty("java.ext.dirs");
    String s1 = property1.replaceAll(";",System.lineSeparator());
    System.out.println(s1);

    System.out.println("-------------------App載入類-------------------");

    String property2 = System.getProperty("java.class.path");
    String s2 = property2.replaceAll(";",System.lineSeparator());
    System.out.println(s2);
  }
}
    /**輸出結果只截取了部分*/
    //E:\JDK\jdk1.8\jre\lib\resources.jar
    //E:\JDK\jdk1.8\jre\lib\rt.jar
    //E:\JDK\jdk1.8\jre\lib\sunrsasign.jar
    //E:\JDK\jdk1.8\jre\lib\jsse.jar
    //E:\JDK\jdk1.8\jre\lib\jce.jar
    //E:\JDK\jdk1.8\jre\lib\charsets.jar
    //E:\JDK\jdk1.8\jre\lib\jfr.jar
    //E:\JDK\jdk1.8\jre\classes
    //----------------------------------------------
    //E:\JDK\jdk1.8\jre\lib\ext
    //C:\Windows\Sun\Java\lib\ext
    //----------------------------------------------
    //E:\JDK\jdk1.8\jre\lib\charsets.jar
    //E:\JDK\jdk1.8\jre\lib\deploy.jar
    //E:\JDK\jdk1.8\jre\lib\ext\access-bridge-64.jar
    //E:\JDK\jdk1.8\jre\lib\ext\cldrdata.jar
    //E:\JDK\jdk1.8\jre\lib\ext\dnsns.jar
    //E:\JDK\jdk1.8\jre\lib\ext\jaccess.jar
    //E:\JDK\jdk1.8\jre\lib\ext\jfxrt.jar

特別注意一點這個的層級關係並沒有繼承的關係在裡面,只是單單純純的語法上的繼承;

下圖為類載入的一個全過程:

用比較通俗的話來解釋這個過程,當有一個類需要被載入時,首先要判斷這個類是否已經被載入到記憶體,判斷載入與否的過程是有順序的,如果有自己定義的類載入器,會先到custom class loader 的cache(快取)中去找是否已經載入,若已載入直接返回結果,否則到App的cache中查詢,如果已經存在直接返回,如果不存在,到Extension中查詢,存在直接返回,不存在繼續向父載入器中尋找直到Bootstrap頂層,如果依然沒找到,那就是沒有載入器載入過這個類,需要委派對應的載入器來載入,先看看這個類是否在自己的載入範圍內,如果是直接載入返回結果,若不是繼續向下委派,以此類推直到最下級,如果最終也沒能載入,就會直接拋異常ClassNotFoundException,這就是雙親委派模式。

通過例項解析Java class檔案編譯載入過程

理解雙親委派模式:

1、父載入器:不是類載入器的載入器,也不是類載入器的父類載入器(此處意思是沒有父類與子類之間的繼承關係)。

package com.example.demo.classloader;

/**
 * 驗證了父載入器不是載入器的載入器
 */
public class ParentAndChild {
  public static void main(String[] args) {
    //AppClassLoader
    ClassLoader classLoader = ParentAndChild.class.getClassLoader();
    System.out.println(classLoader);

    //null 這裡AppClassLoader的載入器不是ExtClassLoader 而是Bootstrap
    ClassLoader appclassLoader = ParentAndChild.class.getClassLoader().getClass().getClassLoader();
    System.out.println(appclassLoader);

    //ExtClassLoader  AppClassLoader的父載入器是ExtClassLoader
    ClassLoader parent = ParentAndChild.class.getClassLoader().getParent();
    System.out.println(parent);

    //null
    ClassLoader parentparent = ParentAndChild.class.getClassLoader().getParent().getParent();
    System.out.println(parentparent);

    //null
    ClassLoader parentparentparent = ParentAndChild.class.getClassLoader().getParent().getParent().getParent();
    System.out.println(parentparent);

    /**輸出結果*/
    //sun.misc.Launcher$AppClassLoader@18b4aac2
    //null
    //sun.misc.Launcher$ExtClassLoader@23fc625e
    //null
    //Exception in thread "main" java.lang.NullPointerException at com.example.demo.classloader.ParentAndChild.main(ParentAndChild.java:22)
  }
}

2、雙親委派:其工作原理的是,如果一個類載入器收到了類載入請求,並不會直接去載入,而是自下而上的向頂層類載入器查詢是否已經被載入了,如果被載入就不用進行載入,如果未被載入過,則會自上而下的檢查是否屬於自己載入的範圍,如果屬於則載入,如果不屬於則向下委託,直到類被載入進來才能叫做成功,如果載入不成功就會拋異常classnotfoundexeption,這就叫做雙親委派。

3、為什麼要搞雙親委派模式?

主要是為了安全,這裡可以使用反證法,如果任何類載入器都可以把class載入到記憶體中,我們就可以自定義類載入器來載入Java.lang.string。在打包時可以把密碼儲存為String物件,偷偷摸摸的把密碼傳送到自己的郵箱,這樣會造成安全問題。

三、自定義類載入器

package com.example.demo.classloader;

public class ClassLoaderByHand {
  public static void main(String[] args) throws ClassNotFoundException {
    Class<?> clazz = ClassLoaderByHand.class.getClassLoader().
        loadClass("com.example.demo.threaddemo.juc_002.Account");
    String name = clazz.getName();
    System.out.println(name);

  }
}

  /**
  * 輸出結果
  */
  //com.example.demo.threaddemo.juc_002.Account

程式碼執行結果可以看出,就是你要載入一個類你只要呼叫classLoader中的loadClass()方法就能把這個類載入到記憶體中,載入完成之後會給你返回一個Class類的物件。

在硬碟上找到這個類的原始碼,把它load到記憶體,與此同時生成一個Class物件,上述的小程式是通過ClassLoaderByHand 找到他的載入器AppClassLoader 然後呼叫它的loadClass()方法,讓它幫我們把Account類載入進來,返回一個clazz物件,使用clazz.getName()方法正常返回Account類。

什麼時候我們需要自己定義去載入一個類?

熱部署時就是先把之前載入的類給幹掉 ,然後使用的自定義類載入器來進行重新載入

spring的動態代理,一個新的class 當需要的時候就會把它load到記憶體中

我們還是來看一下原始碼吧,載入過程最主要的還是ClassLoader中的loaderClass()方法:

結合上面給的類載入過程的圖解一起看會更容易一些;

protected Class<?> loadClass(String name,boolean resolve)
      throws ClassNotFoundException
  {
    synchronized (getClassLoadingLock(name)) {
      /**
       * 在載入之前先呼叫findLoadedClass()方法檢視是否已經載入過此類
       * 若載入過 返回該物件
       * 如果未載入則返回null 進行下一步
       */
      // First,check if the class has already been loaded
      Class<?> c = findLoadedClass(name);
      if (c == null) {
        long t0 = System.nanoTime();
        try {
          //判斷有無父載入器 如果不為空說明還未到頂層Bootstrap遞迴呼叫loadClass()
          if (parent != null) {
            c = parent.loadClass(name,false);
          } else {
            //如果沒有父載入器說明呼叫的載入器為Bootstrap Class Loader,在此載入器記憶體中查詢是否已經載入
            c = findBootstrapClassOrNull(name);
          }
        } catch (ClassNotFoundException e) {
          // ClassNotFoundException thrown if class not found
          // from the non-null parent class loader
        }
        //若以上的操作都沒成功載入此類
        if (c == null) {
          // If still not found,then invoke findClass in order
          // to find the class.
          long t1 = System.nanoTime();
          //呼叫自己的findClass()
          c = findClass(name);

          // this is the defining class loader; record the stats
          sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
          sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
          sun.misc.PerfCounter.getFindClasses().increment();
        }
      }
      if (resolve) {
        resolveClass(c);
      }
      return c;
    }
  }

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。