面試官:在 Java 中 new 一個對象的流程是怎樣的?徹底被問懵了..
關(guān)注JAVA,推送更多 Java 干貨!

對象怎么創(chuàng)建,這個太熟悉了,new一下(其實還有很多途徑,比如反射、反序列化、clone等,這里拿最簡單的new來講):
Dog?dog?=?new?Dog();
我們總是習(xí)慣于固定語句的執(zhí)行,卻對于背后的實現(xiàn)過程缺乏認知,而理解這個過程對后面晦澀難懂的反射和代理其實會有很大幫助,所以請務(wù)必學(xué)好這塊內(nèi)容。
在看這篇文章之前,啰嗦一句:如果你死記硬背下面所說的流程等于白看,就算現(xiàn)在記住了,一個禮拜后呢,一個月后你又能記得多少,因為對象創(chuàng)建過程這個知識點平常的工作中基本不會涉及到,太底層了,背熟的知識點不經(jīng)常加以運用容易遺忘,所以我的建議是什么呢,流程做到心里大概有個數(shù),其中涉及到關(guān)鍵的知識點記牢就可以了。
JVM內(nèi)存
先簡單說下java虛擬機內(nèi)存模型和存放內(nèi)容的區(qū)別,兩部分:
- 棧內(nèi)存 存放基本類型數(shù)據(jù)和對象的引用變量,數(shù)據(jù)可以直接拿來訪問,速度比堆快
- 堆內(nèi)存 存放創(chuàng)建的對象和數(shù)組,會由java虛擬機的自動垃圾回收來管理(GC),創(chuàng)建一個對象放入堆內(nèi)的同時也會在棧中創(chuàng)建一個指向該對象堆內(nèi)存中的地址引用變量,下面說的對象就是存在該內(nèi)存中
下面我們就按照對象生成的過程來一一講解參與其中過程的各個概念。
首先有這么一個類,后面的初始化基于這個講解:
/**
?*?@author?煒哥
?*?@since?2021-04-18?11:01:41
?*
?*?執(zhí)行順序:(優(yōu)先級從高到低。)靜態(tài)代碼塊>構(gòu)造代碼塊>構(gòu)造方法>普通方法。
?*?其中靜態(tài)代碼塊只執(zhí)行一次。構(gòu)造代碼塊在每次創(chuàng)建對象是都會執(zhí)行。
?*/
public?class?Dog?{
????//默認狗狗的最大年齡是16歲
????private?static?int?dog_max_age?=?16;
????//狗狗的名字
????private?String?dog_name;
????{
????????System.out.println("狗狗的構(gòu)造代碼塊");
????}
????static?{
????????System.out.println("狗狗的靜態(tài)代碼塊");
????}
????//無參構(gòu)造器故意沒設(shè)
????//有參構(gòu)造器
????public?Dog(String?dog_name)?{
????????this.dog_name?=?dog_name;
????}
????public?void?getDogInfo(){
????????System.out.println("名字是:"+dog_name?+?"??年齡:"?+?dog_max_age);
????}
????//狗叫
????public?static?void?barking(){
????????System.out.println("汪汪汪~~~");
????}
}
JVM生成.class文件
一個java文件會在編譯期間被初始化生成.class字節(jié)碼文件,字節(jié)碼文件是專門給JVM閱讀的,我們平時吭哧吭哧寫的一行行代碼最終都會被編譯成機器能看懂的語句,這個文件后面會被類加載器加載到內(nèi)存。

類加載器加載.class文件
《深入理解Java的虛擬機》中大概有這么一句話:在虛擬機遇到一條new的指令時,會去檢查一遍在靜態(tài)常量池中能否定位到一個類的符號引用 (就這個類的路徑+名字),并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果不是第一次使用,那必須先執(zhí)行相應(yīng)的類加載過程,這個過程由類加載器來完成。
類加載字面意思就可以理解成加載class文件,更準確點的說法就是會把class文件變成一個二進制流加載到內(nèi)存中,即把類的描述信息加載到Metaspace,至于類加載器如何找到并把一個class文件轉(zhuǎn)成IO流加載到內(nèi)存中,我后面會專門寫一篇關(guān)于類加載器的文章,這里就只要理解創(chuàng)建對象中有這么一步就行了。不過這里面有很重要的概念不得不講:Class對象
知識擴展:Class對象
劃重點,這是個非常重要的概念,理解它對于理解后面的反射和代理會有很大的幫助
類加載器 ClassLoader 加載class文件時,會把類里的一些數(shù)值常量、方法、類信息等加載到內(nèi)存中,稱之為類的元數(shù)據(jù),最終目的是為了生成一個Class對象用來描述類,這個對象會被保存在.class文件里,可能有新手看到這里會比較懵逼,class也有對象?
當(dāng)然了,Class是個實實在在的類(用來描述類的類,比較拗口),有構(gòu)造方法( private ,意味著可以生成對象,但不能手動生成,由JVM自動創(chuàng)建Class對象),類加載器會給每個java文件都創(chuàng)建一個Class對象,用來描述類,我畫個圖:

