1. 請(qǐng)解釋一下對(duì)象的創(chuàng)建過程?(半初始化)

2.對(duì)象在內(nèi)存中的存儲(chǔ)布局?(對(duì)象與數(shù)組的存儲(chǔ)不同)
對(duì)象與數(shù)組的存儲(chǔ)不同

markword:鎖狀態(tài)、分代年齡、hashcode、偏向線程ID、偏向時(shí)間戳等信息。在32位系統(tǒng)占4字節(jié),在64位系統(tǒng)中占8字節(jié)。
類型指針class pointer:對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例, 32位系統(tǒng)占4字節(jié),在64位系統(tǒng)中占8字節(jié)。
實(shí)例數(shù)據(jù)Instance data:是對(duì)象存儲(chǔ)的真正的有效信息,存儲(chǔ)著自身定義的和從父類繼承下來的實(shí)例字段。字段的存儲(chǔ)熟悉怒會(huì)受虛擬機(jī)的分配策略和字段在java源碼中
數(shù)組長(zhǎng)度length:如果是數(shù)組對(duì)象,還有一個(gè)保存數(shù)組長(zhǎng)度的空間,占4個(gè)字節(jié);
對(duì)齊padding,由于虛擬機(jī)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,填充數(shù)據(jù)不是必須存在的,僅僅是為了字節(jié)對(duì)齊。
3.對(duì)象頭具體包括什么?(markword classpointer)synchronized鎖信息
markword與類型指針都是屬于對(duì)象頭

