全文概括
? 虛擬機(jī)可以看作一臺(tái)抽象的計(jì)算機(jī)企蹭,有自己的指令集和運(yùn)行時(shí)內(nèi)存分區(qū)雳攘。堆和方法區(qū)是線程共享的又兵,隨JVM創(chuàng)建消亡任柜。棧和PC計(jì)數(shù)器是線程私有的。
? 堆是最大的一塊沛厨,為對象分配內(nèi)存就是從堆中分配宙地。分配時(shí)需要考慮并發(fā)操作。
? 方法區(qū)存放各個(gè)類的信息逆皮,包括運(yùn)行時(shí)常量池宅粥、字節(jié)碼、和初始化方法电谣。
? 一個(gè)線程一個(gè)棧秽梅,一個(gè)方法一個(gè)棧幀抹蚀。棧幀中有局部變量表、操作數(shù)棧和常量池指針企垦。
? 將類信息加載到方法區(qū)中况鸣,對象實(shí)例在堆中分配內(nèi)存,使棧的本地變量中對象的引用指向堆中的實(shí)例竹观,實(shí)例再指向方法區(qū)的類型镐捧。然后執(zhí)行<init>初始化這塊實(shí)例內(nèi)存。
正文?
全部手敲臭增,如果需要轉(zhuǎn)載請注明出處懂酱。
主要知識(shí)來自《Java虛擬機(jī)規(guī)范》(Java SE8) 周志明翻譯的那本。還有《深入理解Java虛擬機(jī):JVM高級特性與最佳實(shí)踐》
此外還參考了下面這些鏈接文章:http://www.reibang.com/p/eaef248b5a2c誊抛;https://blog.csdn.net/uotail/article/details/83373794
? Java虛擬機(jī)可以看作一臺(tái)抽象的計(jì)算機(jī)列牺,有自己的指令集和運(yùn)行時(shí)內(nèi)存區(qū)域
? 這些分區(qū)是邏輯上的,是抽象的拗窃。和計(jì)算機(jī)的實(shí)際內(nèi)存是不同的瞎领。比如Java棧實(shí)際上是被計(jì)算機(jī)分配在計(jì)算機(jī)的堆上的。
? 這些分區(qū)有些是所有線程共享的随夸,隨著虛擬機(jī)的創(chuàng)建和銷毀九默。有些是線程私有的,每個(gè)線程都有一份宾毒,隨著線程創(chuàng)建和銷毀
一驼修、 線程共享的區(qū)域
1.1 堆 Java Heap
? 被所有線程共享的一塊內(nèi)存區(qū)域,主要用于存放對象實(shí)例诈铛。
? 是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊兒乙各,物理上可以不連續(xù)。所有的對象實(shí)例和數(shù)據(jù)都要在堆上進(jìn)行分配幢竹。這些對象被GC管理著耳峦。
1.1.1 為對象分配內(nèi)存的方法*
? 為對象分配內(nèi)存就是把一塊大小確定的內(nèi)存從堆內(nèi)存中劃分出來。
? Java虛擬機(jī)采用哪種方式為新生對象分配內(nèi)存焕毫,取決于所使用的垃圾收集器蹲坷。
? 為了分帶垃圾收集,堆可以細(xì)分為新生代(細(xì)分為Eden:from:to)和老年代(Old Generation)咬荷。
? 當(dāng)垃圾收集器具有整理過程時(shí)冠句,虛擬機(jī)將采用指針碰撞的方式轻掩;當(dāng)垃圾收集器的回收過程沒有整理過程時(shí)幸乒,則采用空閑列表方式。
指針碰撞法
? 已分配的內(nèi)存和空閑內(nèi)存分別在不同的一側(cè)唇牧,通過一個(gè)指針作為分界點(diǎn)罕扎,需要分配內(nèi)存時(shí)聚唐,僅僅需要把指針往空閑的一端移動(dòng)與對象大小相等的距離。
空閑列表法
? 已分配的內(nèi)存和空閑內(nèi)存相互交錯(cuò)腔召,JVM通過維護(hù)一個(gè)列表杆查,記錄可用的內(nèi)存塊信息
1.1.2 多線程如何并發(fā)分配內(nèi)存?
對象創(chuàng)建是一個(gè)非常頻繁的行為臀蛛,進(jìn)行堆內(nèi)存分配時(shí)還需要考慮多線程并發(fā)問題亲桦,可能出現(xiàn)正在給對象A分配內(nèi)存,指針或記錄還未更新浊仆,對象B又同時(shí)分配到原來的內(nèi)存客峭,解決這個(gè)問題有兩種方案:
CAS(Compare And Swap)*
? 比較并替換,實(shí)現(xiàn)并發(fā)算法時(shí)常用到的一種技術(shù)抡柿。在Java中舔琅,主要在Atomic
包,調(diào)用Unsafe
類相關(guān)方法體現(xiàn)洲劣。
? 涉及三個(gè)值:內(nèi)存中真正存的值(可能被其他線程改變)备蚓、邏輯上的原值、(當(dāng)前線程)要寫入的新值囱稽。通過循環(huán)檢查內(nèi)存中的值是不是原來的值郊尝,以此來判斷是不是正有其他線程在改變它。判斷成功立即寫入新值战惊,這一步是原子的虚循。
? 存在ABA問題,即內(nèi)存中雖然還是邏輯上原來的值样傍,但是已經(jīng)被變成B過了横缔。這個(gè)問題JDK1.5已經(jīng)通過AtomicStampedReference
類解決,將對象追加一個(gè)版本號(hào)stamp
衫哥,這樣每次改變過都是一個(gè)“全新的值”
? 具體可以看我的這篇文章http://www.reibang.com/p/c8e9bce8b3c6
本地線程分配緩沖(Thread Local Allocation Buffer, TLAB)
? 把內(nèi)存分配的行為按照線程進(jìn)行劃分茎刚,在不同的空間中進(jìn)行,每個(gè)線程在Java堆中預(yù)先分配一個(gè)內(nèi)存塊撤逢。哪個(gè)線程要分配內(nèi)存膛锭,就在它的TLAB上分配,如果用完了蚊荣,再追加時(shí)再去考慮同步初狰。JVM參數(shù)中-XX:+/-UseTLAB
可以設(shè)定是否使用此策略。
1.1.3 堆溢出
? 堆是用來存儲(chǔ)對象實(shí)例的互例。如果對象達(dá)到一定的數(shù)量奢入,并且這些對象和根對象之間有可達(dá)路徑,那么就不能被垃圾收集器回收媳叨,超過堆的最大容量腥光,就會(huì)出現(xiàn)溢出关顷,拋OutOfMemoryError
? 具體原因可能有兩種。首先可能是出現(xiàn)了死循環(huán)武福。其次可能是啟動(dòng)JVM時(shí)分配的堆太小议双,如-Xms100m -Xmx200m
設(shè)置堆最小100最大200m。
1.2 方法區(qū)
? 方法區(qū)用于每個(gè)類的結(jié)構(gòu)信息和一些特殊方法捉片。類的結(jié)構(gòu)信息不僅有字段和方法的數(shù)據(jù)平痰、構(gòu)造函數(shù)和普通方法的字節(jié)碼,還有運(yùn)行時(shí)常量池伍纫。而特殊方法主要是指實(shí)例初始化<init>方法觉增、類或接口初始化<clinit>方法。
1.2.1 類的結(jié)構(gòu)信息
運(yùn)行時(shí)常量池 Runtime constant pool
? 它包括了若干種不同的常量翻斟,從編譯期可知的數(shù)值字面量到必須在運(yùn)行期解析后才能獲得的方法或者字段引用逾礁。
? 一些虛擬機(jī),只想對象實(shí)例的引用是一個(gè)指向句柄的指針访惜。這個(gè)句柄由包含了兩個(gè)指針:一個(gè)指向該對象各個(gè)方法及對應(yīng)的Class對象的表格嘹履。另一個(gè)指向在堆中分配的數(shù)據(jù)。
? 但HotSpot VM中债热,指向?qū)ο蟮囊貌⒉煌ㄟ^句柄砾嫉,而是直接指向堆中對象的實(shí)例數(shù)據(jù)。這個(gè)詳見第三部分窒篱。
1.2.2 初始化方法*
<init>實(shí)例初始化方法
? 只能在實(shí)例初始化期間通過JVM的invokespecial
指令調(diào)用焕刮。且只能在尚為初始化的實(shí)例上調(diào)用一次。
<clinit>類或接口初始化方法
? 由JVM自身隱式調(diào)用墙杯,沒有任何JVM字節(jié)碼指令可以調(diào)用這個(gè)方法配并。只會(huì)在類的初始化階段中由虛擬機(jī)自身調(diào)用
1.2.3 方法區(qū)溢出
如果運(yùn)行時(shí)方法區(qū)產(chǎn)生了大量的類,超出了方法區(qū)的最大容量高镐,將拋出OutOfMemoryError異常溉旋。
二、線程私有的分區(qū)
2.1 Java棧
? Java棧是線程私有的嫉髓,是Java方法的執(zhí)行模型观腊。每個(gè)線程對應(yīng)一個(gè)棧,每個(gè)線程在執(zhí)行一個(gè)方法時(shí)會(huì)創(chuàng)建一個(gè)對應(yīng)的棧幀(Stack Frame)算行,棧幀負(fù)責(zé)存儲(chǔ)局部變量變量表梧油、操作數(shù)棧、動(dòng)態(tài)鏈接和方法返回地址等信息州邢。每個(gè)方法的調(diào)用過程儡陨,相當(dāng)于棧幀在Java棧的入棧和出棧過程
2.1.1 棧幀
? 隨方法調(diào)用創(chuàng)建和銷毀,同時(shí)可能存在多個(gè),但只有棧頂?shù)漠?dāng)前棧幀是活動(dòng)的迄委。
如上圖褐筛,一個(gè)線程中的方法調(diào)用鏈可能會(huì)很長类少,很多方法都同時(shí)處于執(zhí)行狀態(tài)叙身。對于執(zhí)行引擎來講,活動(dòng)線程中硫狞,只有虛擬機(jī)棧頂?shù)臈攀怯行У男沤危Q為當(dāng)前棧幀(Current Stack Frame),這個(gè)棧幀所關(guān)聯(lián)的方法稱為當(dāng)前方法(Current Method)残吩。
局部變量表 Local Variable Table
? 局部變量存放了編譯器可知的各種類型财忽,即JVM可操作的類型。
? 通過方法的code屬性保存及提供給棧幀使用泣侮。局部變量表所需的內(nèi)存空間在編譯期間完成分配即彪。
? 以變量槽(Slot)為最小存儲(chǔ)單位,可以放32bit活尊。對于long和double隶校,以高位對齊方式分配兩個(gè)連續(xù)Slot。
JVM可操作的數(shù)據(jù)類型
原始類型 primitive
-
數(shù)值類型 numeric type
整型(byte/short/int/long/char)和浮點(diǎn)型(float/double)
-
boolean類型
false是0蛹锰,true是1深胳。沒有專門的字節(jié)碼指令,只能轉(zhuǎn)換成byte或int再操作
-
returnAddress類型
指向一條JVM操作碼opcode铜犬,原始類型中唯一一個(gè)不能直接和Java的數(shù)據(jù)類型相對應(yīng)的舞终。無法在程序運(yùn)行期間修改。
引用類型 reference
- 類class 類型
- 數(shù)組array 類型
- 接口interface類型
- null
字節(jié)碼指令
? 字節(jié)開頭字母大多指明了為哪種數(shù)據(jù)類型服務(wù):
a-reference癣猾,d-double敛劝,f-float,c-char纷宇,b-byte攘蔽,s-short,l-lomg呐粘,i-int
? 使用索引定位訪問满俗,第0個(gè)位置一定是用來存儲(chǔ)this
,即當(dāng)前對象的引用作岖。隨便把一個(gè)a.java文件javac
編譯一下唆垃,再用javap -verbose a
就可以看到任意一個(gè)非static的方法,前兩句指令都是aload_0
(將this拿到)和dup
(裝入操作數(shù)棧)痘儡。就是方便線程訪問當(dāng)前對象的類信息和類域
操作數(shù)棧 operand stack
? 用來暫存指令取出來的操作數(shù)辕万,也用來準(zhǔn)備調(diào)用方法的參數(shù)及接收方法返回的結(jié)果。
常量池指針 constant_pool point
? 動(dòng)態(tài)鏈接,在類加載的過程中渐尿,將代碼中的符號(hào)引用轉(zhuǎn)換成了直接引用醉途。
2.1.2 棧溢出
? 如果線程請求的棧深度,大于虛擬機(jī)所允許的深度砖茸,將拋出StackoverflowError異常隘擎。
? 但棧的深度也不是越深越好,不然一個(gè)線程就要耗費(fèi)很多內(nèi)存凉夯,如果擴(kuò)展時(shí)無法申請到足夠的內(nèi)存货葬,就會(huì)拋出OutOfMemoryError異常。?
2.2 本地方法棧
? 執(zhí)行native方法劲够,也會(huì)拋出StackoverflowError異常和OutOfMemoryError異常震桶,有的虛擬機(jī)(如HotSpot VM)將本地方法棧與上面的虛擬機(jī)棧合二為一。
2.3 PC指令計(jì)數(shù)器 program counter
? 記錄著JVM正在執(zhí)行的opcode的地址征绎。分支蹲姐、循環(huán)、跳轉(zhuǎn)人柿、異常處理和線程恢復(fù)等操作都依賴這個(gè)計(jì)數(shù)器完成柴墩。如果當(dāng)前方法是native,pc寄存器是undefined顷扩。
三拐邪、對象怎么使用JVM內(nèi)存分區(qū)
3.1 在內(nèi)存中的布局
? 以HotSpot為例對象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對象頭(Header)、實(shí)例數(shù)據(jù)(Instance)隘截、對象填充(Padding)扎阶。
3.1.1 對象頭
markword
? 用來存儲(chǔ)對象自身的運(yùn)行時(shí)數(shù)據(jù),例如hashcode婶芭、GC年代东臀、鎖狀態(tài)、線程持有的鎖犀农、偏向線程的ID惰赋、偏向時(shí)間戳等。
Class對象指針
? 也叫類型指針呵哨,指向這個(gè)對象的類的元數(shù)據(jù)赁濒,JVM樂意根據(jù)它知道這個(gè)對象是哪個(gè)類的實(shí)例。
元數(shù)據(jù)(meta data)
? 結(jié)構(gòu)化的數(shù)據(jù)孟害。將任何信息拆解成元數(shù)據(jù)項(xiàng)目(名字/鍵)和元數(shù)據(jù)內(nèi)容(值)拒炎,并且可以有層次。比如XML
中挨务,每個(gè)標(biāo)簽都是一個(gè)元數(shù)據(jù)击你。每個(gè)標(biāo)簽可以有多個(gè)子標(biāo)簽玉组,每個(gè)標(biāo)簽都有自己的屬性,他們表明了這個(gè)元數(shù)據(jù)要表達(dá)的意圖丁侄。
3.1.2 實(shí)例數(shù)據(jù)
? 即程序中的字段惯雳,包括父類繼承來的。這部分的存儲(chǔ)順序和虛擬機(jī)還有源碼中的位置都有關(guān)系鸿摇,HotSpot默認(rèn)是相同寬度的字段在一起石景。long/double> int > short/char > byte/boolean > oop(Ordinary Object Pointers)。但子類中較窄的可能會(huì)插入父類的空隙里户辱。
3.1.3 對象填充
? HotSpot VM 要求對象的起始地址是8字節(jié)的整數(shù)倍鸵钝。
3.2 對象怎么創(chuàng)建
? 在JVM遇到new時(shí)(這里是普通的對象非數(shù)組或Class)糙臼,檢查這個(gè)類符號(hào)引用在不在方法區(qū)的常量池中庐镐,進(jìn)而檢查是否被加載過。如果沒有变逃,首先要執(zhí)行類加載必逆。
? 如果已加載,那么要給新對象分配內(nèi)存揽乱,即在Java堆中劃出一塊內(nèi)存來(大小在類加載完成確定)名眉。
? 將分配到的內(nèi)存(除對象頭外)都初始化為0。如果使用TLAB凰棉,則這一步需提到TLAB前损拢。
? 根據(jù)對象頭,對這個(gè)對象進(jìn)行必要設(shè)置撒犀。
? 至此福压,對于JVM,新對象已經(jīng)產(chǎn)生或舞。但對于Java程序來說荆姆,對象創(chuàng)建是這之后執(zhí)行<init>的過程。
? 字節(jié)碼中如果跟隨了invokespecial
指令映凳,那么執(zhí)行new指令后接著執(zhí)行方法區(qū)的<init>方法胆筒。
3.3 對象的引用
? Java程序通過棧上的引用類型reference來操作堆中的具體對象。目前主流的訪問方式有兩種诈豌,句柄和直接指針仆救。HotSpot VM 使用的是直接指針。
句柄訪問模式
? 好處是矫渔,reference值不變彤蔽,在對象被移動(dòng)(GC時(shí)需要常常移動(dòng)),只改變了句柄中的實(shí)例數(shù)據(jù)指針蚌斩。
直接指針訪問
? 好處是節(jié)省了一次指針定位的時(shí)間開銷铆惑。對象的訪問是一件非常頻繁的事范嘱。
謝謝觀看,如果有用麻煩點(diǎn)個(gè)喜歡员魏,對我是莫大的鼓勵(lì)丑蛤。