在Java中,物件建立是一個至關重要的過程,它涉及類載入、記憶體分配、初始化、賦值等多個階段。理解這些過程不僅有助於編寫更高效的程式碼,還能幫助我們最佳化程式的效能。本文將深入分析Java物件建立的全過程,重點討論類載入、物件初始化、靜態與非靜態初始化的順序,以及JVM堆疊記憶體的分配機制,並結合程式碼進行解析。
一、類載入過程
Java虛擬機器(JVM)在執行Java程式時,需要首先將類載入到記憶體中。類載入分為以下幾個步驟:
載入(Loading):JVM透過類載入器(ClassLoader)載入位元組碼檔案(
.class
檔案)到記憶體中,並將其轉換為Class
物件。連結(Linking):
驗證(Verification):確保類的位元組碼符合JVM規範。
準備(Preparation):為類的靜態變數分配記憶體,並初始化為預設值。
解析(Resolution):將常量池中的符號引用替換為直接引用。
初始化(Initialization):執行類的靜態程式碼塊以及靜態變數的初始化。
示例程式碼:
public class InitializeDemo2 { private static int k = 1; private static int n = 10; private static int i; private String s = new String("-----"); static { System.out.println("靜態程式碼塊:執行了"); System.out.println("k=" + k + ", n=" + n + ", i=" + i); } private int h = 2; { System.out.println("普通程式碼塊:執行了"); System.out.println(s); } public static void m1() { System.out.println("靜態方法:執行了"); System.out.println("k=" + (k++) + ", n=" + n + ", i=" + i); } public void m2() { System.out.println("普通方法:執行了"); System.out.println("k=" + (k++) + ", n=" + n + ", i=" + i); } public InitializeDemo2(String str) { System.out.println("構造器:執行了"); System.out.println("k=" + (k++) + ", n=" + n + ", i=" + i); } }
在上述程式碼中,InitializeDemo2
類的載入過程會按上述順序進行。靜態變數 k
在“準備”階段被分配記憶體並初始化為預設值 0
,在“初始化”階段被賦值為 1
。
說到這裏,就要暫停下。我們怎麼驗證類的載入過程呢(本文僅從程式設計師更容易看懂的角度)?我們知道,如果呼叫類內部的靜態方法,會觸發類載入過程。
透過在另一個類呼叫IInitializeDemo2
的靜態方法:
public static void main(String args[]) { InitializeDemo2.m1(); }
可以驗證到輸出是:
靜態程式碼塊:執行了 k=1, n=10, i=0 靜態方法:執行了 k=1, n=10, i=0
說明,靜態變數在類載入階段就已經完成了!
二、物件初始化
在物件建立過程中,核心的過程是初始化,其分為兩個主要部分:靜態初始化和非靜態初始化。
靜態初始化:只在類第一次載入時執行,用於初始化靜態變數和執行靜態程式碼塊。
非靜態初始化:每次建立物件時執行,用於初始化例項變數和執行非靜態程式碼塊。
初始化順序:
靜態變數和靜態程式碼塊:按它們在類中出現的順序執行,只在類載入時執行一次,在前面已經透過程式碼得到驗證。
例項變數和例項程式碼塊:在每次建立物件時執行,執行順序同樣是按照它們在類中出現的順序。
建構函式:例項變數和例項程式碼塊執行後,纔會執行建構函式。
示例程式碼:
public class InitOrder { static { System.out.println("Static Block 1"); } static int x = print("Static Variable x"); static { System.out.println("Static Block 2"); } int y = print("Instance Variable y"); { System.out.println("Instance Block"); } public InitOrder() { System.out.println("Constructor"); } static int print(String message) { System.out.println(message); return 0; } public static void main(String[] args) { new InitOrder(); } }
輸出結果:
Static Block 1 Static Variable x Static Block 2 Instance Variable y Instance Block Constructor
從上面的例子中可以看出,靜態塊和靜態變數按照宣告順序初始化(讀者可以調整順序驗證),然後執行例項變數的初始化,接著執行例項塊,最後呼叫建構函式。
三、JVM堆疊記憶體的分配
JVM在建立物件時,主要使用堆記憶體、棧記憶體進行分配:
堆記憶體:用於儲存物件例項和陣列。每當我們用
new
關鍵字建立一個物件時,都會在堆中分配記憶體,其實這是一個動態的過程。棧記憶體:用於儲存方法呼叫相關的資訊,包括方法的引數、區域性變數、運算元棧和返回地址。每次方法呼叫都會在棧中建立一個棧幀(Stack Frame)。
物件建立時的堆、棧記憶體分配過程:
當
new
關鍵字用於建立物件時,JVM首先會在堆中為該物件分配記憶體空間,並初始化預設值(如int
預設值為0
,物件引用預設值為null
)。然後,JVM會將物件的引用地址儲存在棧中的區域性變數表中。
示例程式碼:
public class MemoryAllocation { public static void main(String[] args) { Example example = new Example(); } }
在這段程式碼中,new Example()
會在堆中分配 Example
物件的記憶體,並在棧中儲存指向這個物件的引用 example
。當方法執行結束,棧幀被銷燬,但堆中的物件只要有引用指向它,就不會被垃圾回收。
對於記憶體分配,這裏本文不計劃深入探討了,計劃未來再寫篇文章。
四、賦值過程
在Java中,物件的賦值過程包括兩個主要部分:預設初始化 和 顯式初始化。
預設初始化:在物件記憶體分配後,JVM自動將物件的所有例項變數設定為其預設值。
顯式初始化:在預設初始化之後,Java按照程式碼中出現的順序進行顯式初始化。如果有建構函式,則在顯式初始化完成後執行建構函式中的賦值操作。
示例程式碼:
public class ValueAssignment { int i = 5; String s = "Hello"; public ValueAssignment() { i = 10; s = "World"; } public static void main(String[] args) { ValueAssignment va = new ValueAssignment(); System.out.println("i: " + va.i + ", s: " + va.s); } }
輸出結果:
i: 10, s: World
這裏的過程如下:
預設初始化:
i
被設定為0
,s
被設定為null
。顯式初始化:
i
被賦值為5
,s
被賦值為"Hello"
。建構函式初始化:
i
被重新賦值為10
,s
被重新賦值為"World"
。
結論
物件建立是Java程式中非常基礎但複雜的過程,涉及類載入、靜態與非靜態初始化的順序、記憶體分配和賦值等多個方面。理解這些細節對於編寫高效、健壯的Java程式碼至關重要。透過本文的分析和示例程式碼,我們深入探討了物件建立的每一個階段以及它們在JVM中的具體實現方式,為深入掌握Java的記憶體模型和物件生命週期奠定了基礎。