前言
之前說了類加載的過程,但是有的讀者表示還是有些知識點沒弄清楚,相關面試題也不能思考出結果,所以今天就來總結下類加載、對象實例化方面的知識點/面試題,幫助大家加深印象。
全是干貨,一網打盡類的基礎知識!先看看下面的問題都能回答上來嗎?
- 描述new一個對象的過程,並結合例子說明。
- 類初始化的觸發時機。
- 多線程進行類的初始化會出問題嗎?
- 類的實例化觸發時機。
<clinit>()方法和<init>()方法區別。- 在類都沒有初始化完畢之前,能直接進行實例化相應的對象嗎?
- 類的初始化過程與類的實例化過程的異同?
- 一個實例變量在對象初始化的過程中會被賦值幾次?
描述new一個對象的過程
先上圖,再描述:



Java中對象的創建過程包括 類初始化和類實例化兩個階段。
而new就是創建對象的一種方式,一種時機。
當執行到new的字節碼指令的時候,會先判斷這個類是否已經初始化,如果沒有初始化就要進行類的初始化,也就是執行類構造器<clinit>()方法。
如果已經初始化了,就直接進行類對象的實例化。
類的初始化,是類的生命周期中的一個階段,會為類中各個類成員賦初始值。類的實例化,是指創建一個類的實例的過程。
但是在類的初始化之前,JVM會保證類的裝載,鏈接(驗證、准備、解析)四個階段都已經完成,也就是上面的第一張圖。
裝載是指Java虛擬機查找.class文件並生成字節流,然后根據字節流創建java.lang.Class對象的過程。鏈接是指驗證創建的類,並將其解析到JVM中使之能夠被JVM執行。
那到底類加載的時機是什么時候呢?JVM 並沒有規范何時具體執行,不同虛擬機的實現會有不同,常見有以下兩種情況:
隱式裝載:在程序運行過程中,當碰到通過new等方式生成對象時,系統會隱式調用ClassLoader去裝載對應的 class 到內存中;顯示裝載:在編寫源代碼時,主動調用Class.forName()等方法也會進行 class 裝載操作,這種方式通常稱為顯示裝載。
所以到這里,大的流程框架就搞清楚了:
-
當
JVM碰到new字節碼的時候,會先判斷類是否已經初始化,如果沒有初始化(有可能類還沒有加載,如果是隱式裝載,此時應該還沒有類加載,就會先進行裝載、驗證、准備、解析四個階段),然后進行類初始化。 -
如果已經初始化過了,就直接開始類對象的
實例化工作,這時候會調用類對象的<init>方法。
結合例子說明
然后說說具體的邏輯,結合一段類代碼:
public class Run {
public static void main(String[] args) {
new Student();
}
}
public class Person{
public static int value1 = 100;
public static final int value2 = 200;
public int value4 = 400;
static{
value1 = 101;
System.out.println("1");
}
{
value1 = 102;
System.out.println("3");
}
public Person(){
value1 = 103;
System.out.println("4");
}
}
public class Student extends Person{
public static int value3 = 300;
public int value5 = 500;
static{
value3 = 301;
System.out.println("2");
}
{
value3 = 302;
System.out.println("5");
}
public Student(){
value3 = 303;
System.out.println("6");
}
}
-
首先是類裝載,鏈接(驗證、准備、解析)。
-
當執行類准備過程中,會對類中的
靜態變量分配內存,並設置為初始值也就是“0值”。比如上述代碼中的value1,value3,會為他們分配內存,並將其設置為0。但是注意,用final修飾靜態常量value2,會在這一步就設置好初始值102。 -
初始化階段,會執行類構造器
<clinit>方法,其主要工作就是初始化類中靜態的(變量,代碼塊)。但是在當前類的<clinit>方法執行之前,會保證其父類的<clinit>方法已經執行完畢,所以一開始會執行最上面的父類Object的<clinit>方法,這個例子中會先初始化父類Person,再初始化子類Student。 -
初始化中,靜態變量和靜態代碼塊順序是由語句在源文件中出現的順序所決定的,也就是誰寫在前面就先執行誰。所以這里先執行父類中的
value1=100,value1 = 101,然后執行子類中的value3 = 300,value3 = 301。 -
接着就是創建對象的過程,也就是類的實例化,當對象被類創建時,虛擬機會
分配內存來存放對象自己的實例變量和父類繼承過來的實例變量,同時會為這些事例變量賦予默認值(0值)。 -
分配完內存后,會初始化父類的普通成員變量
(value4 = 400),和執行父類的普通代碼塊(value1=102),順序由代碼順序決定。 -
執行父類的構造函數
(value1 = 103)。 -
父類實例化完了,就實例化子類,初始化子類的普通成員變量
(value5 = 500),執行子類的普通代碼塊(value3 = 302),順序由代碼順序決定。 -
執行子類的構造函數
(value3 = 303)。
所以上述例子打印的結果是:
123456
總結一下執行流程就是:
-
父類靜態變量和靜態代碼塊;
-
子類靜態變量和靜態代碼塊;
-
父類普通成員變量和普通代碼塊;
-
父類的構造函數;
-
子類普通成員變量和普通代碼塊;
-
子類的構造函數。
最后,大家再結合流程圖好好梳理一下:



