JVM學習筆記(八):類加載

1 來源

  • 來源:《Java虛擬機 JVM故障診斷與性能優化》——葛一鳴
  • 章節:第十章

本文是第十章的一些筆記整理。java

2 概述

本文主要講述了類加載器以及類加載的詳細流程。數組

3 類加載流程

類加載的流程能夠簡單分爲三步:安全

  • 加載
  • 鏈接
  • 初始化

而其中的鏈接又能夠細分爲三步:性能優化

  • 驗證
  • 準備
  • 解析

下面會分別對各個流程進行介紹。bash

3.1 類加載條件

在瞭解類接在流程以前,先來看一下觸發類加載的條件。網絡

JVM不會無條件加載類,只有在一個類或接口在初次使用的時候,必須進行初始化。這裏的使用是指主動使用,主動使用包括以下狀況:數據結構

  • 建立一個類的實例的時候:好比使用new建立,或者使用反射、克隆、反序列化
  • 調用類的靜態方法的時候:好比使用invokestatic指令
  • 使用類或接口的靜態字段:好比使用getstatic/putstatic指令
  • 使用java.lang.reflect中的反射類方法時
  • 初始化子類時,要求先初始化父類
  • 含有main()方法的類

除了以上狀況外,其餘狀況屬於被動使用,不會引發類的初始化。多線程

好比下面的例子:ide

public class Main {
    public static void main(String[] args){
        System.out.println(Child.v);
    }
}

class Parent{
    static{
        System.out.println("Parent init");
    }
    public static int v = 100;
}

class Child extends Parent{
    static {
        System.out.println("Child init");
    }
}

輸出以下:函數

Parent init
100

而加上類加載參數-XX:+TraceClassLoading後,能夠看到Child確實被加載了:

[0.068s][info   ][class,load] com.company.Main
[0.069s][info   ][class,load] com.company.Parent
[0.069s][info   ][class,load] com.company.Child
Parent init
100

可是並無進行初始化。另一個例子是關於final的,代碼以下:

public class Main {
    public static void main(String[] args){
        System.out.println(Test.STR);
    }
}

class Test{
    static{
        System.out.println("Test init");
    }
    public static final String STR = "Hello";
}

輸出以下:

[0.066s][info   ][class,load] com.company.Main
Hello

Test類根本沒有被加載,由於final被作了優化,編譯後的Main.class中,並無引用Test類:

0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc           #4                  // String Hello
5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V

在字節碼偏移3的位置,經過ldc將常量池第4項入棧,此時在字節碼文件中常量池第4項爲:

#3 = Class              #24            // com/company/Test
#4 = String             #25            // Hello
#5 = Methodref          #26.#27        // java/io/PrintStream.println:(Ljava/lang/String;)V

所以並無對Test類進行加載,只是直接引用常量池中的常量,所以輸出沒有Test的加載日誌。

3.2 加載

類加載的時候,JVM必須完成如下操做:

  • 經過類的全名獲取二進制數據流
  • 解析類的二進制數據流爲方法區內的數據結構
  • 建立java.lang.Class類的實例,表示該類型

第一步獲取二進制數據流,途徑有不少,包括:

  • 字節碼文件
  • JAR/ZIP壓縮包
  • 從網絡加載

等等,獲取到二進制數據流後,JVM進行處理並轉化爲一個java.lang.Class實例。

3.3 驗證

驗證的操做是確保加載的字節碼是合法、合理而且規範的。步驟簡略以下:

在這裏插入圖片描述

  • 格式檢查:判斷二進制數據是否符合格式要求和規範,好比是否以魔數開頭,主版本號和小版本號是否在當前JVM支持範圍內等等
  • 語義檢查:好比是否全部類都有父類存在,一些被定義爲final的方法或類是否被重載了或者繼承了,是否存在不兼容方法等等
  • 字節碼驗證:會試圖經過對字節碼流的分析,判斷字節碼是否能夠正確被執行,好比是否會跳轉到一條不存在的指令,函數調用是否傳遞了正確的參數等等,可是卻沒法100%判斷一段字節碼是否能夠被安全執行,只是儘量檢查出能夠預知的明顯問題。若是沒法經過檢查,則不會加載這個類,若是經過了檢查,也不能說明這個類徹底沒有問題
  • 符號引用驗證:檢查類或方法是否確實存在,而且肯定當前類有沒有權限訪問這些數據,好比沒法找到一個類就拋出NoClassDefFoundError,沒法找到方法就拋出NoSuchMethodError

3.4 準備

類經過驗證後,就會進入準備階段,在這個階段,JVM爲會類分配相應的內存空間,並設置初始值,好比:

  • int初始化爲0
  • long初始化爲0L
  • double初始化爲0f
  • 引用初始化爲null

若是存在常量字段,那麼這個階段也會爲常量賦值。

3.5 解析

解析就是將類、接口、字段和方法的符號引用轉爲直接引用。符號引用就是一些字面量引用,和JVM的內存數據結構和內存佈局無關,因爲在字節碼文件中,經過常量池進行了大量的符號引用,這個階段就是將這些引用轉爲直接引用,獲得類、字段、方法在內存中的指針或直接偏移量。