//以下操作只能由jvm完成,我們手動做不了
Class?cls1?=?new?Class(Dog.class.getClassLoader());
Class?cls2?=?new?Class(Cat.class.getClassLoader());
Class?cls3?=?new?Class(People.class.getClassLoader());


這里面有個方法 newInstance(),即創(chuàng)建對象, 我把源代碼貼出來并簡單解析下
@CallerSensitive
public?T?newInstance()
????throws?InstantiationException,?IllegalAccessException
????{
????????if?(System.getSecurityManager()?!=?null)?{
????????????checkMemberAccess(Member.PUBLIC,?Reflection.getCallerClass(),?false);
????????}
????????if?(cachedConstructor?==?null)?{
????????????if?(this?==?Class.class)?{
????????????????throw?new?IllegalAccessException(
????????????????????"Can?not?call?newInstance()?on?the?Class?for?java.lang.Class"
????????????????);
????????????}
????????????try?{
????????????????Class<?>[]?empty?=?{};
????????????????//聲明無參構(gòu)造對象
????????????????final?Constructor<T>?c?=?getConstructor0(empty,?Member.DECLARED);
????????????????//?Disable?accessibility?checks?on?the?constructor
????????????????//?since?we?have?to?do?the?security?check?here?anyway
????????????????//?(the?stack?depth?is?wrong?for?the?Constructor's
????????????????//?security?check?to?work)
????????????????java.security.AccessController.doPrivileged(
????????????????????new?java.security.PrivilegedAction<Void>()?{
????????????????????????public?Void?run()?{
????????????????????????????????c.setAccessible(true);
????????????????????????????????return?null;
????????????????????????????}
????????????????????????});
????????????????cachedConstructor?=?c;
????????????}?catch?(NoSuchMethodException?e)?{
?????????????//如果class中沒有無參構(gòu)造方法,那么拋InstantiationException錯誤
????????????????throw?(InstantiationException)
????????????????????new?InstantiationException(getName()).initCause(e);
????????????}
????????}
????????Constructor<T>?tmpConstructor?=?cachedConstructor;
????????//?Security?check?(same?as?in?java.lang.reflect.Constructor)
????????int?modifiers?=?tmpConstructor.getModifiers();
????????if?(!Reflection.quickCheckMemberAccess(this,?modifiers))?{
????????????Class<?>?caller?=?Reflection.getCallerClass();
????????????if?(newInstanceCallerCache?!=?caller)?{
????????????????Reflection.ensureMemberAccess(caller,?this,?null,?modifiers);
????????????????newInstanceCallerCache?=?caller;
????????????}
????????}
????????//?Run?constructor
????????try?{
?????????//最終還是調(diào)用了無參構(gòu)造器對象的newInstance方法
????????????return?tmpConstructor.newInstance((Object[])null);
????????}?catch?(InvocationTargetException?e)?{
????????????Unsafe.getUnsafe().throwException(e.getTargetException());
????????????//?Not?reached
????????????return?null;
????????}
????}
首先搞清楚 newInstance 兩種方法區(qū)別:
Class.newInstance() 只能夠調(diào)用無參的構(gòu)造函數(shù),即默認的構(gòu)造函數(shù),我們在Class源碼里也看到了其實最終還是調(diào)用了無參構(gòu)造器對象 Constructor 的 newInstance 方法,舉個栗子:Dog.class 中是沒有無參構(gòu)造方法,那么會直接拋出 InstantiationException 異常:
//Dog類中只有一個dog_name的有參構(gòu)造方法
Class?c?=?Class.forName("com.service.ClassAnalysis.Dog");
Dog?dog?=?(Dog)?c.newInstance();//直接拋InstantiationException異常

