JVM(七):JVM內存結構
在前幾節(jié)的文章我們多次講到 Class 對象需要分配入 JVM 內存越平,并在 JVM 內存中執(zhí)行 Java 代碼频蛔,完成對象內存的分配、執(zhí)行秦叛、回收等操作晦溪,因此,如今讓我們來走入 JVM挣跋,看看 JVM 中的內存結構是如何構造的三圆,下面就讓我們一探究竟吧。
內存劃分
在本小節(jié)中避咆,我們以《Java 虛擬機規(guī)范》中的要求舟肉,并以當前主流虛擬機 Hotspot VM 為例,詳細講述內存區(qū)域中各個模塊的劃分查库,了解其各自的用途以及其為何如何劃分等路媚。
首先讓我們來看一下 Java 虛擬機內存的劃分方式。
JVM 將內存劃分為 5個部分樊销,分別為線程共享的 堆 和 方法區(qū)整慎,以及線程私有的 程序計數器,虛擬機棧 和 本地方法棧围苫,下面就讓我們針對這 5個區(qū)域進行學習院领,探究其存儲數據,生命周期和功能够吩。
程序計數器
是一塊較小的內存區(qū)域比然,可以看做是當前線程執(zhí)行的字節(jié)碼的行號指示器。在虛擬機概念模型里周循,字節(jié)碼解析器就是通過改變這個計數器的值來選取下一條需要執(zhí)行的字節(jié)碼强法,因此其在分支,循環(huán)湾笛,跳轉饮怯,異常跳轉,線程恢復等功能上都有著大作用嚎研。
PS:如果執(zhí)行的是本地方法蓖墅,那么這個計數器的值則為空。
虛擬機棧
虛擬機棧也是線程私有的临扮,其內描述的是 Java 方法執(zhí)行的內存模型论矾,即在每個執(zhí)行同時創(chuàng)建一個棧幀,棧幀內存儲局部變量表杆勇,操作數棧贪壳,動態(tài)鏈接,方法出口等信息蚜退。每一個方法從開始到結束就對應著一個棧幀從入棧到出棧的過程闰靴。同時只有位于棧頂的棧幀才是有效的彪笼,與其關聯的方法稱為當前方法,執(zhí)行引擎的所有字節(jié)碼指令都只針對當前棧幀進行操作蚂且。
局部變量表
用于存放方法參數和方法內部定義的局部變量配猫,其在 Java 程序被編譯為 Class 文件后,就已經確定了所需的最大容量杏死。
其容量以變量槽(slot)為最小單位章姓。因此在使用過程中是通過索引定位來使用局部變量表的,索引范圍為 0~~slot 最大值识埋。其中如果執(zhí)行的是非 static 方法,那么0則默認為 方法所屬對象實例引用零渐,對應 Java 關鍵字的 this窒舟。其余參數按照順序對應 1之后的槽位。
操作數棧
操作棧是一個后入先出的棧诵盼,其最大深度在編譯時也已經確定惠豺。其對應著方法執(zhí)行過程中,各種字節(jié)碼指令往操作數棧寫入和提取內容风宁,也就是所謂的 入棧/出棧 操作洁墙。
也正是操作數棧的存在,因此Java執(zhí)行引擎也被稱為 基于棧的執(zhí)行引擎戒财,與基于 基于寄存器的執(zhí)行引擎 形成對比热监。
Java采取「基于棧的執(zhí)行引擎」考慮到兩點:
- Java是一門跨平臺的語言,而不同機器的寄存器實現是不同的饮寞,有多又少孝扛,不利于統一;
- 為了使 class 文件更加的緊湊幽崩,這樣設計可以使得大多數指令對齊苦始,并且操作碼只占一個字節(jié)大小,減少數據量慌申。
動態(tài)連接
指向運行時常量池中該棧幀所屬方法的引用陌选,通過這個引用可以完成動態(tài)調用。
關于方法調用過程中的引用詳細解析過程蹄溉,在日后的「方法調用」中咨油,再具體描述。
返回地址
一個方法在執(zhí)行完成后都需要返回到方法被調用的位置柒爵,讓程序繼續(xù)執(zhí)行臼勉。
在方法正常執(zhí)行完成退出后,調用者的程序計數器的值就可以作為返回地址存在棧幀中餐弱,而在方法異常退出后宴霸,返回地址則是通過異常處理器表來確定了囱晴。
附加信息
附加信息不是虛擬機規(guī)范中必須要求有的,但其允許實現者可以增加一些特殊信息到棧幀中瓢谢,例如與調試相關的信息畸写,這部分信息取決于具體的虛擬機實現,在這里不再贅述氓扛。
本地方法棧
本地方法棧和虛擬機棧的作用類似枯芬,區(qū)別僅僅是虛擬機棧為虛擬機執(zhí)行的 Java 方法服務,而本地方法棧則是為 Native 方法服務采郎。其具體實現由虛擬機自行規(guī)定千所。
堆
Java 堆是線程共享的。在一般情況下蒜埋,堆可以說是 Java 內存中最大的內存區(qū)域淫痰。其存放了對象實例,幾乎所有的對象實例在這里存儲整份。(這里說是幾乎待错,是因為 JIT優(yōu)化的存在,可能會有對象不在堆上分配烈评,而在棧上進行分配)火俄。
由于目前考慮到垃圾回收算法大部分都是分代算法,因此堆又可以細分為以下幾塊:
但從其內存本質來看讲冠,其并沒有詳細的區(qū)別瓜客,都是用來存儲對象實例的,這種劃分方式是從內存回收的角度來闡述的竿开,因此具體存放邏輯放在「內存回收」中再詳細闡述忆家。
方法區(qū)
方法區(qū)也是線程共享的。其中存放的是被虛擬機加載的類信息德迹,常量芽卿,靜態(tài)變量,即時編譯器編譯后的代碼等數據胳搞。在HotSpot JDK7 以前的具體實現中卸例,這部分被稱為永久代,和堆一起 JVM 管理肌毅。但在JDK8之后筷转,這部分已經用 元數據(meta space) 來替代了。此外像字符串常量池也被從這一模塊移除悬而,轉而用堆來實現呜舒。
常量池
JDK7 之后將以前放在方法區(qū)的常量池放在堆中進行實現,例如 String 的 intern()
方法笨奠,在 JDK8 之后改為如果存在堆中的引用袭蝗,則直接返回堆中引用唤殴,而并不會重新創(chuàng)建對象。
下面讓我們來看一下這段代碼在 JDK8 下的結果是什么到腥。
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);
String s3 = new String("1") + new String("1");
s3.intern();
String s4 = "11";
System.out.println(s3 == s4);
該代碼在JDK8下輸出結果為:
false
true
下面就讓我們用下圖來分析一下是為什么:
String s = new String("1")
這句生成了兩個對象朵逝,一個是對象 obj(1),另一個在 String pool 中乡范,是 "1"配名,s 則是指向對象。s.intern()
因為 "1" 在String pool中已經存在晋辆,所以直接返回渠脉,String s2 = "1"
,則是直接返回String pool中的引用給s2瓶佳,最后比較的是兩個指向不同地方的引用芋膘,因此結果不同。
String s3 = new String("1") + new String("1")
生成了兩個對象涩哟,一個是對象obj(11),一個是String pool 中的 "1"盼玄,s3.intern()
判斷當 堆中存在對象的時候贴彼,則在字符串常量池中保存該對象的引用,然后返回該對象的引用值埃儿,String s4 = "11"
則讓 s4 指向 String pool 中的值器仗,而 該引用的值就是obj(11)的引用,在最后 System.out.println(s3 == s4)
判斷相等的時候童番,兩個引用其實指向的是同一個值精钮,因此返回相等。
直接內存
Direct Memory 不屬于 JVM 所管的內存區(qū)域剃斧,其受到機器總內存的影響轨香。在具體使用中采用一個在 Java 堆中的DirectByteBuffer
對象作為這塊內存的引用進行操作。
總結
在本文中我們學習了 JVM 在其內部是如何劃分區(qū)域進行功能協作的幼东。了解了其內部將 JVM 劃分哪幾個模塊臂容,每個模塊各自又都有神馬作用,其中存儲了什么數據根蟹,每個模塊的不同特性等脓杉。
在下文中,我們將講述對象在堆中的存儲简逮,使用方式球散,了解的Java的 對象模型。
文章在公眾號「iceWang」第一手更新散庶,有興趣的朋友可以關注公眾號蕉堰,第一時間看到筆者分享的各項知識點凌净,謝謝!筆芯嘁灯!
本系列文章主要借鑒自《深入分析 JavaWeb 技術內幕》和《深入理解 Java 虛擬機-JVM 高級特性與最佳實踐》泻蚊。