天天看點

位元組碼和類加載器位元組碼和類加載器

文章目錄

  • 位元組碼和類加載器
    • 一、類位元組碼
    • 二、類加載機制
      • 1、類的生命周期
      • 2、生命周期不同階段講解
        • 1)加載(查找并加載類的二進制資料)
        • 2)連接配接(確定被加載的類的正确性)
          • 1- 驗證
          • 2 - 準備
          • 3 - 解析
        • 3)初始化
      • 3、類加載器
        • 1)類加載器層次
        • 2)尋找類加載器
        • 3)怎麼進行類加載
      • 4、JVM 類加載機制
        • 1)雙親委派機制
          • 1 - 雙親委派機制講解
          • 2 - 不同層次的類加載器是繼承關系嗎
          • 3 - 類加載器的實作
          • 4 - 實作自己的類加載器
          • 5 - 怎麼破壞雙親委派

位元組碼和類加載器

Jvm,即 java 虛拟機,是 java 能實作"一次編寫、到處執行"的基礎

不管是為了面試,還是為了深入了解 java,我們都有必要對 jvm 進行深度學習

Jvm 的整體架構,可以通過下面這張圖進行概括:

位元組碼和類加載器位元組碼和類加載器

我們後面會根據這張圖,去一點點學習 jvm 的知識

這一篇文章,我們就講解一下其中的 位元組碼 和 類加載器 的相關知識

位元組碼和類加載器位元組碼和類加載器

一、類位元組碼

位元組碼檔案,是 jvm 可讀的檔案形式,檔案字尾是

.class

我們寫的 .java 檔案,需要經過 javac 編譯,才能編譯成位元組碼,供 jvm 處理

現在 jvm 也不再隻支援 java,由 java 衍生出很多其他語言,但大體方式都是差不多的

位元組碼和類加載器位元組碼和類加載器

我們使用指令

javac

就可以對 .class 檔案進行編譯

javac Main.java
           

編譯過後,就會多出一個 Main.class 檔案

打開來看一下:

所有位元組碼檔案的頭四個位元組,都是魔數,它的作用,就是确定該檔案是一個位元組碼檔案

這裡為什麼魔數要選擇 cafe babe,我認為是在16進制中,想用 a-f 拼接出和 java 相關的單詞,

位元組碼和類加載器位元組碼和類加載器

二、類加載機制

1、類的生命周期

位元組碼和類加載器位元組碼和類加載器

類加載的過程包括了

加載

驗證

準備

解析

初始化

五個階段。在這五個階段中,

加載

驗證

準備

初始化

這四個階段發生的順序是确定的,而

解析

階段則不一定,它在某些情況下可以在初始化階段之後開始,這是為了支援 java 的動态綁定(即反射)

2、生命周期不同階段講解

1)加載(查找并加載類的二進制資料)

在加載階段,虛拟機需要完成一下三件事:

  • 通過一個類的全限定名來擷取其定義的二進制位元組流。
  • 将這個位元組流所代表的靜态存儲結構轉化為方法區的運作時資料結構。
  • 在Java堆中生成一個代表這個類的java.lang.Class對象,作為堆方法區中這些資料的通路入口。
位元組碼和類加載器位元組碼和類加載器

加載階段是可控性最強的階段,開發人員可以使用系統提供的類加載器完成加載,也可以自定義自己的類加載器完成加載

有下述這些方法,可以加載 .class 檔案:

  • 從本地系統中直接加載
  • 通過網絡下載下傳.class檔案
  • 從zip,jar等歸檔檔案中加載.class檔案
  • 從專有資料庫中提取.class檔案
  • 将Java源檔案動态編譯為.class檔案

2)連接配接(確定被加載的類的正确性)

1- 驗證

驗證是連接配接階段的第一步,這一階段的目的是為了確定Class檔案的位元組流中包含的資訊符合目前虛拟機的要求,并且不會危害虛拟機自身的安全。

2 - 準備

