類的加載過程詳解


概述

在Java中數據類型分為基本數據類型和引用數據類型。基本數據類型由虛擬機預先定義,引用數據類型則需要進行類的加載

按照Java虛擬機規范,從class文件到加載到內存中的類,到類卸載出內存為止,它的整個生命周期包括如下7個階段:

其中,驗證、准備、解析3個部分統稱為鏈接(Linking)

從程序中類的使用過程看:

過程一:Loading(加載)階段

加載完成的操作

加載的理解

所謂加載,簡而言之就是將Java類的乳節碼文件加載到機器內存中,並在內存中構建出Java類的原型——類模板對象。所謂類模板對象,其實就是Java類在JVM內存中的一個快照,JVM將從字節碼文件中解析出的常量池、類字段、類方法等信息存儲到類模板中,這樣JVM在運行期便能通過類模板而獲取Java類中的任意信息,能夠對Java類的成員變量進行遍歷,也能進行Java方法的調用。

反射的機制即基於這一基礎。如果JVM沒有將Java類的聲明信息存儲起來,則JVM在運行期也無法反射。

加載完成的操作

加載階段,簡言之,查找並加載類的二進制數據,生成Class的實例。
在加載類時,Java虛擬機必須完成以下3件事情:

  • 通過類的全名,獲取類的二進制數據流。
  • 解析類的二進制數據流為方法區內的數據結構(Java類模型)
  • 創建java.lang.Class類的實例,表示該類型。作為方法區這個類的各種數據的訪問入口

二進制流的獲取方式

對於類的二進制數據流,虛擬機可以通過多種途徑產生或獲得。(只要所讀取的字節碼符合JVM規范即可)

  • 虛擬機可能通過文件系統讀入一個class后綴的文件(最常見)
  • 讀入jar、zip等歸檔數據包,提取類文件
  • 事先存放在數據庫中的類的二進制數據
  • 使用類似於HTTP之類的協議通過網絡進行加載
  • 在運行時生成一段Class的二進制信息等

在獲取到類的二進制信息后,Java虛擬機就會處理這些數據,並最終轉為一個java.lang.Class的實例。

如果輸入數據不是ClassFile的結構,則會拋出ClassFormatError。(比如如果不是cafebabe開頭,就會拋出ClassFormatError)

類模型與Class實例的位置

類模型的位置

加載的類在JVM中創建相應的類結構,類結構會存儲在方法區(JDK1.8之前:永久代;JDK1.8及之后:元空間)

Class實例的位置

類將.class文件加載至元空間后,會在堆中創建一個Java.lang.Class對象,用來封裝類位於方法區內的數據結構,該Class對象是在加載類的過程中創建的,每個類都對應有一個Class類型的對象。(instanceKlass → mirror : Class的實例)

外部可以通過訪問代表Order類的Class對象來獲取Order的類數據結構

圖示

再說明

Class類的構造方法是私有的,只有JVM能夠創建。

java.lang.Class實例是訪問類型元數據的接口,也是實現反射的關鍵數據、入口。通過Class類提供的接口,可以獲得目標類所關聯的.class文件中具體的數據結構:方法、字段等信息。

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 * 過程一:加載階段
 * 通過Class類,獲得了java.lang.String類的所有方法信息,並打印方法訪問標識符、描述符
 */
