Jvm java虛擬機(jī)

學(xué)習(xí)記錄

為什么要了解虛擬機(jī)

JVM不單單只支持Java語(yǔ)言,也支持其他語(yǔ)言(Scala、Kotlin、Groovy等等)

區(qū)塊鏈2.0--以太坊(比特幣是區(qū)塊鏈1.0) 中提供了EVM的虛擬機(jī),它的實(shí)現(xiàn)和JVM類(lèi)似,基于棧、生成腳本編譯成字節(jié)碼來(lái)執(zhí)行。知識(shí)通用。(理論大于實(shí)際)

虛擬機(jī)歷史

了解即可,無(wú)需關(guān)注

Hotspot什么意思:熱點(diǎn)代碼探測(cè)技術(shù),及時(shí)編譯器(發(fā)現(xiàn)最有價(jià)值的代碼,如果代碼用得非常多,就會(huì)把這些代碼編譯成本地代碼)。

華為有的項(xiàng)目用的J9

谷歌(谷歌主要開(kāi)發(fā)語(yǔ)言也是Java):Google Android Dalivk VM

未來(lái)的Java技術(shù)

模塊化:使用得最多OSGI,應(yīng)用層面就是微服務(wù),互聯(lián)網(wǎng)的發(fā)展方向

混合語(yǔ)言:多個(gè)語(yǔ)言都可以運(yùn)行在JVM中

多核并行:CPU從高頻次轉(zhuǎn)變?yōu)槎嗪诵?,多核時(shí)代。JDK1.7引入了Fork/Join,JDK1.8提出lambda表達(dá)式(函數(shù)式編程天生適合并行運(yùn)行)

豐富語(yǔ)法:JDK5提出自動(dòng)裝箱、泛型(并發(fā)編程講到)、動(dòng)態(tài)注解等語(yǔ)法。JDK7二進(jìn)制原生支持。try-catch-finally 至try-with-resource

64位:雖然同樣的程序64位內(nèi)存消耗比32位要多一點(diǎn),但是支持內(nèi)存大,所以虛擬機(jī)都會(huì)完全過(guò)渡到64位。

更強(qiáng)的垃圾回收:JDK11 –ZGC(TB級(jí)別內(nèi)存回收)):有色指針、加載屏障

運(yùn)行時(shí)數(shù)據(jù)區(qū)域

抽象概念,內(nèi)部實(shí)現(xiàn)依賴寄存器、高速緩存、主內(nèi)存(具體要分析JVM源碼 C++語(yǔ)言實(shí)現(xiàn),沒(méi)必要看)

計(jì)算器=指令+數(shù)據(jù)

虛擬機(jī)棧、本地方法棧(native方法)、程序計(jì)數(shù)器:指令相關(guān)

堆、方法:數(shù)據(jù)相關(guān)

程序計(jì)數(shù)器

較小的內(nèi)存空間,當(dāng)前線程執(zhí)行的字節(jié)碼的行號(hào)指示器;各線程之間獨(dú)立存儲(chǔ),互不影響(面試可能問(wèn)到為什么需要)

虛擬機(jī)棧

棧:數(shù)據(jù)結(jié)構(gòu)的特點(diǎn)和java中方法中調(diào)用方法的特性一致。(為什么JVM使用棧 –演示代碼StackFilo)

虛擬機(jī)棧:每個(gè)線程私有的,線程在運(yùn)行時(shí),在執(zhí)行每個(gè)方法的時(shí)候都會(huì)打包成一個(gè)棧幀,存儲(chǔ)了局部變量表,操作數(shù)棧,動(dòng)態(tài)鏈接,方法出口等信息,然后放入棧。每個(gè)時(shí)刻正在執(zhí)行的當(dāng)前方法就是虛擬機(jī)棧頂?shù)臈E。方法的執(zhí)行就對(duì)應(yīng)著棧幀在虛擬機(jī)棧中入棧和出棧的過(guò)程。

棧的大小缺省為1M,可用參數(shù) –Xss調(diào)整大小,例如-Xss256k

局部變量表:顧名思義就是局部變量的表,用于存放我們的局部變量的。首先它是一個(gè)32位的長(zhǎng)度,主要存放我們的Java的八大基礎(chǔ)數(shù)據(jù)類(lèi)型,一般32位就可以存放下,如果是64位的就使用高低位占用兩個(gè)也可以存放下,如果是局部的一些對(duì)象,比如我們的Object對(duì)象,我們只需要存放它的一個(gè)引用地址即可。

