一文掌握虛擬機創(chuàng)建對象的秘密
勿在流沙筑高臺,出來混遲早要還的。
本文主要內容講解HotSpot虛擬機在Java堆中對象是如何創(chuàng)建、內存分配布局和訪問方式。
本文地圖:

一、給你創(chuàng)建一個對象
如果你是一直從第一季看過來的,那一定知道前面有個地方講過類的整個生命周期,之前只是講到了初始化階段,類是如何使用和類是如何被卸載還沒有進行講解!那本文就簡單介紹一下類的使用,我們new 一個 “如花” 似玉的girl!
這里再回顧一下,類從被加載到虛擬機內存中開始,到卸載出內存為止,它的生命周期包括了七個階段:
- 加載(Loading)
- 驗證(Verification)
- 準備(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸載(Unloading)
在Java中我們用使用一個類,很多時候是創(chuàng)建這個類的一個實例,也就是常說的創(chuàng)建一個對象。其實在Java程序運行過程中,無時無刻都有對象被創(chuàng)建出來。創(chuàng)建對象(如克隆、反序列化)通常僅僅是一個new關鍵字而已。但是在Java虛擬機中一個對象(只是普通的java對象,不包括數(shù)組和Class對象等)的創(chuàng)建是怎么一個過程呢?
第一:虛擬機遇到一條new指令時,首先會去檢查這個指令的參數(shù)是否能夠在常量池中定位到一個類的符號引用。然后檢查這個符號引用代表的類是否已經被加載、解析和初始化過。如果沒有進行類加載則執(zhí)行相應的類加載的過程。 記住:要new對象,要先加載類!
第二:類加載檢查通過后,虛擬機將為新生的對象分配內存。對象所需的內存大小在類加載的時候便可以完全確定(如何確定對象的下文說明) 。為對象分配內存的任務等同于把一塊確定大小的內存從Java堆中劃分出來。分配方式有 “指針碰撞” 和 “空閑列表” 兩種,選擇那種分配方式由 Java 堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定(對象在堆上的劃分,這是個復雜的問題,后文繼續(xù)探討,這里只要明白是在對象是在堆上分配內存即可)。 記?。阂猲ew對象,要有先分配內存空間!
第三:內存分配完成,虛擬機需要將分配的內存空間都初始化為零值(零值這個概念之前文章也介紹過,這里就不再說明),這一步的操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,因為程序能訪問這些字段的數(shù)據(jù)類型對應的零值。 記住:要new對象,虛擬機會幫你為對象的實例字段自動賦予零值!
第四:虛擬機要對對象進行必要的設置,如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息(JDK7是方法區(qū)保存)、對象的哈希碼、對象的GC分代年齡等信息。這些信息都存放在對象的對象頭(Object Header)中。
上面工作都完成之后,在虛擬機看來,一個對象就已經產生了。但是從Java程序的角度看,對象的創(chuàng)建才剛剛開始,因為
記住:對象不是你想new,想new就可以new的!
下面用通過圖解的例子簡單說明(版本jdk1.7):
第一: 一個PrettyGirl類!
- public class PrettyGirl {
- /**
- * 姑娘姓字名誰
- */
- String name;
- /**
- * 芳齡幾何
- */
- int age;
- /**
- * 家住何方
- */
- static String address;
- /**
- * 可曾婚配否
- */
- boolean marry;
- void sayHello(){
- System.out.println("Hello...");
- }
- @Override
- public String toString() {
- return "PrettyGirl{" +
- "name='" + name + '\'' +
- ", age=" + age +
- ", marry=" + marry +
- '}';
- }
- }

方法區(qū)除了保存類的結構,還保存靜態(tài)屬性與靜態(tài)方法。編寫中小型程序時,一般不會造成方法區(qū)的內存溢出!在JDK1.8 沒有方法區(qū)的概念,前面文章中也有提到,這里為了講解使用圖解還是JDK1.7!
第二:實例化new兩個漂亮女孩!
- public static void main(String[] args) {
- PrettyGirl pg1 = new PrettyGirl();
- pg1.name = "Alice";
- pg1.age = 18;
- pg1.address = "changsha";
- PrettyGirl pg2 = new PrettyGirl();
- pg2.name = "Alexia";
- pg2.age = 28;
- System.out.println(pg1 + " ---" + pg1.address);
- System.out.println(pg2 + "----" + pg2.address);
- }
- ----打印結果:--------
- PrettyGirl{name='Alice', age=18, marry=false} ---changsha
- PrettyGirl{name='Alexia', age=28, marry=false}----changsha

在棧內存為 pg1 變量申請一個空間,在堆內存為PrettyGirl對象申請空間,初始化完畢后將其地址值返回給pg1 ,通過pg1 .name和pg1 .age修改其值,靜態(tài)的變量address是類公有的!
堆存放對象持有的數(shù)據(jù),同時保持對原類的引用。可以簡單的理解為對象屬性的值保存在堆中,對象調用的方法保存在方法區(qū)。
從上圖也可以看到有一個區(qū)域是棧,在程序運行的時候,每當遇到方法 調用時候,Java虛擬機就會在棧中劃分一塊內存稱為棧幀(線程私有,堆和方法區(qū)線程共享的)。就如上面的程序,在調用main方法的時候,會創(chuàng)建一下棧,棧幀中的內存供局部變量(包括基本類型和引用類型)使用,基本類型和引用類型后文會詳情介紹。當方法調用結束后,虛擬機會回收次棧幀占用的內存。
tips: 回顧
1、堆內存溢出會發(fā)生 OutOfMemoryError 錯誤,提示信息“Java heap Space”。
2、在棧中會有兩個異常:
- 如果線程請求的棧的深度大于虛擬機所允許的最大深度,將拋出StackOverflowError 異常(遞歸可能會導致此異常)!
- 如果虛擬機在擴展棧時候無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。
3、如果有方法區(qū) 也會出現(xiàn)OutOfMemoryError 錯誤,提示信息 “PermGen space”。(JDK8 后無此錯誤提示)
每個區(qū)域都有一些參數(shù)可以設置,參數(shù)學習續(xù)持續(xù)更新!
二、對象的內存布局
感慨,創(chuàng)建一個對象還是挺不容易的!
在HotSpot虛擬機中,對象在內存中的布局可以分為3塊區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance data)和對象填充(Padding)。
那下面就對這三塊區(qū)域進行簡單介紹:
1、對象頭- 還是一個看臉的時代!
對象頭包括兩部分信息。第一部分用于存儲對象自身的運行時數(shù)據(jù),如
- 哈希碼(HashCode),一個對象的hashcode是唯一的,如判斷一個對象是不是單例的!
- GC分代年齡(標明是新生代還是老年代..)
- 鎖狀態(tài)標志、線程持有的鎖、偏向線程ID(多線程,同步的時候用到)
- 其他等等….
注: 上面的幾個點,要結合和關聯(lián)其他相關知識,理解會更加深入一點。
如 哈希碼hashCode,對下面兩個問題如果你又自己的一些思考,歡迎留言探討!
- 重寫了equals 必須要重寫hashcode,思考一下,為什么?如果不重寫在使用HashMap的時候會有出現(xiàn)什么問題?
- HashMap中相同key存入數(shù)據(jù)不替換,而是進行疊加存儲,怎么實現(xiàn)?
問題2提示:只要重寫了key的hashCode()和Map的put()方法,其實就可以實現(xiàn)對于相同key下疊加存儲不同的value了。
第二部分是類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過指針來確定這個對象是那個類的實例。(就如我們上圖的箭頭,可以簡單理解為指針!)
說明:
(1)、并不是所有的虛擬機實現(xiàn)都是必須在對象數(shù)據(jù)上保留類型指針,也就是查找對象的元數(shù)據(jù)并一定經過對象本身!
(2)、如果對象是一個Java數(shù)組,那在對象頭中還必須有一塊用于記錄數(shù)組長度的的數(shù)據(jù),因為虛擬機可以通過普通Java對象的元數(shù)據(jù)確定Java對象的大小,但是從數(shù)組的元數(shù)據(jù)卻無法確定數(shù)組的大小。
2、實例數(shù)據(jù)-了解了外在美,還要注重內在美!
實例數(shù)據(jù)部分是對象真正存儲的有效信息,也就是程序代碼中定義的各種類型的字段內容。
不論是從父類繼承下來的,還是在子類中定義的,都需要記錄起來。記錄的存儲順序會受到虛擬機分配策略參數(shù)和字段在Java源碼中的定義的順序相關。
3、對齊填充-對齊填充成為標準網紅!
對象的填充并不是必然存在的,也沒有特別的含義,它僅僅起著占位符的作用!由于HotSpot VM的自動內存管理系統(tǒng)要求兌現(xiàn)的起始地址必須是8字節(jié)的整數(shù)倍,也就是說對象的大小必須是8字節(jié)的整數(shù)倍。而對象頭部分正好是8字節(jié)的整數(shù)倍,因此當對象實例數(shù)據(jù)部分沒有對齊時候,就需要填充來補全。
(類比記憶對齊填充,由于審美的標準,有一些人天生就是俊俏的臉蛋和好的身材,不需要進行其他的填充,有一些人可能有好看的臉蛋,但是某些地方和標準還差點意思,就需要填充來達到標準)
tips:字節(jié)
字節(jié)(byte)計算機里用來存儲空間的基本計量單位。8個二進制位(bit)構成了一個字節(jié)(byte)即1byte=8bit。
三、如何“約”(定位)一個對象
認識了一個對象后,不能總是聊微信,也要約一下吃個飯啥的! 那在Java中建立了一個對象,那肯定是要使用對象的。Java程序是如果找到具體的對象的呢?
在Java程序中需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對象(如開篇的圖示,棧上面的引入指向堆中具體對象)。但是由于Reference類型在Java虛擬機規(guī)范中只規(guī)定了一個指向對象的引用,并沒有定義這個引用應該通過何種方式去定位、訪問堆中的對象的具體位置,所以對象訪問方式也是取決于虛擬機實現(xiàn)而定的。
目前主流的訪問方式有使用句柄和直接指針兩種。
第一:句柄
使用句柄訪問,在Java對中將會劃分出一塊內存來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象的實例數(shù)據(jù)與類型數(shù)據(jù)各自 的具體地址信息,如圖,

第二:直接指針
使用直接指針,在Java堆對象的布局中就必須考慮如果放置訪問類型數(shù)組的相關信息,而reference中存儲的直接就是對象的地址,如圖:

兩種方式都各自優(yōu)勢,簡單總結:
句柄:最大的好處就是reference中存儲的是穩(wěn)定的句柄地址,在對象被移動(垃圾收集移動對象是非常普通的行為)時只會改變句柄中的實例數(shù)據(jù)指針,而Reference本身不需要修改。
直接指針:最大的好處就是速度更快,它節(jié)省一次指針定位的開銷,在Java中對象的訪問是非常頻繁的,因此能減少這類開銷對提高性能還是非常客觀的。
虛擬機Hotspot使用的就是直接指針這種方式。但是其他的語言和框架中使用句柄的情況也很常見!
四、本文總結
本文主要整理了Java中一個對象的創(chuàng)建,對象的內存布局以及如何定位一個對象! 也讓我們知道對象不是你想new就可以new的,new出的對象想要“約”也是有不同方式的。