一 對象創(chuàng)建過程
在Java程序運行中無時無刻都有對象被創(chuàng)建出來,在Java語言層面拳亿,僅僅是一個new關鍵字而已晴股。
而當虛擬機遇到一條new指令時,會進行一序列對象創(chuàng)建的操作肺魁。
1电湘、檢查常量池中是否有即將要創(chuàng)建的這個對象所屬類的符號引用。
1)如果常量池中沒有這個類的符號引用,說明這個類沒有被定義寂呛,則拋出ClassNotFoundException異常怎诫;
2)如果有這個類的符號引用,則進行下步處理贷痪;
2幻妓、檢查這個符號引用所代表的類是否已經(jīng)被JVM加載。
1)如果該類還沒有被加載劫拢,就找到該類的class文件肉津,并加載進方法區(qū);
2)如果該類已經(jīng)被JVM加載舱沧,則準備為對象分配內(nèi)存妹沙;
3、根據(jù)方法區(qū)中該類的信息確定該類所需的內(nèi)存大小狗唉。
對象所需內(nèi)存大小在類加載完成后便可以完全確定初烘,為對象分配空間的任務等同于把一塊確定
大小的內(nèi)存從Java堆中劃分出來。
4分俯、從堆中劃分一塊對應大小的內(nèi)存空間給新的對象肾筐。
分配堆中內(nèi)存有兩種算法方式:
1)指針碰撞
如果JVM垃圾收集器采用復制算法或標記-整理算法等帶整理功能的收集器,則Java堆中的內(nèi)存是絕對規(guī)整的缸剪,
用過的內(nèi)存放在一邊吗铐,空閑的內(nèi)存放在另一邊,中間放著一個指針作為分界點的指示器杏节,那所需要分配的內(nèi)存
就僅僅是把指示器指針向空閑空間那邊挪動一段與對象大小相等的距離唬渗,這種分配方式稱為"指針碰撞"(Bump the Pointer)。
2)空閑列表
如果JVM的垃圾收集器采用標記-清除算法奋渔,Java中內(nèi)存不是規(guī)整的镊逝,已使用內(nèi)存和未使用內(nèi)存相互交錯,
就沒法通過簡單的指針碰撞來為對象分配內(nèi)存嫉鲸,虛擬機針對這種交錯的內(nèi)存維護了一個列表撑蒜,記錄哪些內(nèi)存塊
是可用的,在分配的時候找到一塊足夠大的空間劃分給對象實例玄渗,并更新列表上的記錄座菠,
這種分配方式成為"空閑列表"(Free List)。
總之選擇哪種對象內(nèi)存分配算法是由JVM的垃圾回收算法決定的浴滴。除了劃分內(nèi)存外,還有一個問題岁钓,
虛擬機創(chuàng)建對象非常頻繁升略,即使僅僅是修改一個指針所指向的位置,在并發(fā)情況下也并不是線程安全的品嚣,
可能出現(xiàn)正在給對象A分配內(nèi)存骂远,指針還沒來得及修改,對象B又同時使用了原來的指針分配內(nèi)存的情況腰根。
有兩種方案解決這個問題:
1)對分配內(nèi)存空間的動作做同步處理(虛擬機采用CAS算法配上失敗重試的方式保證更新操作的原子性);
2)把內(nèi)存分配的動作按照線程劃分在不同的空間中進行拓型,即每個線程在Java堆中預先分配一小塊內(nèi)存额嘿,
稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)劣挫。哪個線程要分配內(nèi)存册养,
就在哪個線程的TLAB上分配,只有TLAB用完压固,分配新的TLAB的時候才需要同步鎖定球拦。
虛擬機是否使用TLAB,可以通過-XX:+/-UseTLAB參數(shù)來設定帐我。
5坎炼、內(nèi)存分配完成后,虛擬機需要為對象中的成員變量賦上初始值(默認初始化)拦键。
這么做是為了保證對象的實例字段在Java代碼中可以不賦初始值就直接使用谣光。
6、設置對象頭中的信息芬为。
虛擬機需要對對象進行必要的設置萄金,比如這個對象是哪個類的實例、如何才能找到類的元數(shù)據(jù)信息媚朦、
對象的哈希碼氧敢、對象的GC分代年齡等等。這些信息存放在對象的對象頭(Object Header)中询张。
根據(jù)虛擬機運行狀態(tài)不同孙乖,對象頭也會有不同的設置。
在上面工作完事以后瑞侮,從虛擬機角度看對象已經(jīng)產(chǎn)生了的圆,但是從Java程序看,對象創(chuàng)建才剛剛
開始<init>方法還沒執(zhí)行半火,所有的字段都還為零越妈。所以,一般來說(由字節(jié)碼中是否跟隨invokeSpecial指令所決定)钮糖,
執(zhí)行new命令后會接著執(zhí)行<init>方法梅掠,把對象按照Java程序進行初始化酌住,這樣,真正可用的對象才算完全產(chǎn)生出來阎抒。
總結(jié):
符號引用檢查-->JVM類加載-->對象空間大小確定-->分配內(nèi)存給新對象
-->對象賦初始值-->對象頭信息設置-->最終初始化為真正可用對象酪我。
二 對象內(nèi)存模型
在HotSpot虛擬機中,對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:
對象頭(Object Header)
實例數(shù)據(jù)(Instance Data)
對齊填充(Padding)
1且叁、對象頭
對象頭包含兩部分信息都哭,第一部分用于存儲對象運行時數(shù)據(jù),如哈希碼(HashCode)逞带、GC分代年齡欺矫、
鎖狀態(tài)標識、線程持有的鎖展氓、偏向線程ID穆趴、偏向時間戳等等。
對象頭的另一部分存儲的是類型指針遇汞,即對象指向它的類元數(shù)據(jù)的指針未妹,虛擬機通過這個指針來
確定這個對象是哪個類的實例。如果對象是一個數(shù)組空入,那么對象頭中還要包含數(shù)組長度络它。
2、實例數(shù)據(jù)
實例數(shù)據(jù)部分就是成員變量的值执庐,其中包含父類的成員變量和本類的成員變量酪耕。
3、對齊填充
對齊填充并不是必然存在的轨淌,也沒有特別的含義迂烁,僅僅起著占位符的作用。HotSpot要求對象的
總長度必須是8字節(jié)的整數(shù)倍递鹉。由于對象頭一定是8字節(jié)的整數(shù)倍盟步,但實例數(shù)據(jù)部分的長度是任意的,
因此需要對齊補充字段確保整個對象的總長度為8的整數(shù)倍躏结。
三 訪問對象過程
我們知道却盘,引用類型的變量中存放的是一個地址,那么根據(jù)地址類型的不同媳拴,對象有不同的訪問方式黄橘。
1、句柄訪問方式
堆中需要有一塊叫做“句柄池”的內(nèi)存空間屈溉,用于存放所有對象的地址和所有對象所屬類的類信息塞关。
引用類型的變量存放的是該對象在句柄池中的地址。訪問對象時子巾,首先需要通過引用類型的變量找到該對象的句柄帆赢,
然后根據(jù)句柄中對象的地址再訪問對象小压。如圖所示:
2、直接指針訪問方式
如果使用直接指針訪問椰于,那么Java堆對象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)相關信息怠益,
而reference中存儲的直接就是對象地址,從而不需要句柄池瘾婿,通過引用能夠直接訪問對象蜻牢。
但對象所在的內(nèi)存空間中需要額外的策略存儲對象所屬的類信息的地址。如圖所示:
這兩種訪問方式各有優(yōu)勢偏陪,使用句柄訪問的最大好處就是reference中存儲的是穩(wěn)定的句柄地址孩饼,
在對象移動時只會改變句柄中的實例數(shù)據(jù)指針,而虛擬機棧局部變量表里面的reference引用不需要改變竹挡。
使用直接指針訪問方式的最大好處就是速度更快,它節(jié)省了一次指針定位的時間開銷立膛,由于對象的訪問
在Java中非常頻繁揪罕,因此此類開銷累計執(zhí)行成本也不低。HotSpot使用第二種方式進行對象訪問宝泵,
某些語言或框架使用句柄訪問對象的也不少好啰。
————————————————
版權(quán)聲明:本文為CSDN博主「街燈下的校草」的原創(chuàng)文章,遵循CC 4.0 BY-SA版權(quán)協(xié)議儿奶,轉(zhuǎn)載請附上原文出處鏈接及本聲明框往。
原文鏈接:https://blog.csdn.net/yhl_jxy/article/details/80893288