類初始化的觸發時機
在同一個類加載器下,一個類型只會被初始化一次,剛才說到new對象是類初始化的一個判斷時機,其實一共有六種能夠觸發類初始化的時機:
-
虛擬機啟動時,初始化包含
main方法的主類; -
遇到
new等指令創建對象實例時,如果目標對象類沒有被初始化則進行初始化操作; -
當遇到訪問靜態方法或者靜態字段的指令時,如果目標對象類沒有被初始化則進行初始化操作;
-
子類的初始化過程如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化;
-
使用反射
API進行反射調用時,如果類沒有進行過初始化則需要先觸發其初始化; -
第一次調用
java.lang.invoke.MethodHandle實例時,需要初始化MethodHandle指向方法所在的類。
多線程進行類的初始化會出問題嗎
不會,<clinit>()方法是阻塞的,在多線程環境下,如果多個線程同時去初始化一個類,那么只會有一個線程去執行這個類的<clinit>(),其他線程都會被阻塞。
類的實例化觸發時機
- 使用
new關鍵字創建對象 - 使用Class類的
newInstance方法,Constructor類的newInstance方法(反射機制) - 使用
Clone方法創建對象 - 使用(反)序列化機制創建對象
<clinit>()方法和<init>()方法區別。
-
<clinit>()方法發生在類初始化階段,會執行類中的靜態類變量的初始化和靜態代碼塊中的邏輯,執行順序就是語句在源文件中出現的順序。 -
<init>()方法發生在類實例化階段,是默認的構造函數,會執行普通成員變量的初始化和普通代碼塊的邏輯,執行順序就是語句在源文件中出現的順序。
在類都沒有初始化完畢之前,能直接進行實例化相應的對象嗎?
剛才都說了先初始化,再實例化,如果這個問題可以的話那不是打臉了嗎?
沒錯,要打臉了哈哈。
確實是先進行類的初始化,再進行類的實例化,但是如果我們在類的初始化階段就直接實例化對象呢?比如:
class Person {
public static void main(String[] args) {
staticFunction();
}
static Person person = new Person();
static {
System.out.println("1");
}
{
System.out.println("2");
}
Person() {
System.out.println("3");
System.out.println("a=" + a + ",b=" + b);
}
public static void staticFunction() {
System.out.println("4");
}
int a = 100;
static int b = 200;
}
嘿嘿,這時候該怎么打印結果呢?
按照上面說過的邏輯,應該是先靜態變量和靜態代碼塊,然后普通成員變量和普通代碼塊,最后是構造函數。
但是因為靜態變量又執行了一次new Person(),所以實例化過程被強行提前了,被嵌入到了初始化過程中,在初始化過程中就進行了實例化。而且因為初始化沒有結束就打印了a和b的值,所以static變量b的值就沒有賦值。
這段代碼的結果就變成了:
2
3
a=100,b=0
1
4
所以,實例化不一定要在類初始化結束之后才開始初始化,有可能在初始化過程中就進行了實例化。
另外可以思考下,怎么改就能保證打印的時候b有值呢?(修改代碼順序或者修改變量修飾符)
類的初始化過程與類的實例化過程的異同?
學了上面的內容,這個問題就很簡單了:
-
類的初始化,是指在類裝載,鏈接之后的一個階段,會執行<clinit>()方法,初始化靜態變量,執行靜態代碼塊等。只會執行一次。 -
類的實例化,是指在類完全加載到內存中后創建對象的過程,會執行<init>()方法,初始化普通變量,調用普通代碼塊。可以被調用多次。
一個實例變量在對象初始化的過程中最多可以被賦值幾次?
那我們就試試舉例出最多的情況,其實也就是每個要經過的地方都對實例變量進行一次賦值:
- 1、
對象被創建時候,分配內存會把實例變量賦予默認值,這是肯定會發生的。 - 2、
實例變量本身初始化的時候,就給他賦值一次,也就是int value1=100。 - 3、
初始化代碼塊的時候,也賦值一次。 - 4、
構造函數中,在進行賦值一次。
一共四次,看代碼:
public class Person3 {
public int value1 = 100;
{
value1 = 102;
System.out.println("2");
}
public Person3(){
value1 = 103;
System.out.println("3");
}
}
參考
https://blog.csdn.net/justloveyou_/article/details/72466416
https://kaiwu.lagou.com/course/courseInfo.htm?courseId=67#/detail/pc?id=1860
https://www.jianshu.com/p/8a14ed0ed1e9
拜拜
有一起學習的小伙伴可以關注下❤️ 我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識。公眾號回復111可獲得面試題《思考與解答》以往期刊。