操作數(shù)據(jù)棧:存放我們方法執(zhí)行的操作數(shù)的,它就是一個(gè)棧,先進(jìn)后出的棧結(jié)構(gòu),操作數(shù)棧,就是用來(lái)操作的,操作的的元素可以是任意的java數(shù)據(jù)類(lèi)型,所以我們知道一個(gè)方法剛剛開(kāi)始的時(shí)候,這個(gè)方法的操作數(shù)棧就是空的,操作數(shù)棧運(yùn)行方法是會(huì)一直運(yùn)行入棧/出棧的操作

動(dòng)態(tài)連接****:Java語(yǔ)言特性多態(tài)(需要類(lèi)加載、運(yùn)行時(shí)才能確定具體的方法),動(dòng)態(tài)特性(Groovy、JS、動(dòng)態(tài)代理)

返回地址****:正常返回(調(diào)用程序計(jì)數(shù)器中的地址作為返回)、異常的話(通過(guò)異常處理器表<非棧幀中的>來(lái)確定)

本地方法棧

各虛擬機(jī)自由實(shí)現(xiàn),

線程共享的區(qū)域

方法區(qū)/永久代

用于存儲(chǔ)已經(jīng)被虛擬機(jī)加載的類(lèi)信息,常量("zdy","123"等),靜態(tài)變量(static變量)等數(shù)據(jù),可用以下參數(shù)調(diào)整:

jdk1.7及以前:-XX:PermSize;-XX:MaxPermSize;

jdk1.8以后:-XX:MetaspaceSize; -XX:MaxMetaspaceSize

jdk1.8以后大小就只受本機(jī)總內(nèi)存的限制

如:-XX:MaxMetaspaceSize=3M

幾乎所有對(duì)象都分配在這里,也是垃圾回收發(fā)生的主要區(qū)域,可用以下參數(shù)調(diào)整:

-Xms:堆的最小值;

-Xmx:堆的最大值;

-Xmn:新生代的大?。?/p>

-XX:NewSize;新生代最小值;

-XX:MaxNewSize:新生代最大值;

例如- Xmx256m

直接內(nèi)存

不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域;如果使用了NIO,這塊區(qū)域會(huì)被頻繁使用,在java堆內(nèi)可以用directByteBuffer對(duì)象直接引用并操作;

這塊內(nèi)存不受java堆大小限制,但受本機(jī)總內(nèi)存的限制,可以通過(guò)-XX:MaxDirectMemorySize來(lái)設(shè)置(默認(rèn)與堆內(nèi)存最大值一樣),所以也會(huì)出現(xiàn)OOM異常。

站在線程角度來(lái)看

線程角度看java虛擬機(jī).png
虛擬機(jī)棧的執(zhí)行過(guò)程演示.png

深入辨析堆和棧

n 功能

? 以棧幀的方式存儲(chǔ)方法調(diào)用的過(guò)程,并存儲(chǔ)方法調(diào)用過(guò)程中基本數(shù)據(jù)類(lèi)型的變量(int、short、long、byte、float、double、boolean、char等)以及對(duì)象的引用變量,其內(nèi)存分配在棧上,變量出了作用域就會(huì)自動(dòng)釋放;

? 而堆內(nèi)存用來(lái)存儲(chǔ)Java中的對(duì)象。無(wú)論是成員變量,局部變量,還是類(lèi)變量,它們指向的對(duì)象都存儲(chǔ)在堆內(nèi)存中;

n 線程獨(dú)享還是共享

? 棧內(nèi)存歸屬于單個(gè)線程,每個(gè)線程都會(huì)有一個(gè)棧內(nèi)存,其存儲(chǔ)的變量只能在其所屬線程中可見(jiàn),即棧內(nèi)存可以理解成線程的私有內(nèi)存。

? 堆內(nèi)存中的對(duì)象對(duì)所有線程可見(jiàn)。堆內(nèi)存中的對(duì)象可以被所有線程訪問(wèn)。

n 空間大小

棧的內(nèi)存要遠(yuǎn)遠(yuǎn)小于堆內(nèi)存

棧溢出

參數(shù):-Xss256k

java.lang.StackOverflowError 一般的方法調(diào)用是很難出現(xiàn)的,如果出現(xiàn)了要考慮是否有無(wú)限遞歸。

