面試官問(wèn):Java中的new關(guān)鍵字做了什么事情?
各位小伙伴,當(dāng)我們new一個(gè)對(duì)象的時(shí)候,對(duì)象到底是怎么生產(chǎn)出來(lái)的呢,我們這篇說(shuō)一說(shuō)對(duì)象生成的過(guò)程和內(nèi)存的分配機(jī)制,面試的時(shí)候可以扯一扯,絕對(duì)是加分項(xiàng)。
圖片
1.加載類時(shí)檢查
虛擬機(jī)在執(zhí)行的過(guò)程中,執(zhí)行到new關(guān)鍵字(new關(guān)鍵詞、對(duì)象克隆、對(duì)象序列化等)的時(shí)候,第一步是先去檢查這個(gè)指令的參數(shù)對(duì)應(yīng)的符號(hào)引用是否在常量池中,其對(duì)應(yīng)的類是否已經(jīng)被加載解析和初始化,如果已經(jīng)有,就代表此類已經(jīng)被加載過(guò)了,如果嗎,沒(méi)有就說(shuō)明類還沒(méi)有被加載,那就要執(zhí)行類記載的整個(gè)過(guò)程。
2.內(nèi)存的分配
在類加載過(guò)程完成后,就要對(duì)新創(chuàng)建的對(duì)象進(jìn)行分配內(nèi)存的操作,那么對(duì)應(yīng)所需要的內(nèi)存具體大小是如何確定的呢,其實(shí)對(duì)象所需內(nèi)存的大小在類加載完成后就可以完全確定了,虛擬機(jī)只需要在java堆中劃分出相應(yīng)大小的固定的一塊內(nèi)存空間即可。
但是在分配內(nèi)存這個(gè)過(guò)程中有兩個(gè)問(wèn)題:
- 如何劃分內(nèi)存。
- 在并發(fā)情況下, 可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒(méi)來(lái)得及修改,對(duì)象B又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存的情況。虛擬機(jī)有兩種內(nèi)存分配方法,一種是“指針碰撞”,一種是“空閑列表”,java默認(rèn)采用的是指針碰撞,指針碰撞針對(duì)于規(guī)整的java堆,被使用的內(nèi)存全都集中在堆的一邊,而另一邊都是空閑的內(nèi)存,當(dāng)需要分配固定大小的內(nèi)存時(shí)候,只需要將內(nèi)存的指針(分界點(diǎn)的指示器)從當(dāng)前使用的位置向后挪動(dòng)相應(yīng)大小即可。當(dāng)堆內(nèi)存分配不是規(guī)整的時(shí)候,被使用的內(nèi)存和沒(méi)有被使用的內(nèi)存交錯(cuò)相間,虛擬機(jī)很難找到一塊固定大小且連續(xù)的內(nèi)存空間,這時(shí)候指針碰撞就很難發(fā)揮出作用,這個(gè)時(shí)候虛擬機(jī)采用的是空閑列表,空閑列表是用來(lái)維護(hù)哪些內(nèi)存塊是空閑的,在進(jìn)行分配內(nèi)存的時(shí)候,只需要去空閑列表中找到一塊大小合適且連續(xù)的內(nèi)存塊就可以了,然后再把這塊內(nèi)存空間在空閑列表上更新其記錄。
解決并發(fā)問(wèn)題的方法:
CAS(compare and swap): 虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性來(lái)對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理。
本地線程分配緩沖(Thread Local Allocation Buffer,TLAB): 把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行,即每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存。
通過(guò)-XX:+/-UseTLAB參數(shù)來(lái)設(shè)定虛擬機(jī)是否使用TLAB(JVM會(huì)默認(rèn)開(kāi)啟-XX:+UseTLAB),-XX:TLABSize 指定TLAB大小。
3.初始化零值
內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭), 如果使用TLAB,這一工作過(guò)程也可以提前至TLAB分配時(shí)進(jìn)行。這一步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問(wèn)到這些字段的數(shù)據(jù)類型所對(duì)應(yīng)的零值。
4.設(shè)置對(duì)象頭
初始化零值之后,虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭Object Header之中。
在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header)、 實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。HotSpot虛擬機(jī)的對(duì)象頭包括兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù), 如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí) 間戳等。對(duì)象頭的另外一部分是類型指針,即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。
32位對(duì)象頭:
圖片
64位對(duì)象頭:
圖片
5.執(zhí)行方法
執(zhí)行方法,即對(duì)象按照程序員的意愿進(jìn)行初始化。對(duì)應(yīng)到語(yǔ)言層面上講,就是為屬性賦值(注意,這與上面的賦零值不同,這是由程序員賦的值),和執(zhí)行構(gòu)造方法。
對(duì)象大小與指針壓縮
對(duì)象大小可以用jol-core包查看,引入依賴
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
import org.openjdk.jol.info.ClassLayout;
/**
* 計(jì)算對(duì)象大小
*/
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 默認(rèn)開(kāi)啟的壓縮所有指針
// -XX:+UseCompressedClassPointers 默認(rèn)開(kāi)啟的壓縮對(duì)象頭里的類型指針Klass Pointer
// Oops : Ordinary Object Pointers
public static class A {
//8B mark word
//4B Klass Pointer 如果關(guān)閉壓縮-XX:-UseCompressedClassPointers或-XX:-UseCompressedOops,則占用8B
int id; //4B
String name; //4B 如果關(guān)閉壓縮-XX:-UseCompressedOops,則占用8B
byte b; //1B
Object o; //4B 如果關(guān)閉壓縮-XX:-UseCompressedOops,則占用8B
}
}
運(yùn)行結(jié)果:
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對(duì)象的指針壓縮?
1.jdk1.6 update14開(kāi)始,在64bit操作系統(tǒng)中,JVM支持指針壓縮
2.jvm配置參數(shù):UseCompressedOops,compressed--壓縮、oop(ordinary object pointer)--對(duì)象指針
3.啟用指針壓縮:-XX:+UseCompressedOops(默認(rèn)開(kāi)啟),禁止指針壓縮:-XX:-UseCompressedOops
為什么要進(jìn)行指針壓縮?
1.在64位平臺(tái)的HotSpot中使用32位指針(實(shí)際存儲(chǔ)用64位),內(nèi)存使用會(huì)多出1.5倍左右,使用較大指針在主內(nèi)存和緩存之間移動(dòng)數(shù)據(jù),占用較大寬帶,同時(shí)GC也會(huì)承受較大壓力2.為了減少64位平臺(tái)下內(nèi)存的消耗,啟用指針壓縮功能
3.在jvm中,32位地址最大支持4G內(nèi)存(2的32次方),可以通過(guò)對(duì)對(duì)象指針的存入堆內(nèi)存時(shí)壓縮編碼、取出到cpu寄存器后解碼方式進(jìn)行優(yōu)化(對(duì)象指針在堆中是32位,在寄存器中是35位,2的35次方=32G),使得jvm只用32位地址就可以支持更大的內(nèi)存配置(小于等于32G)
4.堆內(nèi)存小于4G時(shí),不需要啟用指針壓縮,jvm會(huì)直接去除高32位地址,即使用低虛擬地址空間
5.堆內(nèi)存大于32G時(shí),壓縮指針會(huì)失效,會(huì)強(qiáng)制使用64位(即8字節(jié))來(lái)對(duì)java對(duì)象尋址,這就會(huì)出現(xiàn)1的問(wèn)題,所以堆內(nèi)存不要大于32G為好
對(duì)象大小計(jì)算
1. 在32位系統(tǒng)下,存放Class指針的空間大小是4字節(jié),MarkWord是4字節(jié),對(duì)象頭為8字節(jié)。
2. 在64位系統(tǒng)下,存放Class指針的空間大小是8字節(jié),MarkWord是8字節(jié),對(duì)象頭為16字節(jié)。
3. 64位開(kāi)啟指針壓縮的情況下,存放Class指針的空間大小是4字節(jié),MarkWord是8字節(jié),對(duì)象頭為12字節(jié)。數(shù)組長(zhǎng)度4字節(jié)+數(shù)組對(duì)象頭8字節(jié)(對(duì)象引用4字節(jié)(未開(kāi)啟指針壓縮的64位為8字節(jié))+數(shù)組markword為4字節(jié)(64位未開(kāi)啟指針壓縮的為8字節(jié)))+對(duì)齊4=16字節(jié)。
4. 靜態(tài)屬性不算在對(duì)象大小內(nèi)。
關(guān)于對(duì)齊填充:對(duì)于大部分處理器,對(duì)象以8字節(jié)整數(shù)倍來(lái)對(duì)齊填充都是最高效的存取方式。