剛工作的時候岁疼,Leader跟我說:你現(xiàn)在只知道寫業(yè)務(wù)代碼阔涉,但不了解自己的JVM運行情況,怎么去調(diào)優(yōu),怎么去排查故障瑰排。只有真正了解掌控JVM贯要,才能稱為一個真正的Java專家⊥肿。《深入理解Java虛擬機(jī)》這本書崇渗,是介紹JVM知識的一本十分難得的書,值得去反復(fù)閱讀京郑、揣摩宅广、反思。這個系列的文章傻挂,是我在閱讀這本書的過程中的點滴筆記乘碑,如果喜歡,請更多支持原書作者出版的圖書金拒。同時兽肤,如果喜歡本文,請給與多多支持绪抛。
1.概述
對于C资铡、C++開發(fā)人員來說,在內(nèi)存管理方面幢码,擁有對每個對象的絕對控制能力笤休,從對象的產(chǎn)生到終結(jié),承擔(dān)著全部責(zé)任症副。
對于Java開發(fā)人員來說店雅,在虛擬機(jī)的幫助下,不需要為每個對象去寫刪除釋放方法贞铣,不容易出現(xiàn)內(nèi)存泄露和內(nèi)存溢出的問題闹啦。不過,正是由于太過依賴于JVM辕坝,一旦出現(xiàn)內(nèi)存泄露和內(nèi)存溢出的問題窍奋,排查錯誤將會是一件很難的工作。
2.運行時數(shù)據(jù)區(qū)域
Java虛擬機(jī)在執(zhí)行Java程序的過程中會把它管理的內(nèi)存劃為若干個不同的數(shù)據(jù)區(qū)域酱畅。
2.1 程序計數(shù)器
程序計數(shù)器:一塊較小的內(nèi)存空間琳袄,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。每條線程都需要有一個獨立的程序計數(shù)器纺酸。
在JVM的概念模型中窖逗,字節(jié)碼解釋器工作時,通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令餐蔬,分支碎紊、循環(huán)在张、跳轉(zhuǎn)、異常處理矮慕、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成帮匾。
如果線程正在執(zhí)行的是一個Java方法,計數(shù)器記錄的值是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址痴鳄;如果正在執(zhí)行的是一個Native方法瘟斜,計數(shù)器記錄的值為空。
Tip: 此內(nèi)存區(qū)域是唯一一個在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemeoryError情況的區(qū)域痪寻。
2.2 Java虛擬機(jī)棧
Java虛擬機(jī)棧:每個Java方法在執(zhí)行的同時都會創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表螺句、操作數(shù)棧、動態(tài)鏈接橡类、方法出口等信息蛇尚。每個方法從調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧到出棧的過程顾画。
經(jīng)常有人把Java內(nèi)存區(qū)分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack)取劫,這種分法比較粗糙。實際上研侣,“椘仔埃”實際就是虛擬機(jī)棧,或者說虛擬機(jī)棧中的局部變量表部分庶诡。
局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean惦银、byte、char末誓、short扯俱、int、float喇澡、long迅栅、double)、對象引用(一個指向?qū)ο笃鹗嫉刂返囊弥羔樍糜模蛞粋€代表對象的句柄或其他與此對象相關(guān)的位置)和returnAddress類型(指向一條字節(jié)碼指令的地址)库继。
Tip:在Java虛擬機(jī)規(guī)范中箩艺,對這個區(qū)域規(guī)定了兩種異常狀況:
如果線程請求的棧深度大于虛擬機(jī)允許的深度窜醉,則拋出StackOverflowError異常;
如果虛擬機(jī)椧兆唬可動態(tài)擴(kuò)展榨惰,如果擴(kuò)展時無法申請到足夠的內(nèi)存,則拋出OutOfMemoryError異常静汤。
2.3 本地方法棧
本地方法棧:與虛擬機(jī)棧所發(fā)揮的作用非常相似琅催,區(qū)別在于 虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法服務(wù)居凶,而本地方法棧則為虛擬機(jī)使用到的Native方法服務(wù)。與虛擬機(jī)棧一樣藤抡,本地方法棧區(qū)域也會拋出StackOverflowError和OutOfMemoryError異常侠碧。
2.4 Java堆
Java堆:被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動時創(chuàng)建缠黍。此內(nèi)存區(qū)域的唯一目的就是存放對象實例弄兜,幾乎所有對象實例都是在這里分配內(nèi)存。
Java堆是垃圾收集器管理的主要區(qū)域瓷式,因此也成為GC堆替饿。
從內(nèi)存回收角度來看,Java堆可以細(xì)分為:新生代和老年代贸典;再細(xì)致些有Eden空間视卢、From Survivor空間、To Survivor空間等廊驼。
從內(nèi)存分配角度來看据过,線程共享的Java堆可能劃分出多個線程私有的分配緩沖區(qū)(TLAB)。
Tip:如果在堆中沒有內(nèi)存完成實例分配妒挎,并且堆也無法再擴(kuò)展時蝶俱,將會拋出OutOfMemoryError異常〖⒙可以通過-Xmx和-Xms來控制堆大小榨呆。
2.5 方法區(qū)
方法區(qū):線程共享的內(nèi)存區(qū)域,用于存儲已被虛擬機(jī)加載的類信息庸队、常量积蜻、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)彻消。
這個區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收和對類型的卸載竿拆,一般來說,這個區(qū)域的回收成績比較難以令人滿意宾尚。
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定丙笋,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時,將會拋出OutOfMemoryError異常煌贴。
2.6 運行時常量池
運行時常量池:是方法區(qū)的一部分御板。Class文件中除了有類的版本、字段牛郑、方法怠肋、接口等描述信息外,還有一項信息是常量池淹朋,用于存放編譯期生成的各種字面量和符號引用笙各,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運行時常量池中存放钉答。
2.7 直接內(nèi)存
直接內(nèi)存:并不是虛擬機(jī)運行時數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域杈抢。但是這部分內(nèi)存也被頻繁地使用数尿,也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。
在JDK 1.4 中新加入了NIO類惶楼,引入了一種基于通道(Channel)與緩存區(qū)(Buffer)的I/O方式砌创,可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作鲫懒,這樣避免了在Java堆和Native堆中來回復(fù)制數(shù)據(jù)嫩实。
Tip:若忽略直接內(nèi)存的限制,導(dǎo)致各個內(nèi)存區(qū)域總和大于物理內(nèi)存限制窥岩,會導(dǎo)致動態(tài)擴(kuò)展時出現(xiàn)OutOfMemoryError異常甲献。
3.虛擬機(jī)對象探秘
3.1 對象的創(chuàng)建
在語言層面上,創(chuàng)建對象通常僅僅是一個new操作而已颂翼;但是在虛擬機(jī)中晃洒,對象的創(chuàng)建是一個復(fù)雜的過程。
A. 虛擬機(jī)遇到一條new指令時朦乏,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用球及,并且檢查這個符號引用代表的類是否已被加載、解析和初始化過呻疹。如果沒有吃引,則先執(zhí)行相應(yīng)的類加載過程。
B. 在類加載檢查通過后刽锤,接下來虛擬機(jī)將為新生對象分配內(nèi)存镊尺。
在使用Serial、ParNew等帶Compact過程的收集器時并思,系統(tǒng)采用的分配算法是 指針碰撞庐氮;
在使用CMS這種基于Mark-Sweep算法的收集器時,通常采用 空閑列表宋彼。C. 內(nèi)存分配完成后弄砍,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值。
D. 虛擬機(jī)要對對象進(jìn)行必要的設(shè)值输涕,例如這個對象是哪個類的實例音婶、如何才能找到類的元數(shù)據(jù)信息、對象的哈希碼占贫、對象的GC分代年齡等信息桃熄。這些信息存放在對象的對象頭之中先口。
E. 執(zhí)行new指令之后會接著執(zhí)行<init>方法型奥,把對象按照程序員的意愿進(jìn)行初始化瞳收,這樣一個真正可用的對象才算完全產(chǎn)生出來。
3.2 對象的內(nèi)存布局
在HotSpot虛擬機(jī)中厢汹,對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭(Header)螟深、實例數(shù)據(jù)(Instance Data)和對齊填充(Padding)。
A. 對象頭:包含兩部分信息烫葬,第一部分用于存儲對象自身的運行時數(shù)據(jù)界弧,如哈希碼、GC分代年齡搭综、鎖狀態(tài)標(biāo)志垢箕、線程持有的鎖、偏移線程ID兑巾、偏向時間戳等条获;另一部分是類型指針,即對象指向它的類元數(shù)據(jù)的指針蒋歌,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實例帅掘。
B. 實例數(shù)據(jù):對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內(nèi)容堂油。這部分的存儲順序會收到虛擬機(jī)分配策略參數(shù)和字段在Java源代碼中定義順序的影響修档。HotSpot虛擬機(jī)默認(rèn)的分配策略為longs/doubles、ints府框、shorts/chars吱窝、bytes/booleans、oops(Ordinary Object Pointers),相同寬度的字段總是被分配到一起迫靖。
C. 對齊填充:并不是必然存在癣诱。對象的大小必須是8字節(jié)的整數(shù)倍,當(dāng)對象實例數(shù)據(jù)部分沒有對齊時袜香,就需要通過對齊填充來補(bǔ)全撕予。
3.3 對象的訪問定位
Java程序需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對象
目前主流的訪問方式有使用句柄和直接指針兩種:
- A. 若使用句柄訪問,Java堆中將會劃分出一塊內(nèi)存來作為句柄池蜈首,reference中存儲的就是對象的句柄地址实抡,而句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息。
- B. 若使用直接指針訪問欢策,Java堆對象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息吆寨,而reference中存儲的直接就是對象地址。