準備階段是正式為類變量配置設定記憶體并設定類變量初始值的階段,這些記憶體都将在方法區中配置設定。對于該階段有以下幾點需要注意:

  • 這時候進行記憶體配置設定的僅包括類變量(

    static

    ),而不包括執行個體變量,執行個體變量會在對象執行個體化時随着對象一塊配置設定在Java堆中。
  • 這裡所設定的初始值通常情況下是資料類型預設的零值(如 、

    0L

    null

    false

    等),而不是被在Java代碼中被顯式地賦予的值。

    假設一個類變量的定義為:

    public static int value = 3

    ;那麼變量value在準備階段過後的初始值為 ,而不是

    3

    ,因為這時候尚未開始執行任何Java方法,而把value指派為3的

    put static

    指令是在程式編譯後,存放于類構造器

    <clinit>()

    方法之中的,是以把value指派為3的動作将在初始化階段才會執行。
3 - 解析

解析階段是虛拟機将常量池内的符号引用替換為直接引用的過程,解析動作主要針對

接口

字段

類方法

接口方法

方法類型

方法句柄

調用點

限定符7類符号引用進行。符号引用就是一組符号來描述目标,可以是任何字面量。

直接引用

就是直接指向目标的指針、相對偏移量或一個間接定位到目标的句柄。

3)初始化

初始化,為類的靜态變量賦予正确的初始值(連接配接過程中的準備,隻會為靜态變量賦一個0值,初始化還是要在這個階段進行的),JVM負責對類進行初始化,主要對類變量進行初始化。在Java中對類變量進行初始值設定有兩種方式:

  • 聲明類變量是指定初始值
  • 使用靜态代碼塊為類變量指定初始值
private static int a;
static {
	  a=1;
}
           

jvm 的初始化步驟:

  • 假如這個類還沒有被加載和連接配接,則程式先加載并連接配接該類
  • 假如該類的直接父類還沒有被初始化,則先初始化其直接父類
  • 假如類中有初始化語句,則系統依次執行這些初始化語句

類初始化的時機:

  • 建立類的執行個體,也就是new的方式
  • 通路某個類或接口的靜态變量,或者對該靜态變量指派
  • 調用類的靜态方法
  • 反射(如Class.forName(“com.pdai.jvm.Test”))
  • 初始化某個類的子類,則其父類也會被初始化
  • Java虛拟機啟動時被标明為啟動類的類(Java Test),直接使用java.exe指令來運作某個主類

3、類加載器

1)類加載器層次

位元組碼和類加載器位元組碼和類加載器

**BootstrapClassLoader:**由 c++/c 語言實作,嵌套在 jvm 中,程式中無法直接擷取,其隻加載 java、javax、sun 開頭的類,說白了,就是加載 jdk 自帶的包

**ExtClassLoader:**加載 jre/lib/ext 目錄下的包,說白了,就是加載我們項目中引入的第三方 jar 包

**AppClassLoader:**加載我們自己寫的類

**UserClassLoader:**使用者類加載器,類加載器可以是我們使用者自定義的,上面三個類加載器,隻能加載本地的,如果我們有加載網絡上的類的需求,我們就需要自定義加載器,實作對應功能

2)尋找類加載器

我們可以通過代碼,找尋類加載器

public class GetClassLoader {
    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);
        System.out.println(loader.getParent());
        System.out.println(loader.getParent().getParent());
    }
}
           
位元組碼和類加載器位元組碼和類加載器

3)怎麼進行類加載

1、指令行啟動應用時候由JVM初始化加載

2、通過Class.forName()方法動态加載

3、通過ClassLoader.loadClass()方法動态加載

Class.forName()和ClassLoader.loadClass()差別:

  • Class.forName(): 将類的.class檔案加載到jvm中之外,還會對類進行解釋,執行類中的static塊;
  • ClassLoader.loadClass(): 隻幹一件事情,就是将.class檔案加載到jvm中,不會執行static中的内容,隻有在newInstance才會去執行static塊。
  • Class.forName(name, initialize, loader)帶參函數也可控制是否加載static塊。并且隻有調用了newInstance()方法采用調用構造函數,建立類的對象 。

