1. 程式人生 > 其它 >演算法——LeetCode 21. 合併兩個升序連結串列

演算法——LeetCode 21. 合併兩個升序連結串列

技術標籤:jvmjvm

概述

在程式碼編譯後,就會生成JVM(Java虛擬機器)能夠識別的二進位制位元組流檔案(*.class)。JVM把Class檔案中的類描述資料從檔案載入到記憶體,並對資料進行校驗、轉換解析、初始化,使這些資料最終成為可以被JVM直接使用的Java型別,這個說來簡單但實際複雜的過程叫做JVM的類載入機制。
在這裡插入圖片描述

Class檔案中的“類”從載入到JVM記憶體中,到卸載出記憶體過程有七個生命週期階段。類載入機制包括了前五個階段。


類載入時機

載入(loading)階段,java虛擬機器規範中沒有進行約束,這點可以交給虛擬機器的具體實現來自由把握。

初始化階段,java虛擬機器嚴格規定了有且只有如下5種情況必須立即進行初始化(而這一過程自然發生在載入、驗證、準備之後):

  • 使用new例項化物件時,讀取和設定類的靜態變數、靜態非字面值常量(靜態字面值常量除外)時,呼叫靜態方法時

  • 對內進行反射呼叫時

  • 當初始化一個類時,如果父類沒有進行初始化,需要先初始化父類

  • 啟動程式所使用的main方法所在類

  • 當使用1.7的動態語音支援時

注意,對於這五種會觸發類進行初始化的場景,虛擬機器規範中使用了一個很強烈的限定語:有且只有,這五種場景中的行為稱為對一個類進行主動引用。除此之外,所有引用類的方式,都不會觸發初始化,稱為被動引用

注意:類的例項化與類的初始化是兩個完全不同的概念
類的例項化是指建立一個類的例項(物件)的過程;
類的初始化是指為類中各個類成員(被static修飾的成員變數)賦初始值的過程,是類生命週期中的一個階段。


被動引用的常見場景

  • 通過子類引用父類的靜態欄位,只會觸發父類的初始化,而不會觸發子類的初始化。
  • 通過陣列定義來引用類,不會觸發此類的初始化
  • 類A引用類B的static final常量不會導致類B初始化(注意靜態常量必須是字面值常量,否則還是會觸發B的初始化)
public class TestClass {
    public static void main(String[] args) {
        System.out.println(ClassInit.str);
        System.out.println(ClassInit.id);
    }
}
class ClassInit
{ public static final long id=IdGenerator.getIdWorker().nextId();//需要初始化ClassInit類 public static final String str="abc";//字面值常量 static{ System.out.println("ClassInit init"); } }

類載入方式

這裡的類載入不是指類載入階段,而是指整個類載入過程,即類載入階段到初始化完成。

隱式載入

  • 建立類物件
  • 使用類的靜態域
  • 建立子類物件
  • 使用子類的靜態域
  • 在JVM啟動時,BootStrapLoader會載入一些JVM自身執行所需的class
  • 在JVM啟動時,ExtClassLoader會載入指定目錄下一些特殊的class
  • 在JVM啟動時,AppClassLoader會載入classpath路徑下的class,以及main函式所在的類的class檔案

顯式載入

  • ClassLoader.loadClass(className),只加載和連線、不會進行初始化
  • Class.forName(String name, boolean initialize,ClassLoader loader); 使用loader進行載入和連線,根據引數initialize決定是否初始化。

類載入過程

1. 載入(Loading)

在載入階段(可以參考java.lang.ClassLoader的loadClass()方法),虛擬機器需要完成以下三件事情:

(1) 通過一個類的全限定名來獲取定義此類的二進位制位元組流(並沒有指明要從一個Class檔案中獲取,可以從其他渠道,譬如:網路、動態生成、資料庫等);

(2) 將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資料結構;

(3) 在記憶體中(對於HotSpot虛擬就而言就是方法區)生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口;

載入階段和連線階段(Linking)的部分內容(如一部分位元組碼檔案格式驗證動作)是交叉進行的,載入階段尚未完成,連線階段可能已經開始,但這些夾在載入階段之中進行的動作,仍然屬於連線階段的內容,這兩個階段的開始時間仍然保持著固定的先後順序。

特別地,第一件事情(通過一個類的全限定名來獲取定義此類的二進位制位元組流)是由類載入器完成的,具體涉及JVM預定義的類載入器、雙親委派模型等內容,此不贅述。


2. 連線
2.1 驗證(Verification)

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

  • 檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範(例如,是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機器的處理範圍之內、常量池中的常量是否有不被支援的型別)

  • 元資料驗證:對位元組碼描述的資訊進行語義分析,以保證其描述的資訊符合Java語言規範的要求(例如:這個類是否有父類,除了java.lang.Object之外);

  • 位元組碼驗證:通過資料流和控制流分析,確定程式語義是合法的、符合邏輯的;

  • 符號引用驗證:確保解析動作能正確執行。

驗證階段是非常重要的,但不是必須的,它對程式執行期沒有影響。如果所引用的類經過反覆驗證,那麼可以考慮採用-Xverifynone引數來關閉大部分的類驗證措施,以縮短虛擬機器類載入的時間。


2.2 準備(Preparation)

準備階段是正式為類變數(static 成員變數)分配記憶體並設定類變數初始值(零值)的階段,這些變數所使用的記憶體都將在方法區中進行分配。這時候進行記憶體分配的僅包括類變數,而不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在堆中。其次,這裡所說的初始值“通常情況”下是資料型別的零值,假設一個類變數的定義為:

public static int value = 123;

那麼,變數value在準備階段過後的值為0而不是123。因為這時候尚未開始執行任何java方法,而把value賦值為123的putstatic指令是程式被編譯後,存放於類構造器方法()之中,所以把value賦值為123的動作將在初始化階段才會執行。至於“特殊情況”是指:當類欄位的欄位屬性是ConstantValue時,會在準備階段初始化為指定的值,所以標註為final之後,value的值在準備階段初始化為123而非0。

  public static final int value = 123;

2.3 解析(Resolution)

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。解析動作主要針對類或介面、欄位、類方法、介面方法、方法型別、方法控制代碼和呼叫點限定符7類符號引用進行。

3. 初始化

類初始化階段是類載入過程的最後一步。在前面的類載入過程中,除了在載入階段使用者應用程式可以通過自定義類載入器參與之外,其餘動作完全由虛擬機器主導和控制。到了初始化階段,才真正開始執行類中定義的java程式程式碼(位元組碼)。

在準備階段,變數已經賦過一次系統要求的初始值(零值);而在初始化階段,則根據程式猿通過程式制定的主觀計劃去初始化類變數和其他資源

為類的靜態變數賦初值,賦初值兩種方式:

定義靜態變數時指定初始值。如 private static String x="123";
在靜態程式碼塊裡為靜態變數賦值。如 static{ x="123"; }

注意:只有對類的主動使用才會導致類的初始化。
靜態語句塊只能訪問到定義在靜態語句塊之前的變數,定義在它之後的變數,在前面的靜態語句塊可以賦值,但是不能訪問

參考地址:JVM類生命週期概述:載入時機與載入過程
參考地址:JVM類載入過程