程序計(jì)數(shù)器
是一塊較小的內(nèi)存空間,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器谓松。在虛擬機(jī)的概念模型里簸淀,字節(jié)碼解釋器工作時(shí)通過改變計(jì)數(shù)器的值來選取下一跳需要執(zhí)行的字節(jié)碼指令。
由于java虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行實(shí)踐來實(shí)現(xiàn)的毒返,在任何一個(gè)確定的時(shí)刻租幕,一個(gè)處理器只能執(zhí)行一條線程的指令。為了線程切換后能恢復(fù)到正確的執(zhí)行位置拧簸,每條線程都需要一個(gè)獨(dú)立的程序計(jì)數(shù)器劲绪,讓各個(gè)線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),因此程序計(jì)數(shù)器是“私有”的贾富。
如果線程執(zhí)行的是java方法歉眷,那計(jì)數(shù)器記錄的就是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果執(zhí)行的是native方法颤枪,計(jì)數(shù)器值為空(undefined)汗捡。此內(nèi)存區(qū)域是唯一一個(gè)不會(huì)出現(xiàn)OOM(OutOfMemoryError)情況的區(qū)域。
Java虛擬機(jī)棧
Java虛擬機(jī)棧也是“私有”的畏纲,生命周期與線程相同扇住。描述的是Java方法執(zhí)行的內(nèi)存模型,每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀盗胀,用于存儲(chǔ)局部變量表艘蹋、操作數(shù)棧、動(dòng)態(tài)鏈接票灰、方法出口等信息女阀。一個(gè)方法從調(diào)用到執(zhí)行完成的過程,就對(duì)應(yīng)一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過程屑迂。
局部變量表存放了編譯時(shí)期可知的各種基本數(shù)據(jù)類型浸策、對(duì)象引用(reference類型,不等同對(duì)象本身惹盼,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔樀拈唬部赡苁侵甘疽粋€(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置)和returnAddress(指向了一條字節(jié)碼指令的地址)。局部變量表所需的內(nèi)存空間在編譯期間完成分配逻锐,在方法運(yùn)行期間不會(huì)改變局部變量表的大小夫晌。
本地方法棧
與虛擬機(jī)棧相似,但虛擬機(jī)棧執(zhí)行的是java方法昧诱,而本地方法棧執(zhí)行的是native方法晓淀。
Java堆
對(duì)于絕大多數(shù)應(yīng)用來說,堆是Java虛擬機(jī)所管理的內(nèi)存中最大的一塊盏档,被所有線程所共享凶掰,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例蜈亩,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存懦窘。
堆是垃圾收集器管理的主要區(qū)域,從內(nèi)存回收的角度來看稚配,由于現(xiàn)在收集器基本采用分代收集算法畅涂,所以java堆還可以細(xì)分為:新生代和老年代,再往細(xì)里分可以分為:Eden空間道川、From Survivor空間午衰、To Survivor空間等(8:1:1)立宜;從內(nèi)存分配的角度來看,線程共享的堆可能劃分出多個(gè)線程私有的分配緩沖區(qū)(TLAB Thread Local Allocation Buffer)臊岸。進(jìn)一步劃分的目的是為了更好的回收內(nèi)存橙数,或者更快地分配內(nèi)存。
堆可以處于物理上不連續(xù)的內(nèi)存空間帅戒。在實(shí)現(xiàn)時(shí)灯帮,可以是固定大小,也可以是可擴(kuò)展(通過-Xmx和-Xms控制:最大和最小的堆內(nèi)存指定)逻住。
Java方法區(qū)
與堆一樣钟哥,是各個(gè)線程所共享的內(nèi)存區(qū)域,用于存儲(chǔ)已被虛擬機(jī)加載的類信息鄙信、常量瞪醋、靜態(tài)變量忿晕、即使編譯器編譯后的代碼等數(shù)據(jù)(可以看作是堆的一個(gè)邏輯部分)装诡。
對(duì)于HotSpot來說,方法區(qū)也稱為永久代践盼,GC分代手機(jī)擴(kuò)展至方法區(qū)鸦采,或者說使用永久帶來實(shí)現(xiàn)方法區(qū),這樣垃圾收集器就可以像管理堆一樣管理方法區(qū)咕幻,省去專門設(shè)計(jì)方法區(qū)內(nèi)存管理(永久代可以使用-XX:MaxPermSize來設(shè)置上限)渔伯。在JDK7后,把原本放在永久代的字符串常量池移出肄程。
垃圾收集在這塊區(qū)域比較少出現(xiàn)锣吼,這塊區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載。
運(yùn)行時(shí)常量池
是方法區(qū)的一部分蓝厌。Class文件中除了有類的版本玄叠、字段、方法拓提、接口等描述信息外读恃,還有一項(xiàng)信息就是常量池,用于存放編譯期生成的各種字面常量和符號(hào)引用代态,這部分內(nèi)容在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放寺惫。
運(yùn)行時(shí)常量池相對(duì)于Class文件常量池的另外一個(gè)重要特征是具備動(dòng)態(tài)性,運(yùn)行時(shí)期有可能將新的常量放入池中蹦疑,如String的intern()方法西雀。
對(duì)象創(chuàng)建
1、虛擬機(jī)在遇到一條new指令時(shí)歉摧,首相將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用蒋搜,并且檢查這個(gè)符號(hào)引用代表的類是否已經(jīng)貝加載篡撵、解析和初始化過,如果沒有先去加載類豆挽;
2育谬、在類加載檢查通過后,虛擬機(jī)將為新生對(duì)象分配內(nèi)存帮哈。對(duì)象所需內(nèi)存的大小在類加載完成后便可完全確定膛檀,為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從堆中劃分出來:
假設(shè)堆中的內(nèi)存絕對(duì)規(guī)整,所有分配內(nèi)存放在一邊娘侍,所有空閑內(nèi)存放在另一邊咖刃,通過一指針作為分界點(diǎn)的指示器,這種方式成為“指針碰撞”憾筏;
如果堆內(nèi)存不規(guī)整嚎杨,已使用和空閑的內(nèi)存相互交錯(cuò),那么虛擬機(jī)就要維護(hù)一個(gè)列表氧腰,記錄下哪些內(nèi)存塊可用枫浙,在分配的時(shí)候從列表中找到一塊足夠大的空間劃分給對(duì)象實(shí)例,并更新列表上的記錄古拴,這種分配方式成為“空閑列表”箩帚;
劃分可用空間(分配內(nèi)存)還有一個(gè)需要考慮的問題:對(duì)象創(chuàng)建在虛擬機(jī)中是非常頻繁的行為,即使是僅僅修改一個(gè)指針?biāo)赶虻奈恢没苹荆诓l(fā)的情況下也并不是線程安全的(可能在給對(duì)象A分配內(nèi)存指針還沒來得及修改時(shí)紧帕,對(duì)象B又同時(shí)使用了原來的指針來分配內(nèi)存的情況),解決方案有兩種:
對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理--實(shí)際上虛擬機(jī)采用CAS分配失敗重試的方式保證更新操作的原自性桅打;
另一種是把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間之中進(jìn)行是嗜,即每個(gè)線程在堆中預(yù)先分配一小塊的內(nèi)存,成為TLAB挺尾,哪個(gè)線程需要分配內(nèi)存就在哪個(gè)線程的TLAB上分配鹅搪,當(dāng)TLAB用完并分配新的TLAB時(shí)才需要同步鎖定(--XX:+/-UseTLAB參數(shù)設(shè)定)。
3潦嘶、內(nèi)存分配完成后涩嚣,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭),如果使用了TLAB掂僵,也可提前在TLAB分配時(shí)進(jìn)行
4航厚、虛擬機(jī)對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例锰蓬、如何才能找到元數(shù)據(jù)信息幔睬、對(duì)象的哈希碼、對(duì)象的GC分代年齡等信息芹扭,這些信息都存放在對(duì)象的對(duì)象頭中
5麻顶、至此赦抖,從虛擬機(jī)的視角來看,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生辅肾,但從java程序的視角來看队萤,對(duì)象的創(chuàng)建才剛剛開始--<init>方法還沒執(zhí)行,所有的字段都為零矫钓。一般來說(字節(jié)碼中是否跟隨invokespecial指令決定)要尔,執(zhí)行new指令之后會(huì)接著執(zhí)行<init>方法,把對(duì)象按照程序員的意愿進(jìn)行初始化新娜,這樣子一個(gè)真正可用的對(duì)象才算完全產(chǎn)生出來
對(duì)象的內(nèi)存布局
在HotSpot虛擬機(jī)中赵辕,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為:對(duì)象頭、實(shí)例數(shù)據(jù)和補(bǔ)充對(duì)齊
對(duì)象頭包含兩部分信息:一部分用戶存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)概龄,如哈希碼还惠、GC分代年齡、鎖狀態(tài)標(biāo)志私杜、線程持有的鎖蚕键、偏向線程ID、偏向時(shí)間戳等歪今,這部分被官方稱為“Mark Word”嚎幸。對(duì)象要存儲(chǔ)的數(shù)據(jù)很多颜矿,可能超出32位寄猩、64位Bitmap結(jié)構(gòu)所能記錄的限度,但是對(duì)象頭信息是對(duì)象自身定義的數(shù)據(jù)無關(guān)的額外存儲(chǔ)成本骑疆,被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)田篇,以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息,它會(huì)根據(jù)對(duì)象的狀態(tài)復(fù)用自己的存儲(chǔ)空間箍铭,如在32位的HotSpot中泊柬,如果對(duì)象處于未鎖定狀態(tài),那么32位中的25位用來存儲(chǔ)hash诈火,4bit用來存儲(chǔ)對(duì)象分代年齡兽赁,2bit用來存儲(chǔ)鎖標(biāo)識(shí)為,1bit固定0冷守;
另外一部分是類型指針刀崖,即指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例拍摇。如果對(duì)象是一個(gè)數(shù)組亮钦,那么在對(duì)象頭中還有一塊用于記錄數(shù)組長度的數(shù)據(jù)。
實(shí)例數(shù)據(jù):對(duì)象的真正存儲(chǔ)的有效信息充活,也是程序代碼中所定義的各類型的字段內(nèi)容蜂莉。無論是從父類繼承下來的還是在子類中定義的蜡娶,都需要記錄起來。這部分的存儲(chǔ)順序受虛擬機(jī)分配策略和字段在java源碼中定義順序的影響映穗。HotSpot默認(rèn)的分配策略是相同寬度的字段總是被分配到一起窖张,在滿足這個(gè)前提的情況下,在父類中定義的變量會(huì)出現(xiàn)在子類之前蚁滋。如果CompactFields參數(shù)值位true荤堪,那么子類總較窄的變量也可能會(huì)插入到父類變量的空隙中。
對(duì)齊填充:期占位符作用枢赔。因?yàn)镠otSpot自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍澄阳。
對(duì)象的訪問定位
建立對(duì)象是為了使用對(duì)象,java程序需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對(duì)象踏拜。目前主流的訪問方式有句柄和直接指針兩種:
如果使用句柄訪問碎赢,堆中會(huì)劃分出一塊內(nèi)存來作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址速梗,而句柄包含了對(duì)象實(shí)例數(shù)據(jù)與數(shù)據(jù)類型各自的具體地址信息
如果使用直接指針訪問肮塞,reference中存儲(chǔ)的是對(duì)象地址
使用句柄的好處在于reference中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(GC移動(dòng)對(duì)象)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針姻锁,而reference本身不需要修改枕赵;使用直接指針訪問方式的好處是速度更快,節(jié)省了一次指針定位的時(shí)間開銷位隶。