Java虛擬機類加載機制

類從被加載到虛擬機內存開始,到卸載出內存爲止,它的整個生命週期包括:加載,驗證,準備,解析,初始化,使用和卸載7個階段。其中驗證、準備、解析3個部分統稱爲連接,這7個階段的發生順序如圖:
這裏寫圖片描述

加載

這個流程中的加載是類加載機制中的一個階段,這兩個概念不要混淆,這個階段需要完成的事情有:

1.通過一個類的全限定名來獲取定義此類的二進制字節流。

2.將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。

3.在java堆中生成一個代表這個類的java.lang.Class對象,作爲訪問方法區中這些數據的入口。

由於第一點沒有指明從哪裏獲取以及怎樣獲取類的二進制字節流,所以這一塊區域留給我開發者很大的發揮空間。這個我在後面的類加載器中在進行介紹。

驗證

這一階段的主要目的是爲了確保Class文件的字節流中包含的信息是否符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。(由於Class文件不都是從Java源碼編譯而來的,可以使用任意途徑產生),如果輸入的字節流Class 文件的約束,虛擬機就應拋出一個java.lang.VerifyError異常或其子類異常。驗證包括:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。

準備

準備階段是正式爲類變量分配內存並設置類變量的初始值階段,即在方法區中分配這些變量所使用的內存空間。注意這裏所說的初始值概念,比如一個類變量定義爲:

public static int v = 8080;

實際上變量v在準備階段過後的初始值爲0而不是8080,將v賦值爲8080的putstatic指令是程序被編譯後,存放於類構造器方法之中,這裏我們後面會解釋。
但是注意如果聲明爲:

public static final int v = 8080;

在編譯階段會爲v生成ConstantValue屬性,在準備階段虛擬機會根據ConstantValue屬性將v賦值爲8080。

解析

解析階段是指虛擬機將常量池中的符號引用替換爲直接引用的過程。符號引用就是class文件中的:

CONSTANT_Class_info
CONSTANT_Field_info
CONSTANT_Method_info
等類型的常量。

下面我們解釋一下符號引用和直接引用的概念:

符號引用與虛擬機實現的佈局無關,引用的目標並不一定要已經加載到內存中。各種虛擬機實現的內存佈局可以各不相同,但是它們能接受的符號引用必須是一致的,因爲符號引用的字面量形式明確定義在Java虛擬機規範的Class文件格式中。
直接引用可以是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。如果有了直接引用,那引用的目標必定已經在內存中存在。

初始化

初始化階段是類加載最後一個階段,前面的類加載階段之後,除了在加載階段可以自定義類加載器以外,其它操作都由JVM主導。到了初始階段,纔開始真正執行類中定義的Java程序代碼。

初始化階段是執行類構造器方法的過程。方法是由編譯器自動收集類中的類變量的賦值操作靜態語句塊中的語句合併而成的。虛擬機會保證方法執行之前,父類的方法已經執行完畢。p.s: 如果一個類中沒有對靜態變量賦值也沒有靜態語句塊,那麼編譯器可以不爲這個類生成()方法。

注意以下幾種情況不會執行類初始化

  • 通過子類引用父類的靜態字段,只會觸發父類的初始化,而不會觸發子類的初始化。
  • 定義對象數組,不會觸發該類的初始化。
  • 常量在編譯期間會存入調用類的常量池中,本質上並沒有直接引用定義常量的類,不會觸發定義常量所在的類。
  • 通過類名獲取Class對象,不會觸發類的初始化。
  • 通過Class.forName加載指定類時,如果指定參數initialize爲false時,也不會觸發類初始化,其實這個參數是告訴虛擬機,是否要對類進行初始化。
  • 通過ClassLoader默認的loadClass方法,也不會觸發初始化動作。

類加載器:
1.引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自 java.lang.ClassLoader。它負責將<Java_Runtime_Home>/lib下面的核心類庫或-Xbootclasspath選項指定的jar包加載到內存中。由於引導類加載器涉及到虛擬機本地實現細節,開發者無法直接獲取到啓動類加載器的引用,所以不允許直接通過引用進行操作

2.擴展類加載器(extensions class loader):該類加載器在此目錄裏面查找並加載 Java 類。擴展類加載器是由Sun的 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實現的。它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。它負責將< Java_Runtime_Home >/lib/ext或者由系統變量-Djava.ext.dirs指定位置中的類庫加載到內存中。開發者可以直接使用標準擴展類加載器