一個(gè)剛剛 new 出來的對(duì)象,如果開始上鎖 (synchronized),它的一個(gè)升級(jí)過程是:
new -> 偏向鎖 -> 自旋鎖(無鎖、lock-free、輕量級(jí)鎖) -> 重量級(jí)鎖。這些信息都記錄在 markword 里面。
對(duì)象實(shí)際數(shù)據(jù)
對(duì)象實(shí)際數(shù)據(jù)包括了對(duì)象的所有成員變量,其大小由各個(gè)成員變量的大小決定,比如:byte和boolean是1個(gè)字節(jié),short和char是2個(gè)字節(jié),int和float是4個(gè)字節(jié),long和double是8個(gè)字節(jié),reference是4個(gè)字節(jié)(64位系統(tǒng)中是8個(gè)字節(jié))。
| Primitive Type | Memory Required(bytes) |
|---|---|
| boolean | 1 |
| byte | 1 |
| short | 2 |
| char | 2 |
| int | 4 |
| float | 4 |
| long | 8 |
| double | 8 |
對(duì)于reference類型來說,在32位系統(tǒng)上占用4bytes, 在64位系統(tǒng)上占用8bytes。
對(duì)象頭占用空間大小
這里說明一下32位系統(tǒng)和64位系統(tǒng)中對(duì)象所占用內(nèi)存空間的大?。?/p>
| 數(shù)據(jù) | 32位操作系統(tǒng) | 64位操作系統(tǒng) | 64位開啟指針壓縮 |
|---|---|---|---|
| MarkWord | 4 | 8 | - |
| Class Pointer | 4 | 8 | 4 |
指針壓縮
64位JVM消耗的內(nèi)存會(huì)比32位的要多大約1.5倍,這是因?yàn)閷?duì)象指針在64位JVM下有更寬的尋址。對(duì)于那些將要從32位平臺(tái)移植到64位的應(yīng)用來說,平白無辜多了1/2的內(nèi)存占用,這是開發(fā)者不愿意看到的。
從JDK 1.6 update14開始,64位的JVM正式支持了 -XX:+UseCompressedOops 這個(gè)可以壓縮指針,起到節(jié)約內(nèi)存占用的新參數(shù)。
會(huì)壓縮的對(duì)象
- 每個(gè)Class的屬性指針(靜態(tài)成員變量);
- 每個(gè)對(duì)象的屬性指針;
- 普通對(duì)象數(shù)組的每個(gè)元素指針。
` 當(dāng)然,壓縮也不是所有的指針都會(huì)壓縮,對(duì)一些特殊類型的指針,JVM是不會(huì)優(yōu)化的,例如指向PermGen(永久代)的Class對(duì)象指針、本地變量、堆棧元素、入?yún)?、返回值和NULL指針不會(huì)被壓縮。
啟用指針壓縮
在Java程序啟動(dòng)時(shí)增加JVM參數(shù):-XX:+UseCompressedOops來啟用。
注意:32位HotSpot VM是不支持UseCompressedOops參數(shù)的,只有64位HotSpot VM才支持
本文中使用的是JDK 1.8,默認(rèn)該參數(shù)就是開啟的。
4.對(duì)象怎么定位?(直接 間接)
兩種方式:句柄方式 、 直接指針
當(dāng)在堆內(nèi)存中創(chuàng)建對(duì)象之后,java程序需要refrence數(shù)據(jù)來操作對(duì)象。由于reference類型在java虛擬機(jī)規(guī)范中只規(guī)定了指向?qū)ο蟮囊?并沒有規(guī)定這個(gè)引用以如何的方式去定位、訪問堆中的對(duì)象具體位置.
其中Reference是java中的引用類,它主要是對(duì)普通對(duì)象進(jìn)行包裝,從而在JVM在垃圾回收時(shí),按照引用類型的不同,在回收時(shí)采用不同的邏輯。
4.1 句柄
使用句柄方式訪問兌對(duì)象時(shí),需要在堆中劃出一部分內(nèi)存作為句柄池,句柄池中存放各個(gè)對(duì)象的句柄,句柄包含了:
對(duì)象實(shí)例數(shù)據(jù)的指針
對(duì)象類型數(shù)據(jù)的指針
而reference中存儲(chǔ)的則是對(duì)象的句柄地址。
對(duì)象實(shí)例數(shù)據(jù):對(duì)象實(shí)例字段的數(shù)據(jù)
對(duì)象類型數(shù)據(jù):對(duì)象的類型、父類實(shí)現(xiàn)的接口、方法等
4.2 直接指針
使用直接方式訪問對(duì)象時(shí),堆中對(duì)象存放的是對(duì)象的實(shí)例數(shù)據(jù)和指向?qū)ο箢愋蛿?shù)據(jù)的指針,reference則存儲(chǔ)的是對(duì)象地址。
4.3 兩種方式的對(duì)比
2. 加問 DCL 與 volatile 問題?(指令重排)
為了理解什么是 DCL (雙檢鎖/雙重校驗(yàn)鎖(DCL,即 double-checked-locking)),我們先回顧一下 單例模式(Singleton Pattern)。
- 單例類只能有一個(gè)實(shí)例。
- 單例類必須直接創(chuàng)建直接的唯一實(shí)例。
- 單例類必須給所有其他對(duì)象提供這一實(shí)例。
參考代碼1
/**
* 餓漢模式
* 類加載到內(nèi)存后,就實(shí)例化一個(gè)單例。JVM保證線程安全
* 簡(jiǎn)單使用,推薦使用
* 唯一缺點(diǎn):不管用到與否,類裝載時(shí)就完成實(shí)例化
* Class.forName("")
* (話說你不用的,你裝載它干啥)
*/
public class Mgr01 {
// 創(chuàng)建 Mgr01 的一個(gè)對(duì)象
private static final Mgr01 INSTANCE = new Mgr01();
//讓構(gòu)造函數(shù)為 private,這樣該類就不會(huì)被實(shí)例化
private Mgr01(){
}
// 獲取唯一可用的對(duì)象
public static Mgr01 getInstance(){
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
Mgr01 m1 = Mgr01.getInstance();
Mgr01 m2 = Mgr01.getInstance();
System.out.println(m1 == m2);
}
參考代碼1,這種寫法有人會(huì)說 INSTANCE還沒用就直接 new 出來了,假如說創(chuàng)建的過程特別浪費(fèi)資源,能不能夠等我想用的時(shí)候再初始化出來。
參考代碼2:
/**
* 雖然達(dá)到了按需初始化的目的,但卻帶來了線程不安全
*/
public class Mgr02 {
private static Mgr02 INSTANCE;
private Mgr02() {
}
public static Mgr02 getInstance() {
if (INSTANCE == null) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr02();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i< 100; i++) {
new Thread(() ->
System.out.println(Mgr02.getInstance().hashCode())
).start();
}
}
}
還有人接著說,參考代碼2,線程不安全,多線程訪問情況下有可能會(huì) new 出多個(gè)對(duì)象出來。自然而然我們想到加鎖來解決.
參考代碼3
/**
* 增加synchronized,線程安全
*/
public class Mgr03 {
private static Mgr03 INSTANCE;
private Mgr03() {
}
public static synchronized Mgr03 getInstance() {
if (INSTANCE == null) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr03();
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i< 100; i++) {
new Thread(() ->
System.out.println(Mgr03.getInstance().hashCode())
).start();
}
}
}
可是有的人還會(huì)說,你上來二話不說整個(gè)方法全上鎖,鎖的粒度是不是太粗了。于是我們換個(gè)寫法。
參考代碼4
public class Mgr04 {
private static Mgr04 INSTANCE;
private Mgr04() {
}
public static Mgr04 getInstance() {
// 業(yè)務(wù)代碼
if (INSTANCE == null) {
// 妄圖通過減少同步代碼塊的方式提高效率,然后不可行
synchronized (Mgr04.class) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr04();
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i< 100; i++) {
new Thread(() ->
System.out.println(Mgr04.getInstance().hashCode())
).start();
}
}
}
這個(gè)版本在多線程訪問情況下,是線程不安全的。于是誕生了 “DCL” 寫法。
參考代碼5
public class Mgr05 {
private static volatile Mgr05 INSTANCE;
private Mgr05() {
}
public static Mgr05 getInstance() {
// 業(yè)務(wù)代碼
if (INSTANCE == null) { // Double Check Lock
// 雙重檢查
synchronized (Mgr05.class) {
if (INSTANCE == null) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
INSTANCE = new Mgr05();
}
}
}
return INSTANCE;
}
public void m() {
System.out.println("m");
}
public static void main(String[] args) {
for (int i = 0; i< 100; i++) {
new Thread(() ->
System.out.println(Mgr05.getInstance().hashCode())
).start();
}
}
}
volatile 主要有兩個(gè)作用:
線程可見性
禁止指令重排序(上下都要加內(nèi)存屏障)
那么到底需不需加 volatile 關(guān)鍵字,我們來分析下:
當(dāng)?shù)谝粋€(gè)線程來的時(shí)候,判斷它為空,開始對(duì)它進(jìn)行初始化(new)。當(dāng) new 一半的時(shí)候,只拿到了默認(rèn)值,還沒獲取初始化值。

這個(gè)時(shí)候下面兩條指令有可能會(huì)發(fā)生 指令重排序 ,這時(shí)候就會(huì)先建立關(guān)聯(lián),再調(diào)用構(gòu)造方法賦予初始值。目前 t 就執(zhí)行了 半初始化 的這個(gè)狀態(tài)對(duì)象

當(dāng) t 指向半初始化狀態(tài)對(duì)象的時(shí)候,正好這個(gè)時(shí)候第二個(gè)線程來了,當(dāng)前 t指向了半初始化狀態(tài)的對(duì)象, 肯定不為空。那就直接用了,那就用半初始化狀態(tài)的這個(gè)對(duì)象,就會(huì)發(fā)生不可預(yù)知的錯(cuò)誤。
6.對(duì)象怎么分配?(棧上-線程本地-Eden-Old)
7.Object o = new Object() 在內(nèi)存中占用多少字節(jié)?
對(duì)象的內(nèi)存以字節(jié)為單位,且必須是8的倍數(shù),它由3部分組成:
對(duì)象頭 + 實(shí)例數(shù)據(jù) + 對(duì)齊內(nèi)存
對(duì)象頭= Mark word + 類型指針