虛擬機(jī)棧帶給我們的啟示:方法的執(zhí)行因?yàn)橐虬蓷E,所以天生要比實(shí)現(xiàn)同樣功能的循環(huán)慢,所以樹(shù)的遍歷算法中:遞歸和非遞歸(循環(huán)來(lái)實(shí)現(xiàn))都有存在的意義。遞歸代碼簡(jiǎn)潔,非遞歸代碼復(fù)雜但是速度較快。

虛擬機(jī)中的對(duì)象

對(duì)象的分配

虛擬機(jī)遇到一條new指令時(shí):

1)檢查加載

先執(zhí)行相應(yīng)的類(lèi)加載過(guò)程。

2)分配內(nèi)存

指針碰撞

接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存。為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來(lái)。

如果Java堆中內(nèi)存是絕對(duì)規(guī)整的,所有用過(guò)的內(nèi)存都放在一邊,空閑的內(nèi)存放在另一邊,中間放著一個(gè)指針作為分界點(diǎn)的指示器,那所分配內(nèi)存就僅僅是把那個(gè)指針向空閑空間那邊挪動(dòng)一段與對(duì)象大小相等的距離,這種分配方式稱為“指針碰撞”。

空閑列表

如果Java堆中的內(nèi)存并不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯(cuò),那就沒(méi)有辦法簡(jiǎn)單地進(jìn)行指針碰撞了,虛擬機(jī)就必須維護(hù)一個(gè)列表,記錄上哪些內(nèi)存塊是可用的,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄,這種分配方式稱為“空閑列表”。

選擇哪種分配方式由Java堆是否規(guī)整決定,而Java堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。

并發(fā)安全

除如何劃分可用空間之外,還有另外一個(gè)需要考慮的問(wèn)題是對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使是僅僅修改一個(gè)指針?biāo)赶虻奈恢茫诓l(fā)情況下也并不是線程安全的,可能出現(xiàn)正在給對(duì)象A分配內(nèi)存,指針還沒(méi)來(lái)得及修改,對(duì)象B又同時(shí)使用了原來(lái)的指針來(lái)分配內(nèi)存的情況。

CAS機(jī)制

解決這個(gè)問(wèn)題有兩種方案,一種是對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理——實(shí)際上虛擬機(jī)采用CAS配上失敗重試的方式保證更新操作的原子性;

分配緩沖

另一種是把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行,即每個(gè)線程在Java堆中預(yù)先分配一小塊私有內(nèi)存,也就是本地線程分配緩沖(Thread Local Allocation Buffer,TLAB),如果設(shè)置了虛擬機(jī)參數(shù) -XX:UseTLAB,在線程初始化時(shí),同時(shí)也會(huì)申請(qǐng)一塊指定大小的內(nèi)存,只給當(dāng)前線程使用,這樣每個(gè)線程都單獨(dú)擁有一個(gè)Buffer,如果需要分配內(nèi)存,就在自己的Buffer上分配,這樣就不存在競(jìng)爭(zhēng)的情況,可以大大提升分配效率,當(dāng)Buffer容量不夠的時(shí)候,再重新從Eden區(qū)域申請(qǐng)一塊繼續(xù)使用。

TLAB的目的是在為新對(duì)象分配內(nèi)存空間時(shí),讓每個(gè)Java應(yīng)用線程能在使用自己專屬的分配指針來(lái)分配空間,減少同步開(kāi)銷(xiāo)。

TLAB只是讓每個(gè)線程有私有的分配指針,但底下存對(duì)象的內(nèi)存空間還是給所有線程訪問(wèn)的,只是其它線程無(wú)法在這個(gè)區(qū)域分配而已。當(dāng)一個(gè)TLAB用滿(分配指針top撞上分配極限end了),就新申請(qǐng)一個(gè)TLAB。

3)內(nèi)存空間初始化

(注意不是構(gòu)造方法)內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(如int值為0,boolean值為false等等)。這一步操作保證了對(duì)象的實(shí)例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問(wèn)到這些字段的數(shù)據(jù)類(lèi)型所對(duì)應(yīng)的零值。

4)設(shè)置

接下來(lái),虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例、如何才能找到類(lèi)的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭之中。

5)對(duì)象初始化

在上面工作都完成之后,從虛擬機(jī)的視角來(lái)看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了,但從Java程序的視角來(lái)看,對(duì)象創(chuàng)建才剛剛開(kāi)始,所有的字段都還為零值。所以,一般來(lái)說(shuō),執(zhí)行new指令之后會(huì)接著把對(duì)象按照程序員的意愿進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全產(chǎn)生出來(lái)。