public class LoadingTest {
    public static void main(String[] args) {
        try {
            Class clazz = Class.forName("java.lang.String");
            // 獲取當前運行時類聲明的所有方法
            Method[] ms = clazz.getDeclaredMethods();
            for (Method m : ms) {
                // 獲取方法的修飾符
                String mod = Modifier.toString(m.getModifiers());
                System.out.print(mod + " ");
                // 獲取方法的返回值類型
                String returnType = m.getReturnType().getSimpleName();
                System.out.print(returnType + " ");
                // 獲取方法名
                System.out.print(m.getName() + "(");
                // 獲取方法的參數列表
                Class<?>[] ps = m.getParameterTypes();
                if (ps.length == 0) System.out.print(')');
                for (int i = 0; i < ps.length; i++) {
                    char end = (i == ps.length - 1) ? ')' : ',';
                    // 獲取參數的類型
                    System.out.print(ps[i].getSimpleName() + end);
                }
                System.out.println();
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
LoadingTest
public boolean equals(Object)
public String toString()
public int hashCode()
public int compareTo(String)
public volatile int compareTo(Object)
public int indexOf(String,int)
public int indexOf(String)
public int indexOf(int,int)
public int indexOf(int)
static int indexOf(char[],int,int,char[],int,int,int)
static int indexOf(char[],int,int,String,int)
public static String valueOf(int)
public static String valueOf(long)
public static String valueOf(float)
public static String valueOf(boolean)
public static String valueOf(char[])
public static String valueOf(char[],int,int)
public static String valueOf(Object)
public static String valueOf(char)
public static String valueOf(double)
public char charAt(int)
private static void checkBounds(byte[],int,int)
public int codePointAt(int)
public int codePointBefore(int)
public int codePointCount(int,int)
public int compareToIgnoreCase(String)
public String concat(String)
public boolean contains(CharSequence)
public boolean contentEquals(CharSequence)
public boolean contentEquals(StringBuffer)
public static String copyValueOf(char[])
public static String copyValueOf(char[],int,int)
public boolean endsWith(String)
public boolean equalsIgnoreCase(String)
public static transient String format(Locale,String,Object[])
public static transient String format(String,Object[])
public void getBytes(int,int,byte[],int)
public byte[] getBytes(Charset)
public byte[] getBytes(String)
public byte[] getBytes()
public void getChars(int,int,char[],int)
 void getChars(char[],int)
private int indexOfSupplementary(int,int)
public native String intern()
public boolean isEmpty()
public static transient String join(CharSequence,CharSequence[])
public static String join(CharSequence,Iterable)
public int lastIndexOf(int)
public int lastIndexOf(String)
static int lastIndexOf(char[],int,int,String,int)
public int lastIndexOf(String,int)
public int lastIndexOf(int,int)
static int lastIndexOf(char[],int,int,char[],int,int,int)
private int lastIndexOfSupplementary(int,int)
public int length()
public boolean matches(String)
private boolean nonSyncContentEquals(AbstractStringBuilder)
public int offsetByCodePoints(int,int)
public boolean regionMatches(int,String,int,int)
public boolean regionMatches(boolean,int,String,int,int)
public String replace(char,char)
public String replace(CharSequence,CharSequence)
public String replaceAll(String,String)
public String replaceFirst(String,String)
public String[] split(String)
public String[] split(String,int)
public boolean startsWith(String,int)
public boolean startsWith(String)
public CharSequence subSequence(int,int)
public String substring(int)
public String substring(int,int)
public char[] toCharArray()
public String toLowerCase(Locale)
public String toLowerCase()
public String toUpperCase()
public String toUpperCase(Locale)
public String trim()

Process finished with exit code 0

數組類的加載

創建數組類的情況稍微有些特殊,因為數組類本身並不是由類加載器負責創建,而是由JVM在運行時根據需要而直接創建的,但數組的元素類型仍然需要依靠類加載器去創建。創建數組類(下述簡稱A)的過程:

  1. 如果數組的元素類型是引用類型,那么就遵循定義的加載過程遞歸加載和創建數組A的元素類型;

  2. JVM使用指定的元素類型和數組維度來創建新的數組類。

如果數組的元素類型是引用類型,數組類的可訪問性就由元素類型的可訪問性決定。否則數組類的可訪問性將被缺省定義為public

過程二:Linking(鏈接)階段

Verification(驗證)

當類加載到系統后,就開始鏈接操作,驗證是鏈接操作的第一步。

它的目的是保證加載的字節碼是合法、合理並符合規范的。

驗證的步驟比較復雜,實際要驗證的項目也很繁多,大體上Java虛擬機需要做以下檢查,如圖所示。

整體說明

驗證的內容則涵蓋了類數據信息的格式驗證、語義檢查、字節碼驗證,以及符號引用驗證等。

  • 其中格式驗證會和加載階段一起執行。驗證通過之后,類加載器才會成功將類的二進制數據信息加載到方法區中。
  • 格式驗證之外的驗證操作將會在方法區中進行。

鏈接階段的驗證雖然拖慢了加載速度,但是它避免了在字節碼運行時還需要進行各種檢查。(磨刀不誤砍柴工)

具體說明

格式驗證:是否以魔數0xCAFEBABE開頭,主版本和副版本號是否在當前Java虛擬機的支持范圍內,數據中每一個項是否都擁有正確的長度等。

Java虛擬機會進行字節碼的語義檢查,但凡在語義上不符合規范的,虛擬機也不會給予驗證通過。比如:

  • 是否所有的類都有父類的存在(在Java里,除了Object外,其他類都應該有父類)
  • 是否一些被定義為final的方法或者類被重寫或繼承了
  • 非抽象類是否實現了所有抽象方法或者接口方法
  • 是否存在不兼容的方法(比如方法的簽名除了返回值不同,其他都一樣,這種方法會讓虛擬機無從下手調度;abstract情況下的方法,就不能是final的了)

Java虛擬機還會進行字節碼驗證,字節碼驗證也是驗證過程中最為復雜的一個過程。它試圖通過對字節碼流的分析,判斷字節碼是否可以被正確地執行。比如:

  • 在字節碼的執行過程中,是否會跳轉到一條不存在的指令
  • 函數的調用是否傳遞了正確類型的參數
  • 變量的賦值是不是給了正確的數據類型等

棧映射幀(StackMapTable)就是在這個階段,用於檢測在特定的字節碼處,其局部變量表和操作數棧是否有着正確的數據類型。但遺憾的是,100%准確地判斷一段字節碼是否可以被安全執行是無法實現的,因此,該過程只是盡可能地檢查出可以預知的明顯的問題。如果在這個階段無法通過檢查,虛擬機也不會正確裝載這個類。但是,如果通過了這個階段的檢查,也不能說明這個類是完全沒有問題的

在前面3次檢查中,已經排除了文件格式錯誤、語義錯誤以及字節碼的不正確性。但是依然不能確保類是沒有問題的。

校驗器還將進行符號引用的驗證。Class文件在其常量池會通過字符串記錄自己將要使用的其他類或者方法。因此,在驗證階段,虛擬機就會檢查這些類或者方法確實是存在的,並且當前類有權限訪問這些數據,如果一個需要使用類無法在系統中找到,則會拋出NoClassDefFoundError,如果一個方法無法被找到,則會拋出NoSuchMethodError。

此階段在解析環節才會執行。

Preparation(准備)

准備階段(Preparation),簡言之,為類的靜態變量分配內存,並將其初始化為默認值

當一個類驗證通過時,虛擬機就會進入准備階段。在這個階段,虛擬機就會為這個類分配相應的內存空間,並設置默認初始值。Java虛擬機為各類型變量默認的初始值如表所示。

類型 默認初始值
byte (byte) 0
short (short) 0
int 0
long OL
float 0.0f
double 0.0
char \u0000
boolean false
reference null

注意:
Java並不支持boolean類型,對於boolean類型,內部實現是int,由於int的默認值是0,故對應的,boolean的默認值就是false.

  • 這里不包含基本數據類型的字段用static final修飾的情況,因為final在編譯的時候就會分配了,准備階段會顯式賦值。
  • 注意這里不會為實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨着對象一起分配到Java堆中。
  • 在這個階段並不會像初始化階段中那樣會有初始化或者代碼被執行。
/**
 * 過程2:鏈接階段
 * <p>
 * 基本數據類型:非final修飾的變量,在解析環節進行默認初始化復制
 *              final修飾以后,在解析環節直接進行顯示復制
 */
public class LinkingTest {
    private static long id;
    private static final int num = 1;
    public static final String constStr = "CONST";
}

num有ConstantValue

constStr也有ConstantValue,對應在字符串常量池中可以被找到

constStr1在clinit中賦值

Resolution(解析)

在准備階段完成后,就進入了解析階段。

解析階段(Resolution),簡言之,將類、接口、字段和方法的符號引用轉為直接引用

具體描述

符號引用就是一些字面量的引用,和虛擬機的內部數據結構和和內存布局無關。比較容易理解的就是在Class類文件中,通過常量池進行了大量的符號引用。但是在程序實際運行時,只有符號引用是不夠的,比如當如下println()方法被調用時,系統需要明確知道該方法的位置。

舉例: 輸出操作System.out.println()對應的字節碼
invokevirtual #24 <java/io/PrintStream.println>

以方法為例,Java虛擬機為每個類都准備了一張方法表,將其所有的方法都列在表中,當需要調用一個類的方法的時候,只要知道這個方法在方法表中的偏移量就可以直接調用該方法。通過解析操作,符號引用就可以轉變為目標方法在類中方法表中的位置,從而使得方法被成功調用

小結

所謂解析就是將符號引用轉為直接引用,也就是得到類、字段、方法在內存中的指針或者偏移量。因此,可以說,如果直接引用存在,那么可以肯定系統中存在該類、方法或者字段。但只存在符號引用,不能確定系統中一定存在該結構。

不過Java虛擬機規范並沒有明確要求解析階段一定要按照順序執行。在HotSpot VM中,加載、驗證、准備和初始化會按照順序有條不紊地執行,但鏈接階段中的解析操作往往會伴隨着JVM在執行完初始化之后再執行。

字符串的練習

最后,再來看一下CONSTANT_String的解析。由於字符串在程序開發中有着重要的作用,因此,讀者有必要了解一下String在Java虛擬機中的處理。當在Java代碼中直接使用字符串常量時,就會在類中出現CONSTANT_String,它表示字符串常量,並且會引用一個CONSTANT_UTF8的常量項。在Java虛擬機內部運行中的常量池中,會維護一張字符串拘留表(intern),它會保存所有出現過的字符串常量,並且沒有重復項。只要以CONSTANT_String形式出現的字符串也都會在這張表中。使用String.intern()方法可以得到一個字符串在拘留表中的引用,因為該表中沒有重復項,所以任何字面相同的字符串的String.intern()方法返回總是相等的。

過程三:Initialization(初始化)階段

初始化階段,簡言之,為類的靜態變量賦予正確的初始值。

具體描述

類的初始化是類裝載的最后一個階段。如果前面的步驟都沒有問題,那么表示類可以順利裝載到系統中。此時,類才會開始執行Java字節碼。(即:到了初始化階段,才真正開始執行類中定義的Java程序代碼。)

初始化階段的重要工作是執行類的初始化方法<clinit>()方法。

  • 該方法僅能由Java編譯器生成並由JVM調用,程序開發者無法自定義一個同名的方法,更無法直接在Java程序中調用該方法,雖然該方法也是由字節碼指令所組成。
  • 它是由類靜態成員的賦值語句以及static語句塊合並產生的。
/**
 * 過程三:初始化階段
 */
public class InitializationTest {
    public static int id = 1;
    public static int number;

    static {
        number = 2;
        System.out.println("father static{}");
    }
}

說明

在加載一個類之前,虛擬機總是會試圖加載該類的父類,因此父類的總是在子類之前被調用。也就是說,父類的static塊優先級高於子類。

  • 口訣:由父及子,靜態先行
public class SubInitialization extends InitializationTest {
    static {
        // number屬性必須提前已經加載:一定會先加載父類
        number = 4;
        System.out.println("son static{}");
    }

    public static void main(String[] args) {
        System.out.println(number);
    }
}
father static{}
son static{}
4

Process finished with exit code 0

Java編譯器並不會為所有的類都產生()初始化方法。哪些類在編譯為字節碼后,字節碼文件中將不會包含<clinit>()方法?

  • 一個類中並沒有聲明任何的類變量,也沒有靜態代碼塊時
  • 一個類中聲明類變量,但是沒有明確使用類變量的初始化語句以及靜態代碼塊來執行初始化操作時
  • 一個類中包含static final修飾的基本數據類型的字段,這些類字段初始化語句采用編譯時常量表達式

static與final的搭配問題

static不加final的變量都在初始化環節賦值

static加final的常量 → static final

加static的:

  • 基本型,在鏈接階段的准備階段賦值
  • 引用類型:
    • 字面量聲明在鏈接的准備階段賦值(直接賦值常量,而非調用方法)
    • new 等方式在初始化階段clinit中賦值

結論:使用static + final修飾,且顯示賦值中不涉及到方法或構造器調用的基本數據類型或String類型的顯式賦值,是在鏈接階段的准備環節進行。

/**
 * 那些場景下,Java編譯器不會生成clinit方法
 */
public class InitializationTest1 {
    /**
     * 場景1:對於非靜態的方法,不管是否進行了顯示賦值,都不會生成clinit方法
     */
    public int num = 1;

    /**
     * 場景2:靜態的字段,沒有顯示的賦值,也不會生成clinit
     */
    public static int num1;

    /**
     * 場景3:比如對於聲明為static final的基本數據類型的字段,不管是否進行了顯示賦值,都不會生成clinit方法
     */
    public static final int num2 = 1;
}

通過代碼理解static和final的搭配使用情況

import java.util.Random;

/**
 * 說明:使用static + final修飾的字段的顯式賦值的操作,到底是在哪個階段進行的賦值?
 * 情況1:在鏈接階段的准備環節賦值
 * 情況2:在初始化階段<clinit>()中賦值
 * <p>
 * 結論:
 * 在鏈接階段的准備環節賦值的情況:
 * 1. 對於基本數據類型的字段來說,如果使用static final修飾,則顯式賦值(直接賦值常量,而非調用方法)通常是在鏈接階段的准備環節進行
 * 2. 對於String來說,如果使用字面量的方式賦值,使用static final修飾的話,則顯式賦值通常是在鏈接階段的准備環節進行
 * <p>
 * 在初始化階段<clinit>()中賦值的情況:
 * 排除上述的在准備環節賦值的情況之外的情況。
 * <p>
 * 最終結論:使用static + final修飾,且顯示賦值中不涉及到方法或構造器調用的基本數據類型或String類型的顯式賦值,是在鏈接階段的准備環節進行。
 */
public class InitializationTest2 {
    /**
     * 在初始化階段<clinit>()中賦值
     */
    public static int a = 1;
    /**
     * 在鏈接階段的准備環節賦值
     */
    public static final int INT_CONSTANT = 10;

    /**
     * 在初始化階段<clinit>()中賦值
     */
    public static final Integer INTEGER_CONSTANT1 = Integer.valueOf(100);
    /**
     * 在初始化階段<clinit>()中賦值
     */
    public static Integer INTEGER_CONSTANT2 = Integer.valueOf(1000);

    /**
     * 在鏈接階段的准備環節賦值
     */
    public static final String s0 = "helloworld0";
    /**
     * 在初始化階段<clinit>()中賦值
     */
    public static final String s1 = new String("helloworld1");

    public static String s2 = "helloworld2";

    /**
     * 字面量,在鏈接階段的准備環節賦值
     */
    public static final int NUM = 2;
    /**
     * 在初始化階段<clinit>()中賦值,編譯階段確定不了具體值
     */
    public static final int NUM1 = new Random().nextInt(10);
}

<clinit>() 的線程安全性

對於<clinit>()方法的調用,也就是類的初始化,虛擬機會在內部確保其多線程環境中的安全性。

虛擬機會保證一個類的<clinit>()方法在多線程環境中被正確地加鎖、同步,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>()方法,其他線程都需要阻塞等待,直到活動線程執行<clinit>()方法完畢。

正是因為函數<clinit>()帶鎖線程安全的,因此,如果在一個類的<clinit>()方法中有耗時很長的操作,就可能造成多個線程阻塞,引發死鎖。並且這種死鎖是很難發現的,因為看起來它們並沒有可用的鎖信息。

如果之前的線程成功加載了類,則等在隊列中的線程就沒有機會再執行<clinit>()方法了。那么,當需要使用這個類時,虛擬機會直接返回給它已經准備好的信息。

如下代碼:

  • loadA線程加載了staticA,staticA加載了staticB
  • loadB線程加載了staticB,staticB加載了staticA
  • staticA和staticB都有1s的阻塞,所以當staticA想要加載staticB的時候,staticB已經被loadB線程先加載
  • 所以loadA線程需要等待staticB,而loadB線程也要等待staticA
  • 由此出現了類的交叉加載行為,繼而出現了類的死鎖行為
  • 程序呈現出僵持狀態,輸出語句不會打印,相當於程序進入阻塞狀態,出現了事實上的死鎖
class StaticA {
    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        try {
            Class.forName("StaticB");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("StaticA init OK");
    }
}

class StaticB {
    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        try {
            Class.forName("StaticA");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("StaticB init OK");
    }
}

/**
 * 死鎖舉例
 */
public class StaticDeadLockMain extends Thread {
    private char flag;

    public StaticDeadLockMain(char flag) {
        this.flag = flag;
        this.setName("Thread" + flag);
    }

    @Override
    public void run() {
        try {
            Class.forName("Static" + flag);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println(getName() + " over");
    }

    public static void main(String[] args) throws InterruptedException {
        StaticDeadLockMain loadA = new StaticDeadLockMain('A');
        loadA.start();
        StaticDeadLockMain loadB = new StaticDeadLockMain('B');
        loadB.start();
    }
}

類的初始化情況:主動使用vs被動使用

Java程序對類的使用分為兩種:主動使用和被動使用

主動使用

Class只有在必須要首次使用的時候才會被裝載,Java虛擬機不會無條件地裝載Class類型。Java虛擬機規定,一個類或接口在初次使用前,必須要進行初始化。這里指的“使用”,是指主動使用,主動使用只有下列幾種情況:(即:如果出現如下的情況,則會對類進行初始化操作。而初始化操作之前的加載、驗證、准備已經完成。)

  1. 當創建一個類的實例時,比如使用new關鍵字,或者通過反射、克隆、反序列化。

  2. 當調用類的靜態方法時,即當使用了字節碼invokestatic指令。

  3. 當使用類、接口的靜態字段時(final修飾特殊考慮),比如,使用getstatic或者putstatic指令。(對應訪問變量、賦值變量操作)

  4. 當使用java.lang.reflect包中的方法反射類的方法時。比如:Class.forName("com.atguigu.java.Test")

  5. 當初始化子類時,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。

  6. 如果一個接口定義了default方法,那么直接實現或者間接實現該接口的類的初始化,該接口要在其之前被初始化。

  7. 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。

  8. 當初次調用 MethodHandle實例時,初始化該MethodHandle指向的方法所在的類。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄對應的類)

1、2中的情況
import org.junit.jupiter.api.Test;

import java.io.*;

/**
 * 測試類的主動使用
 * <p>
 * 1. 當創建一個類的實例時,比如使用new關鍵字,或者通過反射、克隆、反序列化。
 * <p>
 * 2. 當調用類的靜態方法時,即當使用了字節碼invokestatic指令。
 */
public class ActiveUse1 {
    public static void main(String[] args) {
        Order order = new Order();
    }

    /**
     * 序列化過程
     */
    @Test
    public void test1() {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("order.dat"))) {
            oos.writeObject(new Order());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 反序列化的過程(驗證)
     */
    @Test
    public void test2() {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("order.dat"))) {
            Order order = (Order) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    @Test
    public void test3() {
        Order.method();
    }
}

class Order implements Serializable {
    /**
     * serialVersionUID
     */
    private static final long serialVersionUID = 5944092695545593280L;

    static {
        System.out.println("Order類的初始化過程");
    }

    public static void method() {
        System.out.println("Order method()....");
    }
}
3中的情況
import org.junit.jupiter.api.Test;

import java.util.Random;

/**
 * 測試類的主動使用
 * <p>
 * 3. 當使用類、接口的靜態字段時(final修飾特殊考慮),比如,使用getstatic或者putstatic指令。(對應訪問變量、賦值變量操作)
 */
public class ActiveUse2 {
    @Test
    public void test1() {
        // 類初始化了    num是變量
        System.out.println(User.num);
        // 類沒有初始化   num1是靜態常量
        // System.out.println(User.num1);
        // 類初始化了    num2不是字面量方式聲明的,需要方法調用,編譯期間無法確定
        // System.out.println(User.num2);
    }


    @Test
    public void test2() {
        // 接口不需要初始化
        // System.out.println(CompareA.NUM1);
        // 接口需要初始化
        System.out.println(CompareA.NUM2);
    }
}

class User {
    static {
        System.out.println("User類的初始化過程");
    }

    public static int num = 1;
    public static final int num1 = 1;
    public static final int num2 = new Random().nextInt(10);
}

interface CompareA {
    Thread t = new Thread() {{
        System.out.println("CompareA的初始化");
    }};

    int NUM1 = 1;
    int NUM2 = new Random().nextInt(10);
}
其他情況

import org.junit.jupiter.api.Test;

import java.util.Random;

/**
 * 測試類的主動使用
 * <p>
 * 4. 當使用java.lang.reflect包中的方法反射類的方法時。比如:Class.forName("com.atguigu.java.Test")
 * <p>
 * 5. 當初始化子類時,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
 * <p>
 * 6. 如果一個接口定義了default方法,那么直接實現或者間接實現該接口的類的初始化,該接口要在其之前被初始化。
 * <p>
 * 7. 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
 * <p>
 * 8. 當初次調用 MethodHandle實例時,初始化該MethodHandle指向的方法所在的類。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄對應的類)
 */
public class ActiveUse3 {
    static {
        System.out.println("ActiveUse3的初始化過程");
    }

    /**
     * 當使用java.lang.reflect包中的方法反射類的方法時。比如:Class.forName("com.atguigu.java.Test")
     */
    @Test
    public void test1() {
        try {
            // "Order類的初始化過程"
            Class clazz = Class.forName("Order");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /**
     * 當初始化子類時,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
     * <p>
     * 注意:
     * <p>
     * 當Java虛擬機初始化一個類時,要求它的所有父類都已經被初始化,但是這條規則並不適用於接口。
     * <ol>
     * <li>在初始化一個類時,並不會先初始化它所實現的接口</li>
     * <li>在初始化一個接口時,並不會先初始化它的父接口</li>
     * </ol>
     * <p>
     * 因此,一個父接口並不會因為它的子接口或者實現類的初始化而初始化。只有當程序首次使用特定接口的靜態字段時,才會導致該接口的初始化。
     */
    @Test
    public void test2() {
        // [Loaded Father from file:/Users/xxx/IdeaProjects/jvm/out/production/jvm/]
        // [Loaded Son from file:/Users/xxx/IdeaProjects/jvm/out/production/jvm/]
        // Father類的初始化過程
        // Son類的初始化過程
        // 1
        System.out.println(Son.num);
    }

    @Test
    public void test3() {
        // CompareC的初始化
        // -2070299518
        System.out.println(CompareC.NUM1);
    }

    /**
     * 6、 如果一個接口定義了default方法,那么直接實現或者間接實現該接口的類的初始化,該接口要在其之前被初始化。
     * 如果Son2還有子類,根據5,那么其子類的父類,父類的父類也都要被初始化
     */
    @Test
    public void test4() {
        // Father類的初始化過程
        // CompareB的初始化
        // Son2類的初始化過程
        // 1
        System.out.println(Son2.num);
    }

    /**
     * 7. 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
     *
     * @param args
     */
    public static void main(String[] args) {
        // ActiveUse3的初始化過程
        // hello
        System.out.println("hello");
    }
}

class Father {
    static {
        System.out.println("Father類的初始化過程");
    }
}

class Son extends Father implements CompareB {
    static {
        System.out.println("Son類的初始化過程");
    }

    public static int num = 1;
}

class Son2 extends Father implements CompareB {
    static {
        System.out.println("Son2類的初始化過程");
    }

    public static int num = 1;
}

interface CompareB {
    Thread t = new Thread() {{
        System.out.println("CompareB的初始化");
    }};

    default void method1() {
        System.out.println("你好!");
    }
}

interface CompareC extends CompareB {
    Thread t = new Thread() {{
        System.out.println("CompareC的初始化");
    }};

    int NUM1 = new Random().nextInt();
}

被動使用

除了以上的情況屬於主動使用,其他的情況均屬於被動使用。被動使用不會引起類的初始化。

也就是說:並不是在代碼中出現的類,就一定會被加載或者初始化。如果不符合主動使用的條件,類就不會初始化。

  1. 當訪問一個靜態字段時,只有真正聲明這個字段的類才會被初始化。
    • 當通過子類引用父類的靜態變量,不會導致子類初始化
  2. 通過數組定義類引用,不會觸發此類的初始化
  3. 引用常量不會觸發此類或接口的初始化。因為常量在鏈接階段就已經被顯式賦值了。
  4. 調用ClassLoader類的loadClass()方法加載一個類,並不是對類的主動使用,不會導致類的初始化。
情況1、2
import org.junit.jupiter.api.Test;

/**
 * 關於類的被動使用,即不會進行類的初始化操作,即不會調用<clinit>()
 * 說明:沒有初始化的類,不意味着沒有加載!
 * <p>
 * 1. 當訪問一個靜態字段時,只有真正聲明這個字段的類才會被初始化。
 * - 當通過子類引用父類的靜態變量,不會導致子類初始化
 * <p>
 * 2. 通過數組定義類引用,不會觸發此類的初始化
 * <p>
 * 3. 引用常量不會觸發此類或接口的初始化。因為常量在鏈接階段就已經被顯式賦值了。
 * <p>
 * 4. 調用ClassLoader類的loadClass()方法加載一個類,並不是對類的主動使用,不會導致類的初始化。
 */
public class PassiveUse1 {
    /**
     * 1. 當訪問一個靜態字段時,只有真正聲明這個字段的類才會被初始化。
     * - 當通過子類引用父類的靜態變量,不會導致子類初始化
     */
    @Test
    public void test1() {
        // [Loaded Parent from file:/Users/xxx/IdeaProjects/jvm/out/production/jvm/]
        // [Loaded Child from file:/Users/xxx/IdeaProjects/jvm/out/production/jvm/]
        // Parent的初始化過程
        // 1
        System.out.println(Child.num);
    }

    /**
     * 2. 通過數組定義類引用,不會觸發此類的初始化
     */
    @Test
    public void test2() {
        // 不輸出
        Parent[] parents = new Parent[10];
        // class [LParent;
        System.out.println(parents.getClass());
        // class java.lang.Object
        System.out.println(parents.getClass().getSuperclass());
        // Parent的初始化過程
        parents[0] = new Parent();
        // 不輸出,初始化過了
        parents[1] = new Parent();
    }
}

class Parent {
    static {
        System.out.println("Parent的初始化過程");
    }

    public static int num = 1;
}

class Child extends Parent {
    static {
        System.out.println("Child的初始化過程");
    }
}
情況3、4
import org.junit.jupiter.api.Test;

import java.util.Random;

/**
 * 關於類的被動使用,即不會進行類的初始化操作,即不會調用<clinit>()
 * 說明:沒有初始化的類,不意味着沒有加載!
 * <p>
 * 1. 當訪問一個靜態字段時,只有真正聲明這個字段的類才會被初始化。
 * - 當通過子類引用父類的靜態變量,不會導致子類初始化
 * <p>
 * 2. 通過數組定義類引用,不會觸發此類的初始化
 * <p>
 * 3. 引用常量不會觸發此類或接口的初始化。因為常量在鏈接階段就已經被顯式賦值了。
 * <p>
 * 4. 調用ClassLoader類的loadClass()方法加載一個類,並不是對類的主動使用,不會導致類的初始化。
 */
public class PassiveUse2 {
    /**
     * 3. 引用常量不會觸發此類或接口的初始化。因為常量在鏈接階段就已經被顯式賦值了。
     */
    @Test
    public void test1() {
        // 不會觸發Person類的初始化
        System.out.println(Person.NUM);
        // 觸發了Person類的初始化
        // System.out.println(Person.NUM1);
    }

    @Test
    public void test2() {
        // 接口同理 使用SerialA.ID不會觸發SerialA的初始化
        System.out.println(SerialA.ID);
        // 接口同理 ID1會觸發SerialA的初始化
        System.out.println(SerialA.ID1);
    }

    /**
     * 4. 調用ClassLoader類的loadClass()方法加載一個類,並不是對類的主動使用,不會導致類的初始化。
     */
    @Test
    public void test3() {
        try {
            // 不會觸發Person類的初始化,被動使用
            Class clazz = ClassLoader.getSystemClassLoader().loadClass("Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

class Person {
    static {
        System.out.println("Person類的初始化");
    }

    // 在鏈接過程的准備環節就被賦值為1了
    public static final int NUM = 1;
    // 此時的賦值操作需要在<clinit>()中執行
    public static final int NUM1 = new Random().nextInt(10);
}

interface SerialA {
    Thread t = new Thread() {{
        System.out.println("SerialA的初始化");
    }};

    int ID = 1;
    // 此時的賦值操作需要在<clinit>()中執行
    int ID1 = new Random().nextInt(10);
}

過程四:類的Using使用

任何一個類型在使用之前都必須經歷過完整的加載、鏈接和初始化3個類加載步驟。一旦一個類型成功經歷過這3個步驟之后,便“萬事俱備,只欠東風”,就等着開發者使用了。

開發人員可以在程序中訪問和調用它的靜態類成員信息(比如:靜態字段、靜態方法),或者使用new關鍵字為其創建對象實例。

例:加載一個類時,以Order類為例:

  • 方法區存放Order類模板數據/對象
  • 堆空間中創建一個Order類的Class實例,這個實例指向了方法區中的類模板對象
  • 棧中(棧幀的局部變量表中)中聲明了一個class對象,class對象指向了堆空間中的Class實例
  • Order的對象實例存放在堆中

過程五:類的Unloading(卸載)

類存在於方法區中,jdk8中方法區的落地實現是元空間,元空間使用的是系統內存,所以當類沒有被及時卸載時,可能會出現方法區的OOM

類、類的加載器、類的實例之間的引用關系

在類加載器的內部實現中,用一個Java集合來存放所加載類的引用。另一方面,一個Class對象總是會引用它的類加載器,調用Class對象的getClassLoader()方法,就能獲得它的類加載器。由此可見,代表某個類的Class實例與其類的加載器之間為雙向關聯關系。

一個類的實例總是引用代表這個類的Class對象。在0bject類中定義了getClass()方法,這個方法返回代表對象所屬類的Class對象的引用。此外,所有的Java類都有一個靜態屬性class,它引用代表這個類的Class對象。

類的生命周期

當Sample類被加載、鏈接和初始化后,它的生命周期就開始了。當代表Sample類的Class對象不再被引用,即不可觸及時,Class對象就會結束生命周期,Sample類在方法區內的數據也會被卸載,從而結束Sample類的生命周期。

一個類何時結束生命周期,取決於代表它的Class對象何時結束生命周期。

具體例子

苛刻

loader1變量和obj變量間接應用代表Sample類的Class對象,而objClass變量則直接引用它。

如果程序運行過程中,將上圖左側三個引用變量都置為null,此時Sample對象結束生命周期,Myclass Loader對象結束生命周期,代表SampleClass類的對象也結束生命周期,Sample類在方法區內的二進制數據被卸載。

當再次有需要時,會檢查Sample類的Class對象是否存在,如果存在會直接使用,不再重新加載;如果不存在Sample類會被重新加載,在Java虛擬機的堆區會生成一個新的代表SampleClass類的實例(可以通過哈希碼查看是否是同一個實例)。

四、類的卸載

  1. 啟動類加載器加載的類型在整個運行期間是不可能被卸載的(jvm和jls規范)
  2. 被系統類加載器和擴展類加載器加載的類型在運行期間不太可能被卸載,因為系統類加載器實例或者擴展類的實例基本上在整個運行期間總能直接或者間接的訪問的到,其達到 unreachable的可能性極小。
  3. 被開發者自定義的類加載器實例加載的類型只有在很簡單的上下文環境中才能被卸載,而且一般還要借助於強制調用虛擬機的垃圾收集功能才可以做到。可以預想,稍微復雜點的應用場景中(比如:很多時候用戶在開發自定義類加載器實例的時候采用緩存的策略以提高系統性能),被加載的類型在運行期間也是幾乎不太可能被卸載的(至少卸載的時間是不確定的)。

綜合以上三點,一個已經加載的類型被卸載的幾率很小至少被卸載的時間是不確定的。同時我們可以看的出來,開發者在開發代碼時候,不應該對虛擬機的類型卸載做任何假設的前提下,來實現系統中的特定功能。

回顧:方法區的垃圾回收

方法區的垃圾收集主要回收兩部分內容:常量池中廢棄的常量和不再使用的類型。

HotSpot虛擬機對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收。

判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬於“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:

  • 該類所有的實例都已經被回收。也就是Java堆中不存在該類及其任何派生子類的實例。
  • 加載該類的類加載器已經被回收。這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的。
  • 該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

Java虛擬機被允許對滿足上述三個條件的無用類進行回收,這里說的僅僅是“被允許”,而並不是和對象一樣,沒有引用了就必然會回收。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM