面試官:Java中實(shí)例對(duì)象存儲(chǔ)在哪?

點(diǎn)擊上方「藍(lán)字」關(guān)注我們

在面試時(shí),遇到這個(gè)問(wèn)題,先不要掉以輕心的一口回答在堆中,一般在java程序中,new的對(duì)象是分配在堆空間中的,但是實(shí)際的情況是,大部分的new對(duì)象會(huì)進(jìn)入堆空間中,而并非是全部的對(duì)象,還有另外兩個(gè)地方可以存儲(chǔ)new的對(duì)象,我們稱之為棧上分配以及TLAB
學(xué)習(xí)本章需要一些前置知識(shí),這里我列一下:
2. JVM內(nèi)存結(jié)構(gòu) / 堆分代結(jié)構(gòu)
文章較長(zhǎng),建議 關(guān)注 收藏 在看!
目錄:
理解Java編譯流程
前端編譯(Front End)
后端編譯(Back End)
什么是JIT (Just in time)
編譯器和解釋器的優(yōu)缺點(diǎn)以及實(shí)用場(chǎng)景
熱點(diǎn)檢測(cè)算法
1)基于采樣的熱點(diǎn)探測(cè)
2) 基于計(jì)數(shù)器的熱點(diǎn)探測(cè)
對(duì)象棧上分配的優(yōu)化
逃逸分析
標(biāo)量替換
同步消除(鎖消除)
棧上分配
對(duì)象的內(nèi)存分配
解決堆內(nèi)存分配的并發(fā)問(wèn)題
CAS
TLAB
總結(jié)
理解Java編譯流程
低級(jí)語(yǔ)言是計(jì)算機(jī)認(rèn)識(shí)的語(yǔ)言、高級(jí)語(yǔ)言是程序員認(rèn)識(shí)的語(yǔ)言。如何從高級(jí)語(yǔ)言轉(zhuǎn)換成低級(jí)語(yǔ)言呢?這個(gè)過(guò)程其實(shí)就是編譯。
不同的語(yǔ)言都有自己的編譯器,Java語(yǔ)言中負(fù)責(zé)編譯的編譯器是一個(gè)命令:javac
通過(guò)javac命令將Java程序的源代碼編譯成Java字節(jié)碼,即我們常說(shuō)的.class文件。這也是我們所理解的編譯.
但是.class并不是計(jì)算機(jī)能夠識(shí)別的語(yǔ)言.要想讓機(jī)器能夠執(zhí)行,需要把字節(jié)碼再翻譯成機(jī)器指令,這個(gè)過(guò)程是JVM來(lái)完成的.這個(gè)過(guò)程也叫編譯.只是層次更深..
因此我們了解到,編譯器可劃分為前端(Front End)與后端(Back End)。