另外,因爲字符串有着很重要的做用,JVMString進行了特別的處理,直接使用字符串常量時,就會在類中出現CONSTANT_String,而且會引用一個CONSTANT_UTF8常量項。JVM運行時,內部的常量池中會維護一張字符串拘留表(intern),會保存其中出現過的全部字符串常量,而且沒有重複項。使用String.intern()能夠得到一個字符串在拘留表的引用,好比下面代碼:

public static void main(String[] args){
    String a = 1 + String.valueOf(2) + 3;
    String b = "123";
    System.out.println(a.equals(b));
    System.out.println(a == b);
    System.out.println(a.intern() == b);
}

輸出:

true
false
true

這裏b就是常量自己,所以a.intern()返回在拘留表的引用後就是b自己,比較結果爲真。

3.6 初始化

初始化階段會執行類的初始化方法<clint><clint>是由編譯期生成的,由靜態成員的賦值語句以及static語句共同產生。

另外,加載一個類的時候,JVM老是會試圖加載該類的父類,所以父類的<clint>方法老是在子類的<clint>方法以前被調用。另外一方面,須要注意的是<clint>會確保在多線程環境下的安全性,也就是多個線程同時初始化同一個類時,只有一個線程能夠進入<clint>方法,換句話說,在多線程下可能會出現死鎖,好比下面代碼:

package com.company;

import java.util.concurrent.TimeUnit;

public class Main extends Thread{
    private char flag;
    public Main(char flag){
        this.flag = flag;
    }
    
    public static void main(String[] args){
        Main a = new Main('A');
        a.start();
        Main b = new Main('B');
        b.start();
    }

    @Override
    public void run() {
        try{
            Class.forName("com.company.Static"+flag);
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
    }
}

class StaticA{
    static {
        try {
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        try{
            Class.forName("com.company.StaticB");
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        System.out.println("StaticA init ok");
    }
}

class StaticB{
    static {
        try {
            TimeUnit.SECONDS.sleep(1);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
        try{
            Class.forName("com.company.StaticA");
        }catch (ClassNotFoundException e){
            e.printStackTrace();
        }
        System.out.println("StaticB init ok");
    }
}

在加載StaticA的時候嘗試加載StaticB,可是因爲StaticB已經被加載中,所以加載StaticA的線程會阻塞在Class.forName("com.company.StaticB")處,同理加載StaticB的線程會阻塞在Class.forName("com.company.StaticA")處,這樣就出現死鎖了。

4 ClassLoader

4.1 ClassLoader簡介

ClassLoader是類加載的核心組件,全部的Class都是由ClassLoader加載的,ClassLoader經過各類各樣的方式將Class信息的二進制數據流讀入系統,而後交給JVM進行鏈接、初始化等操做。所以ClassLoader負責類的加載流程,沒法經過ClassLoader改變類的鏈接和初始化行爲。

ClassLoader是一個抽象類,提供了一些重要接口定義加載流程和加載方式,主要方法以下:

  • public Class<?> loadClass(String name) throws ClassNotFoundException:給定一個類名,加載一個類,返回這個類的Class實例,找不到拋出異常
  • protected final Class<?> defineClass(byte[] b, int off, int len):根據給定字節流定義一個類,offlen表示在字節數組中的偏移和長度,這是一個protected方法,在自定義子類中才能使用
  • protected Class<?> findClass(String name) throws ClassNotFoundException:查找一個類,會在loadClass中被調用,用於自定義查找類的邏輯
  • protected Class<?> findLoadedClass(String name):尋找一個已經加載的類

4.2 類加載器分類

在標準的Java程序中,JVM會建立3類加載器爲整個應用程序服務,分別是:

  • 啓動類加載器:Bootstrap ClassLoader
  • 擴展類加載器:Extension ClassLoader
  • 應用類加載器(也叫系統類加載器):App ClassLoader

另外,在程序中還能夠定義本身的類加載器,從整體看,層次結構以下:

在這裏插入圖片描述

通常來講各個加載器負責的範圍以下:

  • 啓動類加載器:負責加載系統的核心類,好比rt.jar包中的類
  • 擴展類加載器:負責加載lib/ext/*.jar下的類
  • 應用類加載器:負責加載用戶程序的類
  • 自定義加載器:加載一些特殊途徑的類,通常是用戶程序的類

4.3 雙親委派

默認狀況下,類加載使用雙親委派加載的模式,具體來講,就是類在加載的時候,會判斷當前類是否已經被加載,若是已經被加載,那麼直接返回已加載的類,若是沒有,會先請求雙親加載,雙親也是按照同樣的流程先判斷是否已加載,若是沒有在此委託雙親加載,若是雙親加載失敗,則會本身加載。

在這裏插入圖片描述

在上圖中,應用類加載器的雙親爲擴展類加載器,擴展類加載器的雙親爲啓動類加載器,當系統須要加載一個類的時候,會先從底層類加載器開始進行判斷,當須要加載的時候會從頂層開始加載,依次向下嘗試直到加載成功。

在全部加載器中,啓動類加載器是最特別的,並非使用Java語言實現,在Java中沒有對象與之相對應,系統核心類就是由啓動類加載器進行加載的。換句話說,若是嘗試在程序中獲取啓動類加載器,獲得的值是null

System.out.println(String.class.getClassLoader() == null);

輸出結果爲真。

相關文章
相關標籤/搜索