解析java創(chuàng)建對象的全過程
一、java創(chuàng)建對象的幾種方式
1.1、使用new關(guān)鍵字
調(diào)用類的構(gòu)造方法創(chuàng)建對象

1.2、反射創(chuàng)建對象
1.2.1、Class.newInstance創(chuàng)建對象

1.2.2、調(diào)用構(gòu)造器再去創(chuàng)建對象Constructor.newInstance
先通過反射獲取類中無參構(gòu)造器,然后通過newInstance()獲取對象

1.3、clone實現(xiàn)
通過Clone創(chuàng)建對象,首先實體類中必須先實現(xiàn)Cloneable接口并復(fù)寫Object的clone方法(因為Object的這個方法是protected的)


1.4、反序列化
序列化:指把 Java 對象轉(zhuǎn)換為字節(jié)序列的過程;
反序列化:指把字節(jié)序列恢復(fù)為 Java 對象的過程;
此方式需要類先實現(xiàn)Serializable接口

public class TestStack {
public static void main(String[] args) throws Exception {
File file =new File("M:/Serializable.txt");
FileOutputStream fileOutputStream = new FileOutputStream(file);
ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream);
UserParam userParam =new UserParam("hello");
outputStream.writeObject(userParam);
FileInputStream fileInputStream = new FileInputStream(file);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
UserParam userParam1 = (UserParam)objectInputStream.readObject();
userParam1.setNickName("world");
System.out.println(userParam1);
}
}二、創(chuàng)建對象的過程
當(dāng)Java虛擬機(jī)遇到一條字節(jié)碼new指令時:
1、檢查類是否已經(jīng)被加載
去常量池中查找該引用所指向的類有沒有被虛擬機(jī)加載,如果沒有被加載,那么會進(jìn)行類的加載過程。類的加載過程需要經(jīng)歷:加載、鏈接、初始化三個階段。對象的大小,在類加載完成時確定。(jdk1.8中,運(yùn)行時常量池、類常量池存在于方法區(qū)中。)
2、 為對象分配內(nèi)存空間
JVM為對象分配空間,即把一塊確定大小的內(nèi)存塊從Java堆中劃分出來。
2.1、分配空間的方式
1、指針碰撞
假設(shè)Java堆中內(nèi)存是絕對規(guī)整的,所有被使用過的內(nèi)存都被放在一邊,空閑的內(nèi)存被放在另一邊,中間放著一個指針作為分界點的指示器,那所分配內(nèi)存就僅僅是把那個指針向空閑空間方向挪動一段與對象大小相等的距離。
①正常情況

②給對象分配內(nèi)存后

這種方式的優(yōu)點是工作簡單,效率高,只需要移動指針就可以分配內(nèi)存空間。
缺點也很明顯:由于用指針碰撞分配內(nèi)存空間分為兩步:
- 1、讀取指針當(dāng)前的位置。
- 2、根據(jù)自身大小移動指針,不是原子操作,對象創(chuàng)建在虛擬機(jī)中是非常頻繁的操作,在并發(fā)情況下,會導(dǎo)致執(zhí)行讀操作或執(zhí)行寫操作的結(jié)果與預(yù)設(shè)的結(jié)果不一致(指針劃分不一致)。
例如:線程A要給對象分配8kb,讀取到指針當(dāng)前的位置,時間片用完,切換到線程B,線程B要給它的對象分配16kb,也讀取到指針當(dāng)前的位置(和線程A讀取到的一樣),將指針向空閑內(nèi)存方向移動16kb大小,線程B時間片用完,切換到線程A繼續(xù)執(zhí)行,由于線程A使用的指針位置還是之前讀到的。(線程不安全問題)
③針對指針碰撞線程不安全,有兩種方案:
1、同步處理(加鎖)分配內(nèi)存空間行為
采用 CAS 分配重試的方式來保證更新操作的原子性
2、把內(nèi)存分配行為按照線程,劃分在不同的內(nèi)存空間進(jìn)行
- 即每個線程在Java堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB),哪個線程要分配內(nèi)存,就在哪個線程的本地緩沖區(qū)中分配,只有本地緩沖區(qū)用完了,分配新的緩存區(qū)時才需要同步鎖定
- 虛擬機(jī)是否使用TLAB,可以通過-XX:+/-UseTLAB參數(shù)來設(shè)定。
2、空閑列表
如果Java堆中的內(nèi)存并不是規(guī)整的,已被使用的內(nèi)存和空閑的內(nèi)存相互交錯在一起,那就沒有辦法簡單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個列表,記錄上哪些內(nèi)存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄。
3、怎么選擇分配方式
兩種方式的選擇由 Java 堆是否規(guī)整決定,Java 堆是否規(guī)整是由選擇的垃圾收集器是否具有壓縮整理能力決定的。
①將內(nèi)存空間初始化為零值
內(nèi)存分配完成之后,虛擬機(jī)必須將分配到的內(nèi)存空間(但不包括對象頭)都初始化為零值。零值初始化意思就是對對象的字段賦0值,或者null值,這也就解釋了為什么這些字段在不需要進(jìn)程初始化時候就能直接使用。
如果使用了TLAB的話,這一項工作也可以提前至TLAB分配時順便進(jìn)行。
②對對象進(jìn)行必要的設(shè)置
例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭中。
從虛擬機(jī)的視角來看,一個新的對象已經(jīng)產(chǎn)生了。但是從Java程序的視角看來,對象創(chuàng)建才剛剛開始——構(gòu)造函數(shù),即Class文件中的()方法還沒有執(zhí)行,所有的字段都為默認(rèn)的零值,對象需要的其他資源和狀態(tài)信息也還沒有按照預(yù)定的意圖構(gòu)造好。
③執(zhí)行實例的初始化方法init
init方法包含成員變量、構(gòu)造代碼塊的初始化,按照聲明的順序執(zhí)行,執(zhí)行對象的構(gòu)造方法,并把堆內(nèi)對象的首地址賦值給引用變量。至此,對象創(chuàng)建成功。

三、注意事項
并發(fā)情況下,需要考慮操作的步驟是不是原子性,如果不是,就要加鎖。
原子性就是動作不能再繼續(xù)被拆分了,讀是原子性,寫也是原子性,但是讀加上寫就不是原子性。
四、總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Docker與Kubernetes部署Java應(yīng)用容器化實踐指南
容器化是一種將應(yīng)用程序及其依賴打包成一個可移植的單元的方法,容器能夠在任何環(huán)境中運(yùn)行,并且具有隔離性、可擴(kuò)展性和高效性,這篇文章主要介紹了Docker與Kubernetes部署Java應(yīng)用容器化實踐指南的相關(guān)資料2026-05-05
jcl與jul?log4j1?log4j2?logback日志系統(tǒng)機(jī)制及集成原理
這篇文章主要介紹了jcl與jul?log4j1?log4j2?logback的集成原理,Apache?Commons-logging?通用日志框架與日志系統(tǒng)的機(jī)制,有需要的朋友可以借鑒參考下2022-03-03
Spring構(gòu)造器注入及@Autowired、lombok的@RequiredArgsConstructor的異同點說明
本文介紹了Spring框架中構(gòu)造器注入的實現(xiàn)原理,包括Spring如何找到參數(shù)、構(gòu)造器注入與Autowired的區(qū)別,以及為什么Spring官方推薦構(gòu)造器注入,文章還強(qiáng)調(diào)了構(gòu)造器注入中使用final關(guān)鍵字的好處,并提供了代碼示例,幫助讀者更好地理解和應(yīng)用Spring的最佳實踐2026-03-03