我們可以把將.java文件編譯成.class的編譯過(guò)程稱之為前端編譯。把將.class文件翻譯成機(jī)器指令的編譯過(guò)程稱之為后端編譯。
前端編譯(Front End)
前端編譯主要指與源語(yǔ)言有關(guān)但與目標(biāo)機(jī)無(wú)關(guān)的部分,包括詞法分析、語(yǔ)法分析、語(yǔ)義分析與中間代碼生成。
例如我們使用很多的IDE,如eclipse,idea等,都內(nèi)置了前端編譯器。主要功能就是把.java代碼轉(zhuǎn)換成`.class字節(jié)碼
后端編譯(Back End)
后端編譯主要指與目標(biāo)機(jī)有關(guān)的部分,包括代碼優(yōu)化和目標(biāo)代碼生成等。
在后端編譯中,通常都經(jīng)過(guò)前端編譯的處理,已經(jīng)加工成.class字節(jié)碼文件了 JVM通過(guò)解釋字節(jié)碼將其逐條讀入并翻譯為對(duì)應(yīng)機(jī)器指令,讀一條翻譯一條,勢(shì)必是分產(chǎn)生效率問(wèn)題因此引入了JIT(just in time)
什么是JIT (Just in time)
當(dāng)JVM發(fā)現(xiàn)某個(gè)方法或代碼塊運(yùn)行特別頻繁的時(shí)候,就會(huì)認(rèn)為這是“熱點(diǎn)代碼”(Hot Spot Code)。JIT會(huì)把部分“熱點(diǎn)代碼”翻譯成本地機(jī)器相關(guān)的機(jī)器碼,并進(jìn)行優(yōu)化,然后
緩存起來(lái),以備下次使用
在HotSpot虛擬機(jī)中內(nèi)置了兩個(gè)JIT編譯器分別是:
- Client complier [客戶端]
- Server complier [服務(wù)端]
目前JVM中默認(rèn)都是采用: 解釋器+一個(gè)JIT編譯器 配合的方式進(jìn)行工作 即混合模式
下圖是我機(jī)器上安裝的JDK ,可以看出,使用的JIT是Server Complier, 解釋器和JIT的工作方式是mixed mode

面試題:為何HotSpot虛擬機(jī)要實(shí)現(xiàn)兩個(gè)不同的即時(shí)編譯器?
HotSpot虛擬機(jī)中內(nèi)置了兩個(gè)即時(shí)編譯器:Client Complier和Server Complier,簡(jiǎn)稱為C1、C2編譯器,分別用在客戶端和服務(wù)端。目前主流的HotSpot虛擬機(jī)中默認(rèn)是采用解釋器與其中一個(gè)編譯器直接配合的方式工作。程序使用哪個(gè)編譯器,取決于虛擬機(jī)運(yùn)行的模式。HotSpot虛擬機(jī)會(huì)根據(jù)自身版本與宿主機(jī)器的硬件性能自動(dòng)選擇運(yùn)行模式,用戶也可以使用“-client”或“-server”參數(shù)去強(qiáng)制指定虛擬機(jī)運(yùn)行在Client模式或Server模式。
用Client Complier獲取更高的編譯速度,用Server Complier 來(lái)獲取更好的編譯質(zhì)量。和為什么提供多個(gè)垃圾收集器類似,都是為了適應(yīng)不同的應(yīng)用場(chǎng)景。
編譯器和解釋器的優(yōu)缺點(diǎn)以及實(shí)用場(chǎng)景
在JVM執(zhí)行代碼時(shí),它并不是馬上開始編譯代碼,當(dāng)一段經(jīng)常被執(zhí)行的代碼被編譯后,下次運(yùn)行就不用重復(fù)編譯,此時(shí)使用JIT是劃算的,但是它也不是萬(wàn)能的,比如說(shuō)一些極少執(zhí)行的代碼在編譯時(shí)花費(fèi)的時(shí)間比解釋器還久,這時(shí)就是得不償失了
所以,解釋器和JIT各有千秋:
解釋器與編譯器兩者各有優(yōu)勢(shì):當(dāng)程序需要迅速啟動(dòng)和執(zhí)行的時(shí)候,解釋器可以首先發(fā)揮作用,省去編譯的時(shí)間,立即執(zhí)行。在程序運(yùn)行后,隨著時(shí)間的推移,編譯器逐漸發(fā)揮作用,把越來(lái)越多的代碼編譯成本地代碼之后,可以獲取更高的執(zhí)行效率。
當(dāng)極少執(zhí)行或者執(zhí)行次數(shù)較少的JAVA代碼使用解釋器最優(yōu). 當(dāng)重復(fù)執(zhí)行或者執(zhí)行次數(shù)較多的JAVA代碼使用JIT更劃算.
熱點(diǎn)檢測(cè)算法
要想觸發(fā)JIT,首先需要識(shí)別出熱點(diǎn)代碼。目前主要的熱點(diǎn)代碼識(shí)別方式是熱點(diǎn)探測(cè)(Hot Spot Detection),有以下兩種:
1)基于采樣的熱點(diǎn)探測(cè)
采用這種方法的虛擬機(jī)會(huì)周期性地檢查各個(gè)線程的棧頂,如果發(fā)現(xiàn)某些方法經(jīng)常出現(xiàn)在棧頂,那這個(gè)方法就是“熱點(diǎn)方法”。這種探測(cè)方法的好處是實(shí)現(xiàn)簡(jiǎn)單高效,還可以很容易地獲取方法調(diào)用關(guān)系(將調(diào)用堆棧展開即可),缺點(diǎn)是很難精確地確認(rèn)一個(gè)方法的熱度,容易因?yàn)槭艿骄€程阻塞或別的外界因素的影響而擾亂熱點(diǎn)探測(cè)。
2) 基于計(jì)數(shù)器的熱點(diǎn)探測(cè)
采用這種方法的虛擬機(jī)會(huì)為每個(gè)方法(甚至是代碼塊)建立計(jì)數(shù)器,統(tǒng)計(jì)方法的執(zhí)行次數(shù),如果執(zhí)行次數(shù)超過(guò)一定的閥值,就認(rèn)為它是“熱點(diǎn)方法”。這種統(tǒng)計(jì)方法實(shí)現(xiàn)復(fù)雜一些,需要為每個(gè)方法建立并維護(hù)計(jì)數(shù)器,而且不能直接獲取到方法的調(diào)用關(guān)系,但是它的統(tǒng)計(jì)結(jié)果相對(duì)更加精確嚴(yán)謹(jǐn)。
那么在HotSpot虛擬機(jī)中使用的是哪個(gè)熱點(diǎn)檢測(cè)方式呢?
在HotSpot虛擬機(jī)中使用的是第二種,基于計(jì)數(shù)器的熱點(diǎn)探測(cè)方法,因此它為每個(gè)方法準(zhǔn)備了兩個(gè)計(jì)數(shù)器:
>1 方法調(diào)用計(jì)數(shù)器
顧名思義,就是記錄一個(gè)方法被調(diào)用次數(shù)的計(jì)數(shù)器。
>2 回邊計(jì)數(shù)器
是記錄方法中的for或者while的運(yùn)行次數(shù)的計(jì)數(shù)器。
在確定虛擬機(jī)運(yùn)行參數(shù)的前提下,這兩個(gè)計(jì)數(shù)器都有一個(gè)確定的閾值,當(dāng)計(jì)數(shù)器超過(guò)閾值溢出了,就會(huì)觸發(fā)JIT編譯。
對(duì)象棧上分配的優(yōu)化
逃逸分析
逃逸分析是一種有效減少JAVA程序中
同步負(fù)載和堆內(nèi)存分配壓力的分析算法.Hotspot編譯器能夠分析出一個(gè)新的對(duì)象的引用的使用范圍從而決定是否要將這個(gè)對(duì)象分配到棧上.
public static StringBuffer method(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append("關(guān)注");
sb.append("java寶典");
return sb;
//此時(shí)sb對(duì)象從method方法逃出..
}
public static String method(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append("關(guān)注");
sb.append("java寶典");
return sb.toString();
//此時(shí)sb對(duì)象 沒(méi)有離開 作用域
}
public void globalVariableEscape(){
globalVariableObject = new Object(); //靜態(tài)變量,外部線程可見,發(fā)生逃逸
}
public void instanceObjectEscape(){
instanceObject = new Object(); //賦值給堆中實(shí)例字段,外部線程可見,發(fā)生逃逸
}
在確定對(duì)象不會(huì)逃逸后,JIT將可以進(jìn)行以下優(yōu)化: 標(biāo)量替換 同步消除 棧上分配
第一段代碼中的sb就逃逸了,而第二段代碼中的sb就沒(méi)有逃逸。
在Java代碼運(yùn)行時(shí),通過(guò)JVM參數(shù)可指定是否開啟逃逸分析,
-XX:+DoEscapeAnalysis :表示開啟逃逸分析
-XX:-DoEscapeAnalysis :表示關(guān)閉逃逸分析
-XX:+PrintEscapeAnalysis 開啟打印逃逸分析篩選結(jié)果
從jdk 1.7開始已經(jīng)默認(rèn)開始逃逸分析
標(biāo)量替換
允許將對(duì)象打散分配在棧上,比如若一個(gè)對(duì)象擁有兩個(gè)字段,會(huì)將這兩個(gè)字段視作局部變量進(jìn)行分配。
逸分析只是棧上內(nèi)存分配的前提,還需要進(jìn)行標(biāo)量替換才能真正實(shí)現(xiàn)。例:
public static void main(String[] args) throws Exception {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
allocate();
}
System.out.println((System.currentTimeMillis() - start) + " ms");
Thread.sleep(10000);
}
public static void allocate() {
MyObject myObject = new MyObject(2019, 2019.0);
}
public static class MyObject {
int a;
double b;
MyObject(int a, double b) {
this.a = a;
this.b = b;
}
}
標(biāo)量,就是指JVM中無(wú)法再細(xì)分的數(shù)據(jù),比如int、long、reference等。相對(duì)地,能夠再細(xì)分的數(shù)據(jù)叫做聚合量
Java虛擬機(jī)中的原始數(shù)據(jù)類型(int,long等數(shù)值類型以及reference類型等)都不能再進(jìn)一步分解,它們就可以稱為標(biāo)量。相對(duì)的,如果一個(gè)數(shù)據(jù)可以繼續(xù)分解,那它稱為聚合量,Java中最典型的聚合量是對(duì)象
如果逃逸分析證明一個(gè)對(duì)象不會(huì)被外部訪問(wèn),并且這個(gè)對(duì)象是可分解的,那程序真正執(zhí)行的時(shí)候?qū)⒖赡懿粍?chuàng)建這個(gè)對(duì)象,而改為直接創(chuàng)建它的若干個(gè)被這個(gè)方法使用到的成員變量來(lái)代替。拆散后的變量便可以被單獨(dú)分析與優(yōu)化,可以各自分別在棧幀或寄存器上分配空間,原本的對(duì)象就無(wú)需整體分配空間了
仍然考慮上面的例子,MyObject就是一個(gè)聚合量,因?yàn)樗蓛蓚€(gè)標(biāo)量a、b組成。通過(guò)逃逸分析,JVM會(huì)發(fā)現(xiàn)myObject沒(méi)有逃逸出allocate()方法的作用域,標(biāo)量替換過(guò)程就會(huì)將myObject直接拆解成a和b,也就是變成了:
static void allocate() {
int a = 2019;
double b = 2019.0;
}
可見,對(duì)象的分配完全被消滅了,而int、double都是基本數(shù)據(jù)類型,直接在棧上分配就可以了。所以,在對(duì)象不逃逸出作用域并且能夠分解為純標(biāo)量表示時(shí),對(duì)象就可以在棧上分配
開啟標(biāo)量替換 (-XX:+EliminateAllocations)
標(biāo)量替換的作用是允許將對(duì)象根據(jù)屬性打散后分配在棧上,默認(rèn)該配置為開啟
同步消除(鎖消除)
如果同步塊所使用的鎖對(duì)象通過(guò)逃逸分析被證實(shí)只能夠被一個(gè)線程訪問(wèn),那么JIT編譯器在編譯這個(gè)同步塊的時(shí)候就會(huì)取消對(duì)這部分代碼的同步。這個(gè)取消同步的過(guò)程就叫同步省略,也叫鎖消除
例子:
public void f() {
Object java_bible = new Object();
synchronized(java_bible) {
System.out.println(java_bible);
}
}
在經(jīng)過(guò)逃逸分析后,JIT編譯階段會(huì)被優(yōu)化成:
public void f() {
Object java_bible = new Object();
System.out.println(java_bible); //鎖被去掉了.
}
如果JIT經(jīng)過(guò)逃逸分析之后發(fā)現(xiàn)并無(wú)線程安全問(wèn)題的話,就會(huì)做鎖消除。
棧上分配
通過(guò)逃逸分析,我們發(fā)現(xiàn),許多對(duì)象的生命周期會(huì)隨著方法的調(diào)用開始而開始,方法的調(diào)用結(jié)束而結(jié)束,很多的對(duì)象的作用域都不會(huì)逃逸出方法外,對(duì)于此種對(duì)象,我們可以考慮使用棧上分配,而不是在堆中分配.
因?yàn)橐坏┓峙湓诙芽臻g中,當(dāng)方法調(diào)用結(jié)束,沒(méi)有了引用指向該對(duì)象,該對(duì)象就需要被gc回收,而如果存在大量的這種情況,對(duì)gc來(lái)說(shuō)反而是一種負(fù)擔(dān)。
JVM提供了一種叫做棧上分配的概念,針對(duì)那些作用域不會(huì)逃逸出方法的對(duì)象,在分配內(nèi)存時(shí)不在將對(duì)象分配在堆內(nèi)存中,而是將對(duì)象屬性打散后分配在棧(線程私有的,屬于棧內(nèi)存,標(biāo)量替換)上,這樣,隨著方法的調(diào)用結(jié)束,??臻g的回收就會(huì)隨著將棧上分配的打散后的對(duì)象回收掉,不再給gc增加額外的無(wú)用負(fù)擔(dān),從而提升應(yīng)用程序整體的性能
那么問(wèn)題來(lái)了,如果棧上分配失敗了怎么辦?
對(duì)象的內(nèi)存分配
創(chuàng)建個(gè)對(duì)象有多種方法: 比如 使用new , reflect , clone 不管使用哪種 ,我們都要先分配內(nèi)存
我們拿new 來(lái)舉個(gè)例子:
T t = new T()
class T{
int m = 8;
}
//javap
0 new #2<T> //new作用在內(nèi)存申請(qǐng)開辟一塊空間 new完之后m的值為 0
3 dup
4 invokespecial #3 <T.<init>>
7 astore_1
8 return
那么它是怎么分配的呢?
當(dāng)我們使用new創(chuàng)建對(duì)象后代碼開始運(yùn)行后,虛擬機(jī)執(zhí)行到這條new指令的時(shí)候,會(huì)先檢查要new的對(duì)象對(duì)應(yīng)的類是否已被加載,如果沒(méi)有被加載則先進(jìn)行類加載,檢查通過(guò)之后,就需要給對(duì)象進(jìn)行內(nèi)存分配,分配的內(nèi)存主要用來(lái)存放對(duì)象的實(shí)例變量
為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來(lái)
根據(jù)內(nèi)存連續(xù)和不連續(xù)的情況,JVM使用不同的分配方式.
連續(xù): 指針碰撞 不連續(xù):空閑列表
指針碰撞(Serial、ParNew等帶Compact過(guò)程的收集器)
假設(shè)Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過(guò)的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為“指針碰撞”(Bump the Pointer)。
空閑列表(CMS這種基于Mark-Sweep算法的收集器)
如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒(méi)有辦法簡(jiǎn)單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱為“空閑列表”(Free List)。
無(wú)論那種方式,最終都需要確定出一塊內(nèi)存區(qū)域,用于給新建對(duì)象分配內(nèi)存。對(duì)象的內(nèi)存分配過(guò)程中,主要是對(duì)象的引用指向這個(gè)內(nèi)存區(qū)域,然后進(jìn)行初始化操作,那么在并發(fā)場(chǎng)景之中,如果多線程并發(fā)去堆中獲取內(nèi)存區(qū)域,怎么保證內(nèi)存分配的線程安全性.
解決堆內(nèi)存分配的并發(fā)問(wèn)題
保證分配過(guò)程中的線程安全有兩種方式:
CAS TLAB
CAS
CAS:采用CAS機(jī)制,配合失敗重試的方式保證線程安全性
CAS對(duì)于內(nèi)存的控制是使用重試機(jī)制,因此效率比較低,目前JVM使用的是TLAB方式,我們著重介紹TLAB.
TLAB
TLAB:每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,然后再給對(duì)象分配內(nèi)存的時(shí)候,直接在自己這塊"私有"內(nèi)存中分配,當(dāng)這部分區(qū)域用完之后,再分配新的"私有"內(nèi)存,注意這個(gè)私有對(duì)于創(chuàng)建對(duì)象時(shí)是私有的,但是對(duì)于讀取是共享的.
TLAB (Thread local allcation buffer ) 在“分配”這個(gè)動(dòng)作上是線程獨(dú)占的,至于在讀取、垃圾回收等動(dòng)作上都是線程共享的。在對(duì)象的創(chuàng)建時(shí),首先嘗試進(jìn)行棧上分配,如果分配失敗,會(huì)使用TLAB嘗試分配,如果失敗查看是否是大對(duì)象,如果是大對(duì)象直接進(jìn)入老年代,否則進(jìn)入新生代(Eden).這里我總結(jié)了一張流程圖,如下:

我們可以總結(jié)出: 創(chuàng)建大對(duì)象和創(chuàng)建多個(gè)小對(duì)象相比,多個(gè)小對(duì)象的效率更高
不知道大家有沒(méi)有注意到,TLAB分配空間,每個(gè)線程在Java堆中預(yù)先分配一小塊內(nèi)存,他們?cè)诙阎腥尩乇P的時(shí)候,也會(huì)出現(xiàn)并發(fā)問(wèn)題,但是對(duì)于TLAB的同步控制和我們直接在堆中分配相比效率高了不少(不至于因?yàn)橐峙湟粋€(gè)對(duì)象而鎖住整個(gè)堆了).
總結(jié)
為了保證Java對(duì)象的內(nèi)存分配的安全性,同時(shí)提升效率,每個(gè)線程在Java堆中可以預(yù)先分配一小塊內(nèi)存,這部分內(nèi)存稱之為TLAB(Thread Local Allocation Buffer),這塊內(nèi)存的分配時(shí)線程獨(dú)占的,讀取、使用、回收是線程共享的。
虛擬機(jī)是否使用TLAB 可以通過(guò) -XX:+/-UseTLAB 參數(shù)指定
如果此篇文章對(duì)你有用,那么就關(guān)注我吧!
最近我創(chuàng)建了一個(gè)JAVA學(xué)習(xí)的交流群,有小伙伴想加群可以掃碼進(jìn)

詳解jvm內(nèi)存結(jié)構(gòu),java內(nèi)存模型,java對(duì)象布局,別再搞混啦!

今天我們來(lái)聊聊JVM類加載機(jī)制

Java并發(fā)之AQS詳解

JAVA的SPI機(jī)制
