對于前面幾篇文章, 主要就是說明了一個.java文件是如何一步步編譯, 解析最后加載到JVM中運行的, 那么本篇文章將說明對象是如何創建的, 包括創建過程、對象頭與指針壓縮、jvm對象內存分配詳解、逃逸分析,線上分配,標量替換等等內容。
內容有點多,所以準備分為三篇文章來寫:
如果感覺文章中有的圖片字太小不清楚的可以通過公眾號加我,然后說明是哪篇文章的圖片,然后我發給你。
對象創建的主要流程:
圖片
虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。new指令對應到語言層面上講是,new關鍵詞、對象克隆、對象序列化等。
對于我們來說,我們寫的java代碼是new 一個對象,實際上對于底層jvm實際上是執行了一個new 指令。
這里用的插件是:jclasslib Bytecode Viewer
圖片
首先會判斷這個類有沒有被加載過,如果沒有加載過,那么它首先會執行加載類的過程(前幾篇文章有講),如果加載過了,那么就要開始new對象了,這個對象一般來說可能放在堆中也有可能放在棧里邊,但是不管放在哪,前提都是需要分配一塊內存空間的。
在類加載檢查通過后,接下來虛擬機將為新生對象分配內存。對象所需內存的大小在類 加載完成后便可完全確定,為對象分配空間的任務等同于把 一塊確定大小的內存從Java堆中劃分出來。
這個步驟有兩個問題:
劃分內存的方法:
如果Java堆中內存是絕對規整的,所有用過的內存都放在一邊,空閑的內存放在另一邊,中間放著一個指針作為分界點的指示器,比如下圖中藍色實線表示當前指針位置,虛線表示挪動后的位置,那所謂分配內存就僅僅是把那個指針向空閑空間那邊挪動一段與對象大小相等的距離。
圖片
如果Java堆中的內存并不是規整的,已使用的內存和空 閑的內存相互交錯,那就沒有辦法簡單地進行指針碰撞了,虛擬機就必須維護一個列表,記錄上哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例, 并更新列表上的記錄。
圖片
但是具體使用的是指針碰撞的方式還是使用的是空閑列表的分配方式,取決于使用的什么垃圾回收算法,如果使用的是標記整理的話,那么最終剩余的內存肯定是第一種,那么使用的也就是指針碰撞的方式,如果使用的是標記清除的話,那么最終剩余的內存肯定是第二種,所以就使用空閑列表的方式來分配內存。
解決并發問題的方法:
不管使用哪種方式分配,都會出現并發問題,也就是兩個線程同時創建了一個對象,然后爭搶同一塊內存
圖片
多個線程創建了多個對象,但是內存空間只有一塊,那么jvm為了解決這種并發問題,采取了以下兩種措施
虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性來對分配內存空間的動作進行同步處理。
CAS配上失敗重試也就是線程A和線程B同時爭搶這一塊內存,如果線程A先爭搶到了這塊內存,那么線程B重新進行分配,發現這塊內存分配給了線程A,然后就會在這塊內存后面進行內存分配操作。這樣線程A、B對象的內存空間就在并發的情況下被分配了。
把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中(比如Eden區)預先分配一小塊內存。通過-XX:+/-UseTLAB參數來設定虛擬機是否使用TLAB(JVM會默認開啟-XX:+UseTLAB)
那么這個內存也不可能特別大,好像默認是Eden區的1% , 通過-XX:TLABSize 可以指定TLAB大小。如果這個時候放不下了,那么就會恢復CAS配上失敗重試的方式進行分配。當然,一般不推薦你去改JVM默認的參數設置。
圖片
線程A和線程B在Eden區預先分配一塊屬于自己的內存空間,然后把各自的對象放到各自的空間種。JDK8默認使用的就是這種方式。
對象的分配過程會在下一篇文章詳細說明。
內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭), 如果使用TLAB,這一工作過程也可以提前至TLAB分配時進行。這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
也就是對于對象的成員變量,比如int initData = 666;那么在這個過程,會先給initData 賦一個0值,就和前面有一篇文章中提到過靜態變量的初始化賦值是一樣的。最終可能有一步會把真正的值666賦給initData。
初始化零值之后,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭Object Header之中。
在HotSpot虛擬機中,對象在內存中存儲的布局可以分為3塊區域:對象頭(Header)、 實例數據(Instance Data)和對齊填充(Padding)。HotSpot虛擬機的對象頭包括兩部分信息,第一部分用于存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時 間戳等。對象頭的另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
圖片
對象頭中有一個Mark Word標記字段,第一列是對象的一個狀態,可能有一些對象被加鎖了或者是被GC標記了,不同的對象,它對象頭的結構是不一樣的,比如說一個對象是正常的對象,也就是沒有任何的鎖,對象頭中前面25bit存儲是對象的hashCode,中間4bit存儲的是對象的分代年齡,分代年齡在上一篇文章中有講過,它是4bit,也就以為著它的分代年齡是<=15的,因為4位(bit)大小可以表示從0到15的數值,因此無法存儲大于15的數值,當然還有一些偏向鎖、鎖標志位等鎖的標記。關于鎖的相關內容也會在后面寫并發相關的文章的時候進行詳解。
還有一塊就是Klass Pointer類型指針,一個對象new出來之后是放到堆中的,但是在這個對象的頭部區域,有一個指針,指向方法區的對象所屬的類的元數據信息。如下圖中畫紅線的地方的示例。
圖片
比如說就是下面這一段代碼,要想在元數據區找到compute方法對應的代碼,就是通過這個類型指針Klass Pointer去找。
圖片
那么還有一個對象叫做類對象,比如Math類所屬的對象mathClass,這個對象是放在堆中的。
圖片
堆中的這個mathClass對象和元數據區的Math.class是什么關系呢?
Math.class是類的元數據信息,也就是我們編寫的代碼,那么mathClass是類裝載完之后,是jvm給我們開發人員在我們想訪問類的元數據信息是提供的一個對象,我們可以通過這個對象mathClass去訪問類的元數據信息,簡單一點就是反射,通過反射是可以獲取到很多信息的,類的名稱。方法的名稱等等。但是mathClass對象中是不會存儲這些代碼的,代碼只是存儲在方法區。
這個是jvm提供給我們開發人員去使用的,但是jvm內部不會這么干,而是通過剛剛講的類型指針。而元數據信息的存儲介質是C++對象,這個類型指針也是C++實現的。
還有一塊就是數組的長度,如果是一個數組對象的話,對象頭中還有一塊會存儲數組的長度。
圖片
對象頭在hotspot的C++源碼markOop.hpp文件里的注釋如下:
// Bit-format of an object header (most significant first, big endian layout below)://// 32 bits:// --------// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)// size:32 ------------------------------------------>| (CMS free block)// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)//// 64 bits:// --------// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)// size:64 ----------------------------------------------------->| (CMS free block)//// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
執行方法,即對象按照程序員的意愿進行初始化。對應到語言層面上講,就是為屬性賦值(注意,這與上面的賦零值不同,這是由程序員賦的值),和執行構造方法。
這一步的話比如就會把initData賦值為666, 因為在初始化零值這個步驟中initData被賦值為0,這一步可以說是真正的進行賦值。也就是下圖中框起來的部分,這個過程是C++調用的。
圖片
對象大小可以用jol-core包查看,引入依賴
<dependency> <groupId>org.openjdk.jol</groupId> <artifactId>jol-core</artifactId> <version>0.9</version></dependency>
以下這幾行代碼的話主要就是想查看new Object() 以及new int[]{}還有new A()對象的大小。
package com.liuxs.fusionx;import org.openjdk.jol.info.ClassLayout;/** * @author: Liu Yuehui * @ClassName: JOLSample * @date: 2023/11/27 0:25 * @description: 查看對象大小 * @version:v1:2023/11/27 0:25: **/public class JOLSample { public static void main(String[] args) { ClassLayout layout = ClassLayout.parseInstance(new Object()); System.out.println(layout.toPrintable()); System.out.println(); ClassLayout layout1 = ClassLayout.parseInstance(new int[]{}); System.out.println(layout1.toPrintable()); System.out.println(); ClassLayout layout2 = ClassLayout.parseInstance(new A()); System.out.println(layout2.toPrintable()); } // -XX:+UseCompressedOops 默認開啟的壓縮所有指針 // -XX:+UseCompressedClassPointers 默認開啟的壓縮對象頭里的類型指針Klass Pointer // Oops : Ordinary Object Pointers public static class A { //8B mark word //4B Klass Pointer 如果關閉壓縮-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,則占用8B int id; //4B String name; //4B 如果關閉壓縮-XX:-UseCompressedOops,則占用8B byte b; //1B Object o; //4B 如果關閉壓縮-XX:-UseCompressedOops,則占用8B }}
運行結果:
圖片
Object對象大概是可以分為以下幾塊
圖片
這里的類型指針只占了4個字節是因為64位系統默認是8字節,但是會涉及到指針壓縮,壓縮之后就是4字節。
這里有一個叫對象對齊,也就是上面說到對象頭的第三塊對齊填充(Padding),這塊部分有的時候有,有的時候沒有,也就是jvm內部會把內存的讀取信息按照8個字節對齊,這個是整個jvm底層包括計算機組成原理經過大量實踐證明的,也就是通過8個字節的對象的對齊,會讓整個計算機的存取效率非常之高。
比如我這個操作系統是64位的,它的內存大概是一格一格的,比如下面這張圖,一共就是64位,現在有個對象只占一點空間,你在查這個對象的時候,還要評估這個對象的大小,然后從這個大小的起始位置去偏移,這就比較麻煩了,那么8個字節的存取說白了就是對象尋址最優的一種方式。
圖片
比如Object對象中,Mark Word標記字段和Klass pointer類型指針占了12字節,也就是這個Object對象真正的大小是12字節,但是為了滿足對象對齊是8的整數倍,所以有搞了4個字節的對齊,這樣就成了16字節,也就是8的2倍。讓我們對象總共的大小是16字節。
圖片
數組對象會多一個數組長度。
圖片
其它內容一樣,這里就不過多贅述了,這里的bate類型的b只占用了1字節,但是會有內部對齊,對齊成為了4字節,然后Object對象只占用了4字節,就是因為Object對象存儲的是指針,只占了4個字節是因為64位系統默認是8字節,但是會涉及到指針壓縮,壓縮之后就是4字節。
圖片
圖片
對于上面查看對象大小的代碼先在IDEA中設置一些jvm的參數。
XX:-UseCompressedOops禁止指針壓縮
運行結果
圖片
可以發現對象對齊沒有了,但是多了一個對象頭,也就是說會有兩個4字節大小的位置來存儲類型指針。包括A類中name和Object對象也都是8字節,這些對象都是放在堆中的,如果不開啟指針壓縮,會無形的增大很多空間,會導致整個堆的壓力非常大,很容易就放滿,然后GC...
1.jdk1.6 update14開始,在64bit操作系統中,JVM支持指針壓縮。
2.jvm配置參數:UseCompressedOops,compressed--壓縮、oop(ordinary object pointer)--對象指針。
3.啟用指針壓縮:-XX:+UseCompressedOops(默認開啟),禁止指針壓縮:-XX:-UseCompressedOops。
1.在64位平臺的HotSpot中使用32位指針(實際存儲用64位),內存使用會多出1.5倍左右,使用較大指針在主內存和緩存之間移動數據,占用較大寬帶,同時GC也會承受較大壓力。
2.為了減少64位平臺下內存的消耗,啟用指針壓縮功能。
3.在jvm中,32位地址最大支持4G內存(2的32次方),但是現在的機器基本都是64位的,也就是2的64次方,這絕對是一個非常大的數字,也就是64位能表述的內存非常大,可以通過對對象指針的存入堆內存時壓縮編碼、取出到cpu寄存器后解碼方式進行優化(對象指針在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的內存配置(小于等于32G)。
4.堆內存小于4G時,不需要啟用指針壓縮,jvm會直接去除高32位地址,即使用低虛擬地址空間。
5.堆內存大于32G時,壓縮指針會失效,會強制使用64位(即8字節)來對java對象尋址,這就會出現1的問題,所以堆內存不要大于32G為好。
說的簡單一點就是如果壓縮了,只占用4個字節,如果沒有壓縮占用8個字節,是為了節約內存空間。
本文鏈接:http://www.tebozhan.com/showinfo-26-35318-0.html原來New關鍵字創建對象的背后還隱藏了這么多秘密,看完這篇文章我頓悟了
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com