java虛擬機棧:
每個方法執(zhí)行時創(chuàng)建棧幀明未,存儲局部變量表槽华,操作數(shù)棧,動態(tài)鏈接趟妥,方法出入口等信息猫态。一個方法調(diào)用到完成過程,就是一個棧幀在虛擬機棧中入到到出棧過程披摄。
局部變量表存放編譯器可知的基本數(shù)據(jù)類型亲雪、對象引用(reference類型)和returnAddress類型(指向了一條字節(jié)碼指令的地址)
64位的long和double類型占用兩個局部變量空間(slot),其余類型只占用一個疚膊。局部變量表需要內(nèi)存空間在編譯期間完成分配义辕,進入一個方法時,這個方法需要在幀中分配多大局部變量表示確定的寓盗,方法運行期間不會改變局部變量表大小灌砖。
在棧區(qū)域規(guī)定了兩種異常:
如果線程請求棧深度大于虛擬機允許的深度,拋出stackoverflowerror異常
如果虛擬機可以動態(tài)擴展(大部分都可以)傀蚌,如果擴展時無法申請到足夠內(nèi)存基显,拋出outofmemoryerror異常
java堆:
可以處于物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可善炫。通過-Xms -Xmx實現(xiàn)可擴展撩幽。如果堆中沒有內(nèi)存完成實例分配,并且堆無法再擴展時销部,拋出oom異常
方法區(qū):
和堆一樣摸航,各個線程共享,存儲已經(jīng)被虛擬機加載的類信息舅桩、常量酱虎、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)擂涛。java虛擬機規(guī)范將方法區(qū)描述為堆的一個邏輯部分读串,但是它卻有一個別名Non-heap(非堆),目的應(yīng)該是和堆區(qū)分出來撒妈。
方法區(qū)和永久代并不等價恢暖。只是因為hotspot虛擬機設(shè)計團隊選擇將gc分代收集擴展至方法區(qū),或者說用永久代實現(xiàn)方法區(qū)而已狰右,這樣hotspot垃圾收集器可以像管理java堆一樣管理這部分內(nèi)存杰捂,省去專門為方法區(qū)編寫內(nèi)存管理的代碼。對其他虛擬機(如bea jrockit)不存在永久代概念棋蚌。
使用永久代實現(xiàn)方法區(qū)并不合適嫁佳,容易內(nèi)存溢出挨队,永久代有-XX:MaxPermSize的上限,J9和JRockit只要沒到進程可用內(nèi)存上限(如32位4Gb)就不會有問題蒿往。而從jdk1.7的hotspot開始盛垦,已經(jīng)把原本在永久代的字符串常量池移出。
方法區(qū)和堆一樣不需要連續(xù)內(nèi)存瓤漏,可以選擇固定大小或可擴展腾夯,另外還可以選擇不實現(xiàn)垃圾回收。無法滿足內(nèi)存分配拋出oom.
運行時常量池:
方法區(qū)一部分蔬充。class文件中除了有類版本蝶俱,字段,方法娃惯,接口等描述信息外跷乐,還有一項信息是常量池(constant pool table)肥败,用于存放編譯期生成的字面量和符號引用趾浅,這部分將在類加載后進入方法區(qū)的運行時常量池中存放。
運行時常量池相對class常量池一個特征就是具有動態(tài)性馒稍,java并不要求常量只有編譯期才能產(chǎn)生皿哨,就是并非預(yù)置入class文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中纽谒,比如String類的intern()方法证膨。
直接內(nèi)存:
直接內(nèi)存(Direct Memory)不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是java虛擬機規(guī)范中定義的內(nèi)存區(qū)域鼓黔,但這部分內(nèi)存也可能導致oom央勒。
jdk1.4中加入了nio(new input/output)類,引入了基于通道(channel)與緩沖區(qū)(buffer)的I/O方法澳化,使用native函數(shù)庫分配堆外內(nèi)存崔步,通過java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作。在一些場合可以提高性能缎谷,因為避免了java堆和native堆中來回復(fù)制數(shù)據(jù)井濒。
對象的創(chuàng)建
虛擬機遇到一條new指令時,首先將去檢查這個指令是否能在常量池中定位到一個類的符號引用列林,并且檢查這個符號引用代表的類是否已經(jīng)被加載瑞你、解析和初始化過,如果沒有希痴,那必須先執(zhí)行相應(yīng)的類加載過程者甲。在類加載檢查通過后,虛擬機將為新生對象分配內(nèi)存砌创,對象所需的內(nèi)存大小在類加載完成后便可以確定虏缸。
java劃分堆的方式:
指針碰撞:如果java堆內(nèi)存規(guī)整甥厦,用過的內(nèi)存放一邊,空閑的另一邊寇钉,中間放指針做分界點指示器刀疙,那內(nèi)存分配就只需要向空閑區(qū)挪動指針和對象大小相等距離。
空閑列表:不規(guī)整扫倡,維護一個列表谦秧,記錄那些內(nèi)存塊可用。
java堆是否規(guī)整由垃圾收集器是否由壓縮整理功能決定撵溃。使用Serial,parnew帶compact過程的收集器時疚鲤,采用指針碰撞。使用cms這種基于mark-sweep算法的收集器缘挑,采用空閑列表集歇。
并發(fā)情況下移動指針有安全問題:
1.對分配內(nèi)存空間動作進行同步處理,使用cas配失敗重試保證更新原子性语淘。
2.把內(nèi)存分配的動作按照線程劃分在不同空間中進行诲宇,每個線程在java堆中分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)惶翻。哪個線程要分配內(nèi)存姑蓝,就在哪個線程的TLAB上分配,只有TLAB用完并分配新的TLAB時吕粗,才需要同步鎖定纺荧。是否使用TLAB通過-XX:+/-UseTLAB參數(shù)來設(shè)定。
JVM在內(nèi)存新生代Eden Space中開辟了一小塊線程私有的區(qū)域颅筋,稱作TLAB(Thread-local allocation buffer)宙暇。默認設(shè)定為占用Eden Space的1%。在Java程序中很多對象都是小對象且用過即丟议泵,它們不存在線程共享也適合被快速GC占贫,所以對于小對象通常JVM會優(yōu)先分配在TLAB上,并且TLAB上的分配由于是線程私有所以沒有鎖開銷肢簿。因此在實踐中分配多個小對象的效率通常比分配一個大對象的效率要高靶剑。
也就是說,Java中每個線程都會有自己的緩沖區(qū)稱作TLAB(Thread-local allocation buffer)池充,每個TLAB都只有一個線程可以操作桩引,TLAB結(jié)合bump-the-pointer技術(shù)可以實現(xiàn)快速的對象分配,而不需要任何的鎖進行同步收夸,也就是說坑匠,在對象分配的時候不用鎖住整個堆,而只需要在自己的緩沖區(qū)分配即可卧惜。
接下來厘灼,虛擬機對對象進行必要的設(shè)置夹纫,如對象是哪個類實例,如何找到類元數(shù)據(jù)信息设凹,對象哈希碼舰讹,對象GC分代年齡等信息。這些信息存放在對象頭中(Object Header).
上面工作完成后闪朱,從虛擬機視角看月匣,一個新對象已經(jīng)產(chǎn)生,但java視角對象創(chuàng)建才開始奋姿,<init>方法還未執(zhí)行锄开,字段還為零。所以一般來說(由字節(jié)碼是否跟隨invokespecial指令決定)称诗,new指令后會接著執(zhí)行<init>方法初始化萍悴,可用對象才算產(chǎn)生。
對象結(jié)構(gòu)
hotspot虛擬機中寓免,對象分為三塊區(qū)域:對象頭(Header),實例數(shù)據(jù)(Instance Data),對齊填充(Padding)
對象頭包括兩部分:1.用于存儲對象自身的運行時數(shù)據(jù)癣诱。如哈希碼,GC分代年齡再榄,鎖狀態(tài)標志狡刘,線程持有的鎖,偏向線程ID,偏向時間鎖等创肥。官方稱為Mark Word
2.類型指針徙赢。即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針確定這個對象是哪個類的實例肩碟。
對齊填充僅僅占位符作用,用于補齊沒有對齊的實例數(shù)據(jù)部分(對象大小必須8字節(jié)整數(shù)倍)。
對象的訪問定位
句柄:會在堆中劃分句柄池猬腰,ref存儲對象的句柄池地址,句柄中包括對象實例數(shù)據(jù)和類型數(shù)據(jù)的地址猜敢。
直接指針:ref存儲的直接就是對象地址姑荷。
句柄好處:對象移動(gc)時候只需要改變句柄的實例數(shù)據(jù)指針,ref本身不變缩擂。
直接指針:訪問速度快鼠冕,節(jié)省一次指針定位開銷。hotspot使用這種胯盯。