面試官問:Java中的new關鍵字做了什么事情?
各位小伙伴,當我們new一個對象的時候,對象到底是怎么生產出來的呢,我們這篇說一說對象生成的過程和內存的分配機制,面試的時候可以扯一扯,絕對是加分項。
圖片
1.加載類時檢查
虛擬機在執行的過程中,執行到new關鍵字(new關鍵詞、對象克隆、對象序列化等)的時候,第一步是先去檢查這個指令的參數對應的符號引用是否在常量池中,其對應的類是否已經被加載解析和初始化,如果已經有,就代表此類已經被加載過了,如果嗎,沒有就說明類還沒有被加載,那就要執行類記載的整個過程。
2.內存的分配
在類加載過程完成后,就要對新創建的對象進行分配內存的操作,那么對應所需要的內存具體大小是如何確定的呢,其實對象所需內存的大小在類加載完成后就可以完全確定了,虛擬機只需要在java堆中劃分出相應大小的固定的一塊內存空間即可。
但是在分配內存這個過程中有兩個問題:
- 如何劃分內存。
- 在并發情況下, 可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。虛擬機有兩種內存分配方法,一種是“指針碰撞”,一種是“空閑列表”,java默認采用的是指針碰撞,指針碰撞針對于規整的java堆,被使用的內存全都集中在堆的一邊,而另一邊都是空閑的內存,當需要分配固定大小的內存時候,只需要將內存的指針(分界點的指示器)從當前使用的位置向后挪動相應大小即可。當堆內存分配不是規整的時候,被使用的內存和沒有被使用的內存交錯相間,虛擬機很難找到一塊固定大小且連續的內存空間,這時候指針碰撞就很難發揮出作用,這個時候虛擬機采用的是空閑列表,空閑列表是用來維護哪些內存塊是空閑的,在進行分配內存的時候,只需要去空閑列表中找到一塊大小合適且連續的內存塊就可以了,然后再把這塊內存空間在空閑列表上更新其記錄。
解決并發問題的方法:
CAS(compare and swap): 虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性來對分配內存空間的動作進行同步處理。
本地線程分配緩沖(Thread Local Allocation Buffer,TLAB): 把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存。
通過-XX:+/-UseTLAB參數來設定虛擬機是否使用TLAB(JVM會默認開啟-XX:+UseTLAB),-XX:TLABSize 指定TLAB大小。
3.初始化零值
內存分配完成后,虛擬機需要將分配到的內存空間都初始化為零值(不包括對象頭), 如果使用TLAB,這一工作過程也可以提前至TLAB分配時進行。這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
4.設置對象頭
初始化零值之后,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭Object Header之中。
在HotSpot虛擬機中,對象在內存中存儲的布局可以分為3塊區域:對象頭(Header)、 實例數據(Instance Data)和對齊填充(Padding)。HotSpot虛擬機的對象頭包括兩部分信息,第一部分用于存儲對象自身的運行時數據, 如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、線程持有的鎖、偏向線程ID、偏向時 間戳等。對象頭的另外一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
32位對象頭:
圖片
64位對象頭:
圖片
5.執行方法
執行方法,即對象按照程序員的意愿進行初始化。對應到語言層面上講,就是為屬性賦值(注意,這與上面的賦零值不同,這是由程序員賦的值),和執行構造方法。
對象大小與指針壓縮
對象大小可以用jol-core包查看,引入依賴
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
import org.openjdk.jol.info.ClassLayout;
/**
* 計算對象大小
*/
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
}
}
運行結果:
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1) //mark word
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) //mark word
8 4 (object header) e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243) //Klass Pointer
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
[I object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 6d 01 00 f8 (01101101 00000001 00000000 11111000) (-134217363)
12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
16 0 int [I.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
com.tuling.jvm.JOLSample$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 61 cc 00 f8 (01100001 11001100 00000000 11111000) (-134165407)
12 4 int A.id 0
16 1 byte A.b 0
17 3 (alignment/padding gap)
20 4 java.lang.String A.name null
24 4 java.lang.Object A.o null
28 4 (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
什么是java對象的指針壓縮?
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次方),可以通過對對象指針的存入堆內存時壓縮編碼、取出到cpu寄存器后解碼方式進行優化(對象指針在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的內存配置(小于等于32G)
4.堆內存小于4G時,不需要啟用指針壓縮,jvm會直接去除高32位地址,即使用低虛擬地址空間
5.堆內存大于32G時,壓縮指針會失效,會強制使用64位(即8字節)來對java對象尋址,這就會出現1的問題,所以堆內存不要大于32G為好
對象大小計算
1. 在32位系統下,存放Class指針的空間大小是4字節,MarkWord是4字節,對象頭為8字節。
2. 在64位系統下,存放Class指針的空間大小是8字節,MarkWord是8字節,對象頭為16字節。
3. 64位開啟指針壓縮的情況下,存放Class指針的空間大小是4字節,MarkWord是8字節,對象頭為12字節。數組長度4字節+數組對象頭8字節(對象引用4字節(未開啟指針壓縮的64位為8字節)+數組markword為4字節(64位未開啟指針壓縮的為8字節))+對齊4=16字節。
4. 靜態屬性不算在對象大小內。
關于對齊填充:對于大部分處理器,對象以8字節整數倍來對齊填充都是最高效的存取方式。