本文會主要介紹對象創(chuàng)建并簡要介紹對象創(chuàng)建過程中的一些操作 虛擬機HotSpot
對象創(chuàng)建
虛擬機接受到一個new指令時
- 首先檢查參數(shù)能否在常量池內(nèi)定位到類的符號引用轨淌,并判斷類是否已經(jīng)被加載,解析和初始化(類的加載過程見附1)
- 接著虛擬機為對象分配內(nèi)存迄靠,分配內(nèi)存的大小在類加載完成時就已經(jīng)確定瞒窒。(分配內(nèi)存的方法見附2)
- 內(nèi)存分配完成后,將內(nèi)存空間全部賦零值(不包括對象頭)
- 接下來完成對對象頭的設(shè)置 包括屬于哪個類 如何找到類元數(shù)據(jù) 哈希碼 GC分代年齡等信息(對象頭詳細內(nèi)容見附3)
- 調(diào)用對象的<init>方法 完成初始化
至此 一個完整的Java對象就被創(chuàng)建完成了
注意:多線程使用同一對象時需要注意對象創(chuàng)建時指令重排序的問題
附錄1 類加載
需要類初始化的五種情況(有且僅有這五種) (類的加載沒有硬性規(guī)定)
- 遇到new getstatic putstatic invokestatic這四個指令時 如果類沒有初始化,則會觸發(fā)類的初始化兵罢。四個指令對應(yīng)的最常見的情況為使用new關(guān)鍵字實例化對象,讀取靜態(tài)變量滓窍,設(shè)置靜態(tài)變量(編譯期結(jié)果放入常量池的除外 會觸發(fā)前幾個階段) 卖词,調(diào)用類的靜態(tài)方法
- 使用java.lang.reflect包的方法對類進行反射調(diào)用
- 初始化一個類時 如果父類未初始化 則初始化父類
- 虛擬機啟動時指定的主類
- java1.7動態(tài)語言支持
常見的不會觸發(fā)初始化的引用方式
- 通過子類引用父類的靜態(tài)變量 只會初始化父類 不會初始化子類
- 創(chuàng)建類的數(shù)組
- 引用常量池內(nèi)的變量
加載
通過名字獲取類的二進制流并在內(nèi)存中生成class對象拢操。
驗證
驗證二進制流的正確性和安全性 包括文件格式驗證锦亦,元數(shù)據(jù)驗證,字節(jié)碼驗證以及符號引用驗證四個步驟令境。
準(zhǔn)備
給類變量(static)分配空間并完成初始化 注意:如果不是final變量杠园,值均為零值。
解析
將符號引用解析為直接引用舔庶,包括類或接口的解析抛蚁,字段解析,類方法解析惕橙,接口方法解析瞧甩。
初始化
執(zhí)行類的<cinit>()方法,這個方法由所有對靜態(tài)變量的賦值操作和所有靜態(tài)代碼塊組成弥鹦,虛擬機會保證父類的<cinit>()方法在子類的方法開始之前結(jié)束肚逸,并且提供線程安全的保證(類似于double check,多個線程同時初始化時只有一個線程進入方法彬坏,其他線程阻塞朦促,執(zhí)行完成后其他線程不會再進入方法)
類加載器-雙親委派模型
Java推薦的類加載器的實現(xiàn)模型,除了啟動類加載器(bootstrap classLoader)以外的所有類加載器都應(yīng)該擁有父加載器栓始,這個關(guān)系不是通過繼承來實現(xiàn)务冕,而是通過組合的方式。類加載器收到加載請求時幻赚,首先請求父加載器進行加載禀忆,如果父加載器不能加載則調(diào)用自己的加載方法。自定義類加載器時如果我們希望遵從雙親委派模型則重寫findClass()方法落恼,否則重寫loadClass()方法
類加載器-分類
遵循雙親委派從上到下可以分為
- 啟動類加載器 (Bootstrap classLoader) 加載<JAVA_HOME>\lib下的指定文件名的類
- 擴展類加載器 (Extension classLoader) 加載<JAVA_HOME>\lib\ext下的類
- 系統(tǒng)類加載器(應(yīng)用類加載器) 加載Classpath內(nèi)的類
- 自定義類加載器
不遵循雙親委派的常見類加載器:
- SPI (Service Provider Interface)加載器 - 線程上下文加載器 實現(xiàn)父加載器向子加載器請求加載
- OSGi模塊化加載器 每個模塊擁有一個自定義類加載器 網(wǎng)狀結(jié)構(gòu)的加載過程
附錄2 內(nèi)存的分配
1.確定對象內(nèi)存大小
對象的大小在類加載完成后就已經(jīng)確定油湖,對象在內(nèi)存中可以分為三塊
- 對象頭(附錄三)
大小確定 與類無關(guān) 與操作系統(tǒng)有關(guān)
- 實例數(shù)據(jù)
即使父類的實例字段被子類覆蓋或者被private修飾,都照樣為其分配內(nèi)存领跛,相同寬度的字段會分配在一起乏德,其次,父類的字段在子類之前
- 對齊填充
滿足虛擬機對8的倍數(shù)的要求
2.分配內(nèi)存
內(nèi)存分配與內(nèi)存回收緊密相關(guān),根據(jù)不同的回收策略也有不同的分配策略喊括。
如果采用的是具有壓縮過程的垃圾回收策略胧瓜,如Serial,ParNew,則Java堆中的內(nèi)存是規(guī)整的郑什,我們只需要將內(nèi)存指針向后移內(nèi)存大小的位置即可府喳,這種方式稱為指針碰撞(Bump the Pointer)。如果采用的回收策略沒有壓縮過程蘑拯,如CMS钝满,那虛擬機就需要維護一個列表,記錄哪些內(nèi)存是可用的申窘,這種方式稱為空閑列表(Free List)
其次弯蚜,對象創(chuàng)建也需要考慮線程安全的問題,一種方案是采用CAS+失敗重試的方法來保證線程安全剃法,另一種方法則是為每一個線程提前分配一塊內(nèi)存碎捺,稱為本地線程分配緩沖(Thread Local Allocation Buffer , TLAB),線程創(chuàng)建對象時優(yōu)先在自己的TLAB上分配贷洲。
附錄3 對象頭
對象頭包括
- MarkWord 32bit/64bit 取決于操作系統(tǒng)
- 類型指針 指向類元數(shù)據(jù)的指針
- 數(shù)組長度 如果是數(shù)組的話
我們主要介紹MarkWord
根據(jù)鎖狀態(tài)的不同收厨,markword會復(fù)用自己的空間,分別記錄一些不同的信息优构。
我們注意到 輕量級鎖和重量級鎖狀態(tài)時诵叁,會將分代年齡覆蓋掉,那當(dāng)鎖狀態(tài)解除時钦椭,要怎么恢復(fù)呢黎休?
答案是上鎖時,鎖的數(shù)據(jù)中會保存一份原markword的備份