1. 程式人生 > >Java虛擬機器 —— 類的載入機制

Java虛擬機器 —— 類的載入機制

我們知道class檔案中儲存了類的描述資訊和各種細節的資料,在執行Java程式時,虛擬機器需要先將類的這些資料載入到記憶體中,並經過校驗、轉換、解析和初始化過後,最終形成可以直接使用的Java型別。

類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入、驗證、準備、解析、初始化、使用和解除安裝7個階段。其中驗證、準備、解析3個部分統稱為連線。

類的生命週期

類的生命週期

類的載入機制實際上就是類的生命週期中載入、驗證、準備、解析、初始化5個過程。

載入

載入是類的載入過程的第一個階段,在載入階段,虛擬機器需要完成以下3件事情:

  1. 通過一個類的全限定名來獲取定義此類的二進位制位元組流;
  2. 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;
  3. 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。

通過全限定名來獲取二進位制流可以有很多種方式,比如從JAR、EAR、WAR檔案包中讀取,從網路獲取,也可以由其他檔案來生成(jsp檔案生成對應的Servlet類),甚至還可以通過執行時動態生成(Java動態代理)。

相比類載入過程的其他階段,載入階段是可控性最強的。因為開發者既可以利用系統提供的啟動類載入器來完成,也可以通過自定義類載入去完成(重寫loadClass方法,控制位元組流的獲取方式)。

關於類載入器的詳細介紹將放在文章最後。

載入階段完成後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中。然後在記憶體中例項化一個java.lang.Class類的物件,這樣就可以通過這個物件來訪問方法區中的這些資料。

驗證

驗證是連線階段的第一步,這一階段的目的是為了確保class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。驗證階段大致上會完成下面4個階段的檢驗動作:檔案格式驗證、元資料驗證、位元組碼驗證、符號引用驗證。

  • 檔案格式驗證: 驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的虛擬機器處理。該驗證階段的主要目的是保證輸入的位元組流能正確地解析並存儲於方法區之內,格式上符合描述一個Java型別資訊的要求。通過了這個階段的驗證後,位元組流才會進入記憶體的方法區中進行儲存,後面的
    3個驗證階段全部是基於方法區的儲存結構進行的,不會再直接操作位元組流。
  • 元資料驗證: 對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求。這個主要目的是對類的元資料資訊進行語義校驗,保證不存在不符合Java語言規範的元資料資訊。
  • 位元組碼驗證: 對類的方法體進行校驗分析,保證被校驗類的方法在執行時不會做出危害虛擬機器安全的事件。
  • 符號驗證: 對類自身以外(常量池中的各種符號引用)的資訊進行匹配性校驗,這個階段發生在將符號引用轉化為直接引用的時候(解析階段中發生),目的是確保解析動作能正常執行。

準備

準備階段是正式為類變數(靜態變數)分配記憶體並設定初始值的階段,這些類變數所使用的記憶體都將在方法區中進行分配。

這裡有兩點需要注意:

  1. 成員變數不是在這裡分配記憶體的,成員變數是在類例項化物件的時候在堆中分配的。
  2. 這裡設定初始值是指型別的零值(比如0,null,false等),而不是程式碼中被顯示的賦予的值。

比如:

public class Test {
    public int number = 111;
    public static int sNumber = 111; 
}

成員變數number在這個階段就不會進行記憶體分配和初始化。而類變數sNunber會在方法區中分配記憶體,並設定為int型別的零值0而不是111,賦值為111是在初始化階段才會執行。

Java基本資料型別和引用資料型別零值

Java基本資料型別和引用資料型別零值


但是呢,如果類變數如果是被final修飾,為靜態常量,那麼在準備階段也會在方法區中分配記憶體,並且將其值設定為顯示賦予的值。

比如:

public class Test {
    public static final int NUMBER = 111; 
}

此時,就會在準備階段將NUMBER的值設定為111。

解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。

  • 符號引用: 符號引用以一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。
  • 直接引用: 直接引用可以是直接指向目標的指標、相對偏移量或是一個能間接定位到目標的控制代碼。

