今天是JVM的生日进统,那么我們就從今天開始聊聊JVM相關(guān)的內(nèi)容吧。
JVM虛擬機是JAVA的基礎(chǔ),正是它的存在使得JAVA擺脫了硬件平臺的束縛策州,實現(xiàn)了“一次編譯,到處運行”的理想宫仗。盡管我們寫代碼的時候可能不會直接接觸JVM虛擬機够挂,但是了解其原理依舊是非常有必要的。那么藕夫,今天我們就來聊聊JVM虛擬機中的內(nèi)存機制孽糖。
本文的要點如下:
- 概述
- JVM虛擬機的內(nèi)存區(qū)域劃分
- 程序計數(shù)器
- Java虛擬機棧
- 本地方法棧
- Java堆
- 方法區(qū)
- 運行時常量池
- HotSpot虛擬機對象內(nèi)存
- 對象的創(chuàng)建
- 對象的內(nèi)存布局
- 對象的訪問定位
概述
“Java和C++之間有一堵由內(nèi)存動態(tài)分配和垃圾收集技術(shù)所圍成的“高墻”,墻外面的人想進去毅贮,墻里面的人卻想出來办悟。” ——《深入理解Java虛擬機》
我們知道滩褥,Java語言的一個巨大優(yōu)勢就是虛擬機存在著自動內(nèi)存管理機制病蛉,程序員不再需要為每一個new操作去寫配對的delete/free代碼,也不容易出現(xiàn)內(nèi)存泄露和內(nèi)存溢出問題瑰煎。這就是C铺然、C++程序員天天喊著Java比C++簡單的原因。但是其實真的是這樣么酒甸?正因為Java程序員把內(nèi)存的控制權(quán)交給了Java虛擬機魄健,一旦出現(xiàn)了內(nèi)存泄露、內(nèi)存溢出的問題插勤,排查錯誤是很困難的沽瘦。因此,我們就需要了解Java虛擬機是怎樣使用內(nèi)存的饮六。
JVM虛擬機的內(nèi)存區(qū)域劃分
Java虛擬機管理的內(nèi)存包括以下幾個區(qū)域:
相信上面的圖大家應(yīng)該都見過其垄,那么我們就來看看每一部分具體是做什么的。
1.程序計數(shù)器(Program Counter Register)
程序計數(shù)器是一塊較小的內(nèi)存空間卤橄,可以看作是當(dāng)前程序所執(zhí)行的字節(jié)碼的行號指示器绿满。虛擬機中的字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的指令,循環(huán)窟扑、分支喇颁、跳轉(zhuǎn)漏健、異常處理、線程恢復(fù)等基礎(chǔ)功能都要依靠這個計數(shù)器來實現(xiàn)橘霎。
我們知道蔫浆,Java虛擬機的多線程是通過線程輪流切換并分配處理器時間片進行執(zhí)行的方式來實現(xiàn)的。那么為了保證線程切換后能恢復(fù)到正確的執(zhí)行位置姐叁,每個線程都要有一個獨立的程序計數(shù)器瓦盛,程序計數(shù)器之間互不干擾,即程序計數(shù)器是“線程私有”的外潜。
- 如果線程正在執(zhí)行的是一個Java方法原环,那么計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址;
- 如果線程正在執(zhí)行的是一個Native方法处窥,那么計數(shù)器的值則為空嘱吗。
在Java虛擬機規(guī)范中,是唯一一個沒有規(guī)定任何OutOfMemoryError情況的區(qū)域滔驾。
2.Java虛擬機棧(Java Virtual Machine Stacks)
- Java虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型谒麦,每個方法在執(zhí)行時都會創(chuàng)建一個棧幀用于儲存局部變量表、操作數(shù)棧哆致、動態(tài)鏈接绕德、方法出口等信息。每一個方法從調(diào)用到執(zhí)行結(jié)束的過程沽瞭,就對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程迁匠。
- 虛擬機棧是線程私有內(nèi)存,生命周期與線程相同驹溃。
- 局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型城丧、對象引用類型和returnAddress類型,它所需的內(nèi)存空間在編譯期間完成分配豌鹤。
- 在Java虛擬機規(guī)范中亡哄,對這個區(qū)域規(guī)定了兩種異常狀況:
- 如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常布疙;
- 如果虛擬機椢霉撸可動態(tài)擴展且擴展時無法申請到足夠的內(nèi)存,將拋出OutOfMemoryError異常灵临。
3.本地方法棧(Native Method Stack)
聽名字就知道這個棧和Java虛擬機棧類似截型,也是線程私有,只不過是服務(wù)于Native方法儒溉。
- 在虛擬機規(guī)范中宦焦,對這個區(qū)域無強制規(guī)定,由具體的虛擬機自由實現(xiàn)。
- 同樣波闹,本地方法棧也會拋出StackOverflowError異常h和OutOfMemoryError異常酝豪。
4.Java堆(Java Heap)
Java堆是虛擬機所管理的內(nèi)存中的最大的一塊,它被創(chuàng)建的唯一目的就是存放對象實例精堕,幾乎所有的對象實例都在這里分配內(nèi)存,Java堆可處于物理上不連續(xù)的內(nèi)存空間中孵淘,只要邏輯上是連續(xù)的即可。
- Java堆是垃圾收集器管理的主要區(qū)域歹篓,因此又叫做“GC堆”瘫证。
- Java堆是被所有線程共享的,在虛擬機啟動時就被創(chuàng)建出來滋捶。
- 線程共享的Java堆中痛悯,可能劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)重窟,但無論哪個區(qū)域,存儲的都仍然是對象實例惧财,進一步劃分的目的只是為了更好地回收內(nèi)存巡扇,或者更快地分配內(nèi)存。
- 在Java虛擬機規(guī)范中垮衷,如果在堆中沒有內(nèi)存完成實例分配厅翔,且堆也無法再擴展時,將會拋出OutOfMemoryError異常搀突。
5.方法區(qū) (Method Area)
方法區(qū)用于儲存已被虛擬機加載的類信息刀闷、常量、靜態(tài)變量仰迁、即時編譯器編譯后的代碼等數(shù)據(jù)甸昏,并且除了和Java堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或可擴展外,還可選擇不實現(xiàn)GC徐许。
- 與Java堆一樣施蜜,是各個線程共享的內(nèi)存區(qū)域。
- 很多人把方法區(qū)稱作“永久代”雌隅,但其實兩者不是等價的翻默,只是部分Java虛擬機用永久代來實現(xiàn)方法區(qū)而已。發(fā)布的JDK1.7的HotSpot中恰起,已經(jīng)把原本放在永久代的字符串常量池移出修械。它還有個別名叫做Non-Heap(非堆)。
- 數(shù)據(jù)并非進入“永久代”之后就可以永久存在了检盼,方法區(qū)的GC也是有必要的肯污,主要的目的是針對常量池的回收和類型卸載。
- 在Java虛擬機規(guī)范中,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時仇箱,將拋出OutOfMemoryError異常县恕。
6.運行時常量池(Runtime Constant Pool)
Class文件中除了有類的版本、字段剂桥、方法忠烛、接口等描述信息外,還有一項信息是常量池(Constant Pool Table)权逗,用于存放編譯期生成的各種字面量和符號引用美尸,這部分內(nèi)容將在類加載后進入方法區(qū)的運行時常量池中存放。
- 運行時常量池的一個重要特征是具備動態(tài)性斟薇,體現(xiàn)在并非只有預(yù)置入Class文件中常量池的內(nèi)容才能進入方法區(qū)運行時常量池师坎,運行期間也可能將新的常量放入池中(String的intern()方法)。
- 運行時常量池是方法區(qū)的一部分堪滨,會受到方法區(qū)內(nèi)存的限制胯陋。
- 在Java虛擬機規(guī)范中,當(dāng)常量池?zé)o法再申請到內(nèi)存時會拋出OutOfMemoryError異常袱箱。
HotSpot虛擬機對象內(nèi)存
對象的創(chuàng)建
在Java語言層面遏乔,創(chuàng)建對象通常僅僅是一個new關(guān)鍵字而已,那么虛擬機中對象的創(chuàng)建又是一個什么樣的過程呢发笔?
1. 類加載檢查:
當(dāng)虛擬機遇到一條new指令的時候盟萨,首先會檢查這個指令是否能在常量池中定位到一個類的符號引用,并且檢查這個符號代表的類是否已經(jīng)被加載了讨、解析和初始化過了捻激。如果沒有會先執(zhí)行類的加載過程。
2. 分配內(nèi)存:
類加載檢查通過后前计,虛擬機會為對象分配內(nèi)存胞谭。對象所需的內(nèi)存的大小在類加載后便可確定。根據(jù)內(nèi)存是否規(guī)整可以分為兩種內(nèi)存分配方式:
- 若規(guī)整残炮,采用“指針碰撞”分配方式:將用過和空閑的內(nèi)存放在兩邊韭赘,中間以一個指針作為分界指示器。當(dāng)分配內(nèi)存時势就,就把指針向空閑一邊挪動與對象大小相等的距離即可泉瞻。
- 若非規(guī)整,則采用“空閑列表”分配方式:維護一個記錄可用內(nèi)存塊的列表苞冯。當(dāng)分配內(nèi)存時袖牙,就從列表中找到一塊足夠大的空間劃分給對象實例并更新記錄。
另外舅锄,為了保證內(nèi)存分配是線程安全的鞭达,有如下兩種方案:
- 對內(nèi)存分配的動作進行同步處理;
- 每個線程在Java堆中預(yù)先分配一塊內(nèi)存(本地線程分配緩沖TLAB),在本線程的TLAB上進行分配畴蹭,當(dāng)TLAB用完需要分配新的TLAB時再同步鎖定坦仍。
3. 設(shè)置對象頭:
將對象的所屬類、找到類的元數(shù)據(jù)信息的方式叨襟、對象的哈希碼繁扎、對象的GC分代年齡等信息存放在對象的對象頭(Object Header)中。
4.執(zhí)行init方法:
盡管經(jīng)過1糊闽、2梳玫、3步驟對象在虛擬機中已經(jīng)產(chǎn)生了,但此時所有的字段都還為零右犹,還需要執(zhí)行<init>方法進行初始化提澎,才能成為真正可用的對象。
對象的內(nèi)存布局
- 對象頭(Object Header):
包括Mark Word和類型指針兩部分:
- Mark Word:用于存儲對象自身的運行時數(shù)據(jù)念链,如哈希碼盼忌、GC分代年齡、鎖狀態(tài)標(biāo)志钓账、線程持有的鎖碴犬、偏向線程ID、偏向時間戳等梆暮。根據(jù)虛擬機的位數(shù)不同長度為32bit或64bit,會根據(jù)對象狀態(tài)復(fù)用空間绍昂。
- 類型指針:用于確定這個對象的所屬類啦粹。
另外,如果對象是數(shù)組窘游,還會有一塊記錄數(shù)組長度的數(shù)據(jù)唠椭。- 實例數(shù)據(jù)(Instance Data):存儲真正的有效信息,是程序代碼中定義的各種類型的字段內(nèi)容忍饰。存儲順序會受虛擬機分配策略參數(shù)和字段在Java源碼中定義順序這兩個因素影響贪嫂。
- 對齊填充(Padding):占位符,幫助補全未對齊的對象實例數(shù)據(jù)部分(保證是8字節(jié)的倍數(shù))艾蓝,非必需力崇。
對象的訪問定位
- 通過句柄訪問對象:在Java堆中劃分出一塊內(nèi)存來作為句柄池,reference存儲的是對象的句柄地址赢织,在句柄中包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息亮靴。優(yōu)勢:reference中存儲的是穩(wěn)定的句柄地址,在對象被移動時只會改變句柄中的實例數(shù)據(jù)指針于置,而reference本身不需要修改茧吊。
- 通過直接指針訪問對象:在Java堆對象的布局中考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,reference存儲的直接就是對象地址。優(yōu)勢:速度更快搓侄,節(jié)省了一次指針定位的時間開銷瞄桨。