Constructor.newInstance() 可以根據(jù)傳入的參數(shù),調(diào)用任意構(gòu)造構(gòu)造函數(shù),也可以反射私有構(gòu)造器(了解就行)
//Dog類中只有一個dog_name的有參構(gòu)造方法
Constructor?cs?=?Dog.class.getConstructor(String.class);
Dog?dog?=?(Dog)?cs.newInstance("小黑");//執(zhí)行沒有問題
但是這里面的 newInstance跟我們這次要說的 new 方法存在區(qū)別,兩者創(chuàng)建對象的方式不同,創(chuàng)建條件也不同:
-
使用
newInstance時必須要保證這類已經(jīng)加載并且已經(jīng)建立連接,就是已經(jīng)被類記載器加載完畢,而 new 不需要 -
class對象的
newInstance方法只能用無參構(gòu)造,上面已經(jīng)提到了,而 new 不需要 - 前者使用的是類加載機制,是一種方法,后者是創(chuàng)建一個新類,一種關(guān)鍵字
這個不能說newInstance 不方便,相反它在反射、工廠設(shè)計模式、代理中發(fā)揮了重要作用,后續(xù)我也會寫下代理和反射,因為理解起來確實有點繞。還有一點需要注意,不管以哪種方式創(chuàng)建對象,對應(yīng)的Class對象都是同一個
Dog?dog1?=?new?Dog("旺財");
Dog?dog2?=?new?Dog("小黑");
Class?c?=?Class.forName("com.service.classload.Dog");//為了測試,加了無參構(gòu)造
Dog?dog3?=?(Dog)?c.newInstance();
System.out.println(dog1.getClass()?==?dog2.getClass());
System.out.println(dog1.getClass()?==?dog3.getClass());

連接和初始化
在此階段首先為靜態(tài)static變量內(nèi)存中分配存儲空間,設(shè)立初始值(還未被初始化)比如:
public?static?int?i?=?666;//被類加載器加載到內(nèi)存時會執(zhí)行,賦予一個初始值
public?static?Integer?ii?=?new?Integer(666);//也被賦值一個初始值
但請注意,實際上i 的初始值是0,不是666,其他基本數(shù)據(jù)類型比如boolean的初始值就是false,以此類推。如果是引用類型的成員變量 ii 那么初始值就是null。
Dog?dog?=?new?Dog("旺財");//在這里打個斷點
執(zhí)行,首先會執(zhí)行靜態(tài)成員變量初始化,默認值是0:

但有例外,如果加上了 final 修飾詞那么初始值就是設(shè)定的值。

接著對已經(jīng)分配存儲空間的靜態(tài)變量真正賦值,比如為上面的dog_max_age 賦值16,還有執(zhí)行靜態(tài)代碼塊,也就是類似下面的代碼:
static?{
????System.out.println("狗狗的靜態(tài)代碼塊");
}
到這為止,類的加載過程才算完成。
創(chuàng)建實例
在加載類完畢后,對象的所需大小根據(jù)類信息就可以確認了,具體創(chuàng)建的步驟如下:
- 先給對象分配內(nèi)存(包括本類和父類的所有實例變量,不包括上面的靜態(tài)變量),并設(shè)置默認值,如果有引用對象那么會在棧內(nèi)存中申請一個空間用來指向的實際對象。
- 執(zhí)行初始化代碼實例化,先初始化父類再初始化子類,賦予給定值(尊重長輩是java的傳統(tǒng)美德)
- 對象實例化完畢后如果存在引用對象的話還需要把第一步的棧對象指向到堆內(nèi)存中的實際對象,這樣一個真正可用的對象才被創(chuàng)建出來。
說了這么多估計很多人都沒概念,懵逼狀態(tài)中,其實很簡單,我們只要記住new的創(chuàng)建對象就兩步:初始化和實例化,再給你們搞一張圖:可以簡單理解②③④為初始化⑤實例化(可惡,我這該死的責(zé)任感?。?/span>

本文不指望你能使勁弄懂java虛擬機底層加載創(chuàng)建對象的過程(其實有些步驟我都懶得講了,因為說出來也都非常理論化,沒多大意思),是想讓你知道對象的誕生過程有哪幾個重要概念參與了,弄懂這些概念比起單單知道對象創(chuàng)建的過程有意義的多:
- 類加載器,可以找找網(wǎng)上的資料,蠻多的,這塊內(nèi)容做個了解就行
- Class類和Class對象的概念,請重點掌握,不然理解反射和動態(tài)代理很費勁,spring的源碼也會難以理解
- 棧內(nèi)存和堆內(nèi)存以及對應(yīng)的基本類型和引用類型,也很重要,爭取能基本理解
來源:blog.csdn.net/qq_16887951/article/details/115872678
最近 熬夜給大家準備了非常全的一套Java一線大廠面試題。全面覆蓋BATJ等一線互聯(lián)網(wǎng)公司的面試題及解答,由BAT一線互聯(lián)網(wǎng)公司大牛帶你深度剖析面試題背后的原理,不僅授你以魚,更授你以漁,為你面試掃除一切障礙。

資源,怎么領(lǐng)?。?/span>
掃二維碼,加我微信,備注:面試題
一定要備注:面試題,不要急哦,工作忙完后就會通過!