解析動作主要就是在常量池中尋找類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼、呼叫點限定符等7類符號引用,把這些符號引用替換為直接引用。下面主要介紹下類或介面、欄位、類方法、介面方法的解析:

  1. 類或介面解析: 假設當前的類A通過符號X引用了類B,虛擬機器會把代表類B的全限定名傳遞給A的類載入器去載入BB經過載入、驗證、準備過程,在解析過程又可能會觸發B引用的其他的類的載入過程,相當於一個類引用鏈的遞迴載入過程,整個過程只要不出現異常,B的就是一個載入成功的類或介面了,也就是可以獲取到代表Bjava.lang.Class物件。在驗證了A具備對B的訪問許可權後,就將符號引用X替換為B的直接引用。
  2. 欄位解析: 解析未被解析過的欄位,要先解析欄位所屬的類或介面的符號引用。如果類本身就包含了簡單的名稱和欄位描述與目標欄位相匹配,就直接返回這個欄位引用;如果實現了介面,將會按照繼承關係從下往上遞迴搜尋各個介面和它的父介面,如果介面中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位;如果是繼承自其他類的話,將會按照繼承關係從下往上遞迴搜尋其父類,如果在父類中包含了簡單名稱和欄位描述符都與目標相匹配的欄位,則返回這個欄位的直接引用。
  3. 類方法解析:類方法解析和欄位解析的方式類似,也是依據繼承和實現關係從小到上搜索,只不過是先搜尋類,後搜尋介面。如果有簡單名稱和欄位描述符都與目標相匹配的欄位,就返回欄位引用。
  4. 介面的方法解析: 與類方法解析類似,從小到上搜索介面(介面沒有父類,只可能有父介面)。如果存在簡單名稱和欄位描述符都與目標相匹配的欄位,就返回欄位引用。

初始化

類的初始化類載入過程的最後一步,在前面的過中,除了在載入階段開發者可以自定義載入器之外,其餘的動作都是完全有虛擬機器主導和控制完成。到了初始化階段,才真正開始執行類中定義的Java程式碼。

在準備階段,類變數已經設定了系統要求的零值,而在初始化階段,則根據程式設計師通過程式制定的主觀計劃去初始化類變數和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。

<clinit>()方法是由編譯器自動收集類中所有的類變數(static變數)和靜態程式碼塊(static{}塊)中的語句合併生成的。編譯器收集的順序是由語句在原始檔中出現的順序所決定的,靜態程式碼塊中只能訪問到定義在靜態程式碼塊之前的變數,定義在它之後的變數,在前面的靜態程式碼塊可以賦值,但是不能訪問。

public class Test {
    static {
        number = 111;               // 可以賦值
        System.out.println(number); // 不能讀取,編輯器或報錯Illegal forward reference
    }
    static int number;
}

<clinit>()方法與類的建構函式(或者說例項構造器<init>()方法)不同,它不需要顯式地呼叫父類的<clinit>()方法,虛擬機器會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法已經執行完畢。所以,父類定義的靜態程式碼塊要先與子類的賦值操作。

class Parent {
    public static int A = 1;
    static {
        A = 2;
    }
}

class Sub extends Parent {
    public static int B = A;
    public static void main(String[] args) {
        System.out.println(Sub.B);
    }
}

<clinit>()方法對於類或介面來說並不是必需的,如果一個類中沒有靜態語句塊,也沒有對變數的賦值操作,那麼編譯器可以不為這個類生成<clinit>()方法。

介面中不能使用靜態語句塊,但仍然有變數初始化的賦值操作,因此介面與類一樣都會生成<clinit>()方法。但介面與類不同的是,執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法。只有當父介面中定義的變數使用時,父接口才會初始化。另外,介面的實現類在初始化時也一樣不會執行介面的<clinit>()方法。

虛擬機器會保證一個類的<clinit>()方法在多執行緒環境中被正確地加鎖、同步,如果多個執行緒同時去初始化一個類,那麼只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行<clinit>()方法完畢。如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個程序阻塞。

類載入器

在之前的載入過程中,提到了類載入器通過一個類的全限定名來獲取描述此類的二進位制位元組流,這個過程可以讓開發中自定義類載入器來決定如何獲取需要的位元組流。那麼,什麼是類載入器呢?

對於任意一個Java類,都必須通過類載入器載入到方法區,並生成java.lang.Class物件才能使用類的各個功能,所以我們可以把類載入器理解為一個將class類檔案轉換為java.lang.Class物件的工具。

對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在Java虛擬機器中的唯一性,每一個類載入器,都擁有一個獨立的類名稱空間。也就是說,如果兩個類“相等”,那麼這兩個類必須是被同一個虛擬機器中的同一個類載入器載入,並且來自同一個class檔案。

在Java當中,已經有3個預製的類載入器,分別是BootStrapClassLoaderExtClassLoader、AppClassLoader

  • BootStrapClassLoader: 啟動類載入器,它是由C++來實現的,在Java程式中不能顯氏的獲取到。它負責載入存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下的類。
  • ExtClassLoader: 擴充套件類載入器,它是由sun.misc.Launcher$ExtClassLoader實現,負責載入JDK\jre\lib\ext目錄中,或者由java.ext.dirs系統變數指定的路徑中的所有類庫。開發者可以直接使用它。
  • AppClassLoader: 應用程式類載入器,由sun.misc.Launcher$AppClassLoader來實現,它負責載入使用者類路徑(ClassPath)所指定的類,開發者可以直接使用該類載入器。一般來說,開發者自定義的類就是由應用程式類載入器載入的。