3.系統類加載器(system class loader):系統類加載器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實現的。它負責將系統類路徑java -classpath-Djava.class.path變量所指的目錄下的類庫加載到內存中。開發者可以直接使用系統類加載器。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它

這些類加載器之間的關係,稱爲類加載器的雙親委派模型:
這裏寫圖片描述
這裏寫圖片描述

在這裏,需要着重說明的是,JVM在加載類時默認採用的是雙親委派機制。通俗的講,就是某個特定的類加載器在接到加載類的請求時,首先將加載任務委託給父加載器依次遞歸,如果父加載器可以完成類加載任務,就成功返回;只有父加載器無法完成此加載任務時,才自己去加載。

好處:java類隨着它的類加載器一起具備了一種帶有優先級的層次關係。例如類java.lang.Object,它存放在rt.jar中,無論哪個類加載器要加載這個類,最終都會委派給啓動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。相反,如果用戶自己寫了一個名爲java.lang.Object的類,並放在程序的Classpath中,那系統中將會出現多個不同的Object類,java類型體系中最基礎的行爲也無法保證,應用程序也會變得一片混亂。

接下來看看ClassLoader的源碼實現:

public Class<?> loadClass(String name)throws ClassNotFoundException {
            return loadClass(name, false);
    }

    protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
            // 首先判斷該類型是否已經被加載
            Class c = findLoadedClass(name);
            if (c == null) {
                //如果沒有被加載,就委託給父類加載或者委派給啓動類加載器加載
                try {
                    if (parent != null) {
                         //如果存在父類加載器,就委派給父類加載器加載
                        c = parent.loadClass(name, false);
                    } else {
                    //如果不存在父類加載器,就檢查是否是由啓動類加載器加載的類,通過調用本地方法native Class findBootstrapClass(String name)
                        c = findBootstrapClass0(name);
                    }
                } catch (ClassNotFoundException e) {
                 // 如果父類加載器和啓動類加載器都不能完成加載任務,才調用自身的加載功能
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }

最後來說一下自定義加載器:

網上的大部分自定義類加載器文章,幾乎都是貼一段實現代碼,然後分析一兩句自定義ClassLoader的原理。但是我覺得首先得把爲什麼需要自定義加載器這個問題搞清楚,因爲如果不明白它的作用的情況下,還要去學習它顯然是很讓人困惑的。

首先介紹自定義類的應用場景

(1)加密:Java代碼可以輕易的被反編譯,如果你需要把自己的代碼進行加密以防止反編譯,可以先將編譯後的代碼用某種加密算法加密,類加密後就不能再用Java的ClassLoader去加載類了,這時就需要自定義ClassLoader在加載類的時候先解密類,然後再加載。

(2)從非標準的來源加載代碼:如果你的字節碼是放在數據庫、甚至是在雲端,就可以自定義類加載器,從指定的來源加載類。

(3)以上兩種情況在實際中的綜合運用:比如你的應用需要通過網絡來傳輸 Java 類的字節碼,爲了安全性,這些字節碼經過了加密處理。這個時候你就需要自定義類加載器來從某個網絡地址上讀取加密後的字節代碼,接着進行解密和驗證,最後定義出在Java虛擬機中運行的類。

自定義一個類加載器,需要繼承ClassLoader類,並實現findClass方法。其中defineClass方法可以把二進制流字節組成的文件轉換爲一個java.lang.Class(只要二進制字節流的內容符合Class文件規範)

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class MyClassLoader extends ClassLoader
{
    public MyClassLoader()
    {

    }

    public MyClassLoader(ClassLoader parent)
    {
        super(parent);
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException
    {
        File file = new File("D:/People.class");
        try{
            byte[] bytes = getClassBytes(file);
            //defineClass方法可以把二進制流字節組成的文件轉換爲一個java.lang.Class
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        } 
        catch (Exception e)
        {
            e.printStackTrace();
        }

        return super.findClass(name);
    }

    private byte[] getClassBytes(File file) throws Exception
    {
        // 這裏要讀入.class的字節,因此要使用字節流
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);

        while (true){
            int i = fc.read(by);
            if (i == 0 || i == -1)
            break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }
}


//主函數中
MyClassLoader mcl = new MyClassLoader();   
Class<?> clazz = Class.forName("People", true, mcl);   
Object obj = clazz.newInstance();  

System.out.println(obj);  
System.out.println(obj.getClass().getClassLoader());//打印出我們的自定義類加載器  
發佈了38 篇原創文章 · 獲贊 47 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章