4、JVM 類加載機制

類加載機制有四個:

**

全盤負責

:**當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也将由該類加載器負責載入,除非顯示使用另外一個類加載器來載入

**

父類委托

:**先讓父類加載器試圖加載該類,隻有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類

**

緩存機制

:**緩存機制将會保證所有加載過的Class都會被緩存,當程式中需要使用某個Class時,類加載器先從緩存區尋找該Class,隻有緩存區不存在,系統才會讀取該類對應的二進制資料,并将其轉換成Class對象,存入緩存區。這就是為什麼修改了Class後,必須重新開機JVM,程式的修改才會生效

**

雙親委派機制

:**如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成,依次向上,是以,所有的類加載請求最終都應該被傳遞到頂層的啟動類加載器中,隻有當父加載器在它的搜尋範圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類。

1)雙親委派機制

雖然有四個,但是我們還是重點去了解雙親委派

1 - 雙親委派機制講解

我們的類加載器,由淺至深,分别是 AppClassLoader、ExtClassLoader、BootstrapClassLoader

雙親委派,就是說子類加載器很懶,總是會讓父類加載器去加載類,如果父類加載器也加載不到該類,子類才會去加載

這也是為什麼我們自己寫一個 java.lang.String,使用的還是 jdk 中的 String

位元組碼和類加載器位元組碼和類加載器

使用雙親委派,能夠:

  • 防止重複加載某個類:

父類加載過了,子類就不會去加載了

  • 保證了安全性:

因為Bootstrap ClassLoader在加載的時候,隻會加載JAVA_HOME中的jar包裡面的類,如java.lang.Integer,那麼這個類是不會被随意替換的,除非有人跑到你的機器上, 破壞你的JDK。

2 - 不同層次的類加載器是繼承關系嗎

不是,子類加載器和父類加載之間是 組合關系

位元組碼和類加載器位元組碼和類加載器
3 - 類加載器的實作
protected Class<?> loadClass(String var1, boolean var2) 
  throws ClassNotFoundException {
  synchronized(this.getClassLoadingLock(var1)) {
    // 檢查類加載器有沒有被加載
    Class var4 = this.findLoadedClass(var1);
    if (var4 == null) {
      long var5 = System.nanoTime();
      
      try {
        if (this.parent != null) {
          var4 = this.parent.loadClass(var1, false);
        } else {
          var4 = this.findBootstrapClassOrNull(var1);
        }
      } catch (ClassNotFoundException var10) {
      }

      // 如果還是沒有找到
      if (var4 == null) {
        long var7 = System.nanoTime();
        var4 = this.findClass(var1);
        PerfCounter.getParentDelegationTime().addTime(var7 - var5);
        PerfCounter.getFindClassTime().addElapsedTimeFrom(var7);
        PerfCounter.getFindClasses().increment();
      }
    }

    if (var2) {
      this.resolveClass(var4);
    }

    return var4;
  }
}
           
4 - 實作自己的類加載器

通過上面類加載器的實作,我們可以看到,我們隻要重寫 ClassLoader 的

findClass()

方法即可

public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 實作
    }
}


//...

// 使用
public static void main(String[] args) {
  MyClassLoader myClassLoader = new MyClassLoader();

  Class<?> aClass = myClassLoader.loadClass("<full class name>");
  // ...
}
           
5 - 怎麼破壞雙親委派

先去看看 ClassLoader 的 loadClass 方法

會發現,如果這個類在目前類加載中沒有加載的話,會丢給父加載器去加載

位元組碼和類加載器位元組碼和類加載器

了解這件事,我們就知道該怎麼破壞雙親委派了

隻要我們自己實作類加載器(繼承 ClassLoader),然後重寫

loadClass()

方法,讓它不要去使用雙親委派機制即可

然後我們就要去使用我們自己實作的這個 ClassLoader 就行了