ExtClassLoader作為類載入器,但它也是一個Java類,是由BootStrapClassLoader來載入的,所以,ExtClassLoader的parent是BootStrapClassLoader。但是由於BootStrapClassLoaderc++實現的,我們通過ExtClassLoader.getParent獲取到的是null。同樣地,AppClassLoader是由ExtClassLoader載入,AppClassLoader的parent是ExtClassLoader

public class Test {
    public static void main(String[] args) {
        ClassLoader cl = Test.class.getClassLoader();
        while (cl != null) {
            System.out.println(cl);
            cl = cl.getParent();
        }
    }
}

列印結果:

[email protected]
[email protected]

同時我們可以定義自己的類載入器CustomClassLoader,那麼它的parent肯定就是AppClassLoader了。類載入器的這種層次關係稱為雙親委派模型。

類載入器

類載入器

雙親委派模型

雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器。這裡類載入器之間的父子關係不是以繼承的關係來實現,而是都使用遞迴的方式來呼叫父載入器的程式碼。

雙親委派模型的工作過程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去載入。

ClassLoader的原始碼:

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
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                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;
    }
}

先檢查是否已經被載入過,若沒有載入則呼叫父類載入器的loadClass()方法,依次向上遞迴。若父類載入器為空則說明遞迴到啟動類載入器了。如果從父類載入器到啟動類載入器的上層次的所有載入器都載入失敗,則呼叫自己的findClass()方法進行載入。

使用雙親委派模型能使Java類隨著載入器一起具備一種優先順序的層次關係,保證同一個類只加載一次,避免了重複載入,同時也能阻止有人惡意替換載入系統類。

自定義類載入器

一般地,在ClassLoader方法的loadClass方法中已經給開發者實現了雙親委派模型,在自定義類載入器的時候,只需要複寫findClass方法即可。

public class CustomClassLoader extends ClassLoader {

    private String root;