對(duì)象的內(nèi)存布局

在HotSpot虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)。

對(duì)象頭包括兩部分信息,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等。

對(duì)象頭的另外一部分是類(lèi)型指針,即對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例。

第三部分對(duì)齊填充并不是必然存在的,也沒(méi)有特別的含義,它僅僅起著占位符的作用。由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)對(duì)象的大小必須是8字節(jié)的整數(shù)倍。當(dāng)對(duì)象其他數(shù)據(jù)部分沒(méi)有對(duì)齊時(shí),就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。

對(duì)象的訪問(wèn)定位

建立對(duì)象是為了使用對(duì)象,我們的Java程序需要通過(guò)棧上的reference數(shù)據(jù)來(lái)操作堆上的具體對(duì)象。目前主流的訪問(wèn)方式有使用句柄和直接指針兩種。

句柄

如果使用句柄訪問(wèn)的話,那么Java堆中將會(huì)劃分出一塊內(nèi)存來(lái)作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類(lèi)型數(shù)據(jù)各自的具體地址信息。

直接指針

如果使用直接指針訪問(wèn), reference中存儲(chǔ)的直接就是對(duì)象地址。

這兩種對(duì)象訪問(wèn)方式各有優(yōu)勢(shì),使用句柄來(lái)訪問(wèn)的最大好處就是reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference本身不需要修改。

使用直接指針訪問(wèn)方式的最大好處就是速度更快,它節(jié)省了一次指針定位的時(shí)間開(kāi)銷(xiāo),由于對(duì)象的訪問(wèn)在Java中非常頻繁,因此這類(lèi)開(kāi)銷(xiāo)積少成多后也是一項(xiàng)非??捎^的執(zhí)行成本。

對(duì)Sun HotSpot而言,它是使用直接指針訪問(wèn)方式進(jìn)行對(duì)象訪問(wèn)的。

逃逸分析

棧上分配

虛擬機(jī)提供的一種優(yōu)化技術(shù),基本思想是,對(duì)于線程私有的對(duì)象,將它打散分配在棧上,而不分配在堆上。好處是對(duì)象跟著方法調(diào)用自行銷(xiāo)毀,不需要進(jìn)行垃圾回收,可以提高性能。

棧上分配需要的技術(shù)基礎(chǔ),逃逸分析。逃逸分析的目的是判斷對(duì)象的作用域是否會(huì)逃逸出方法體。注意,任何可以在多個(gè)線程之間共享的對(duì)象,一定都屬于逃逸對(duì)象。

public void test(int x,inty ){

String x = “”;

User u = ….

…..

}

User類(lèi)型的對(duì)象u就<u>沒(méi)有</u>逃逸出方法test。

public User test(int x,inty ){

String x = “”;

User u = ….

…..

return u;

}

User類(lèi)型的對(duì)象u就逃逸出方法test。

如何啟用棧上分配

-server JVM運(yùn)行的模式之一, server模式才能進(jìn)行逃逸分析, JVM運(yùn)行的模式還有mix/client

-Xmx10m和-Xms10m:堆的大小

-XX:+DoEscapeAnalysis:?jiǎn)⒂锰右莘治?默認(rèn)打開(kāi))

-XX:+PrintGC:打印GC日志

-XX:+EliminateAllocations:標(biāo)量替換(默認(rèn)打開(kāi))

-XX:-UseTLAB 關(guān)閉本地線程分配緩沖

TLAB: ThreadLocalAllocBuffer,具體解釋參見(jiàn)下文《虛擬機(jī)中的對(duì)象---對(duì)象的分配----2)》

對(duì)棧上分配發(fā)生影響的參數(shù)就是三個(gè),-server、-XX:+DoEscapeAnalysis和-XX:+EliminateAllocations,任何一個(gè)發(fā)生變化都不會(huì)發(fā)生棧上分配,因?yàn)閱⒂锰右莘治龊蜆?biāo)量替換默認(rèn)是打開(kāi)的,所以,在我們的例子中,JVM的參數(shù)只用-server一樣可以有棧上替換的效果(以King老師機(jī)器上JDK1.8為例,其他版本JDK請(qǐng)自行嘗試)。

棧上分配的效果

同樣的User的對(duì)象實(shí)例,分配100000000次,啟用棧上分配,只需6ms,不啟用,需要900多ms。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容