    public CustomClassLoader(String root) {
        this.root = root;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String name) {
        String fileName = root + File.separatorChar
                + name.replace('.', File.separatorChar)
                + ".class";
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

新建一個類com.xiao.U,編譯成class檔案,放到桌面,來測試一下:

public class Test {
    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\PC\\Desktop");
        try {
            Class clazz = customClassLoader.loadClass("com.xiao.U");
            Object o = clazz.newInstance();
            System.out.println(o.getClass().getClassLoader());
        } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }
}

列印結果:

[email protected]

自定義類載入器在可以實現服務端的熱部署,在移動端比如android也可以實現熱更新。

我有一個微信公眾號,經常會分享一些Java技術相關的乾貨;如果你喜歡我的分享,可以用微信搜尋“Java團長”或者“javatuanzhang”關注。

相關推薦

java虛擬機器載入機制學習

1、什麼是類的載入 類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構。類的載入的最終產品是位於堆區中的Class物件,Class物件封裝了類在方法區內的資料結構

Java虛擬機器載入機制經典案例

package io.lgxkdream.test; class Father { static Father f = new Father(); static { System.out.println("father-1"); } { System.out.println("

jdk原始碼解析(七)——Java虛擬機器載入機制

前面我們講解了class檔案的格式,以及它是什麼樣的。那麼接下來需要了解它怎麼被載入到jvm中呢?jvm的載入機制又是怎麼一個過程呢?本文參考了《Java 虛擬機器規範(Java SE 7 版)》的第五章內容來詳細解釋一下 虛擬機器類載入機制:虛擬機器把描述類的資料從cla

Java 虛擬機器載入機制

看到這個題目,很多人會覺得我寫我的java程式碼,至於類,JVM愛怎麼載入就怎麼載入,博主有很長一段時間也是這麼認為的。隨著程式設計經驗的日積月累,越來越感覺到了解虛擬機器相關要領的重要性。閒話不多說,老規矩,先來一段程式碼吊吊胃口。public class SSClass{

Java虛擬機器載入機制

原文出處:http://www.importnew.com/18548.html類載入過程類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的整個生命週期包括:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolut

JVM(三)-java虛擬機器載入機制

概述:   上一篇文章,介紹了java虛擬機器的執行時區域,Java虛擬機器根據不同的分工,把記憶體劃分為各個不同的區域。在java程式中,最小的執行單元一般都是建立一個物件,然後呼叫物件的某個 方法。通過上一篇文章我們知道呼叫某個方法是通過虛擬機器棧的棧幀並通過執行引擎來實現的,但是實際上一個方法的執行前提

《深入理解Java虛擬機器》個人讀書總結——虛擬機器載入機制

我們都知道Java虛擬機器是用來執行我們編譯好的.class檔案的,class檔案中夾帶類的各種資訊,虛擬機器要執行這些檔案,第一件事就是要載入到虛擬機器中,這就引出了這次總結的問題——虛擬機器是如何載入這些class檔案的?載入後虛擬機器是怎麼處理檔案中夾帶的資訊的? 類載入機制

JAVA虛擬機器(七)虛擬機器載入機制

虛擬機器的類載入機制是指 把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗,轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。類的載入連線和初始化過程都是在程式執行期間完成的。 類的生命週期: 載入->連線(驗證,準備,解析)->初始化->使用

深入理解Java虛擬機器筆記——虛擬機器載入機制

虛擬機器類載入機制 類載入機制:虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗、  轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。 在Java中,型別的載入和連線過程都是在程式執行期間完成的。   類載入時機(類從載入到虛擬

讀書筆記 ---- 《深入理解Java虛擬機器》---- 第6篇:虛擬機器載入機制

上一篇:類檔案結構:https://blog.csdn.net/pcwl1206/article/details/84197219 第6篇:虛擬機器類載入機制 1、概述 上一篇文章中講訴了Class檔案儲存格式的具體細節,在Class檔案中的描述的各種資訊,最終都要載入到虛擬機器中之後才

讀薄《深入理解 Java 虛擬機器虛擬機器載入機制

#虛擬機器類載入機制 類被載入的生命週期包括 載入→驗證→準備→解析→初始化→使用→解除安裝 解析階段在某些情況下可以在初始化階段之後開始,這是為了支援 Java 語言的執行時繫結。 虛擬機器規範嚴格規定了有且只有 5 種情況必須立即對類進行初始化。 遇到 n

深入理解java虛擬機器---4虛擬機器載入機制

類載入的整個生命週期:    載入、連線(驗證、準備、解析)、初始化、使用、解除安裝。 載入:      class檔案中的二進位制資料讀取到記憶體中,然後將該位元組流所代表的靜態資料結構轉化為方法區中執行的資料結構,並且在堆記憶體中生成一個java.lang.Class物

詳解Java記憶體區域?虛擬機器載入機制

一、Java執行時資料區域 1、程式計數器 “執行緒私有”的記憶體,是一個較小的記憶體空間,它可以看做當前執行緒所執行的位元組碼的行號指示器。Java虛擬機器規範中唯一一個沒有OutOfMemoryError情況的區域。 位元組碼直譯器工作時就說通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,

深入理解Java虛擬機器學習筆記——三、虛擬機器載入機制

1、概述 虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成能夠被虛擬機器直接使用的資料型別,這就是虛擬機器的類載入機制。在Java中,類的載入、校驗、解析和初始化都是在執行期間完成的。 2、類載入的時機 類從被載入都虛擬機器記

深入理解JAVA虛擬機器(四):虛擬機器載入機制

虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。在Java語言裡面,型別的載入、連線和初始化過程都是在程式執行期間完成的。 1、類載入的時機 類從被載入到虛擬機

Java記憶體區域與虛擬機器載入機制

一、Java執行時資料區域 1、程式計數器  “執行緒私有”的記憶體,是一個較小的記憶體空間,它

深入理解JAVA虛擬機器讀書筆記----虛擬機器載入機制

概述 虛擬機器類載入機制:虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。 不像C語言,寫好程式碼後,編譯-》連結-》執行;Java語言裡,型別的載入和連線過程是在程式執行

Java虛擬機器載入器及雙親委派機制

所謂的類載入器(Class Loader)就是載入Java類到Java虛擬機器中的,前面《面試官,不要再問我“Java虛擬機器類載入機制”了》中已經介紹了具體載入class檔案的機制。本篇文章我們重點介紹載入器和雙親委派機制。 類載入器 在JVM中有三類ClassLoader構成:啟動類(或根類)載入器(Bo

JVM的虛擬機器載入機制

首先,我們要知道為什麼會存在類的載入機制。Java語言編寫的.java在經過編譯器編譯後會生成.class檔案,這個和C\C++語言是不一樣的。 C語言它們是會被編譯生成為本地機器碼,然後在被執行。這種做法的缺點就是無法完成編寫程式碼的跨平臺使用。想想就知道,windows下編譯好的程式碼在li

深入理解JVM虛擬機器讀書筆記【第七章】虛擬機器載入機制

7.1 概述 7.2 類載入的時機 7.3 類載入的過程 7.3.1 載入 7.3.2 驗證 1.檔案格式驗證 2.元資料驗證 3.位元組碼驗證