JVM在執行Jva程序時候會把它所管理的內存劃分爲若干個不同的數據區域。這些區域有各自的用途,以及創建和銷燬的時間。下圖表示運行時數據區的基本劃分,圖片來自其他微博。
1 程序計數器
程序計數器是一塊比較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。在虛擬機概念模型裏,通過改變這個計數器的值還選取下一條需要執行的字節碼指令。分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
程序計數器是線程私有的,每個線程都有自己的程序計數器並且不相互干擾。
- 當線程在執行一個Java方法的時候,程序計數器記錄的是當前正在執行的虛擬機字節碼指定地址
- 當正在執行的是Native方法的時候,計數器的值爲空。
程序計數器是Java虛擬機規範中唯一沒有規定OutOfMemoryError情況的區域。
2 Java虛擬機棧
與程序計數器一樣,虛擬機棧也是線程私有的,它的生命週期與線程是一樣的,虛擬機棧是描述Java執行的內存模型:每當啓動一個新線程的時候,java虛擬機都會爲它分配一個java棧,每個方法執行的同時都會創建一個棧幀,它是Java虛擬機棧的棧元素結構。每個方法調用都是方法棧幀從入棧到出棧的過程。
虛擬機棧定義了兩種異常
- StackOverflowError異常:如果線程請求的棧深度大於允許的深度,將會拋出
- OutOfMemoryError異常:如果虛擬機棧可能動態拓展,如果拓展時無法申請到足夠的內存,將會拋出。
2.1 棧幀
棧幀是用於支持虛擬機進行方法調用和方法執行的數據結構。棧幀儲存了方法的局部變量表、操作數棧、動態鏈接和方法返回地址等信息。在編譯程序時,棧幀需要多大的局部變量表、多深的操作數棧就已經完全確定了,並且寫入方法表的Code屬性中。因此一個棧幀需要的大多不會受到運行期變量數據的影響,而僅僅取決於具體的虛擬機實現。
一個線程中的方法調用鏈可能會很長,很多方法都同時處於執行狀態。對於執行引擎來說,在活動線中,只有位於棧頂的棧幀是有效的,稱爲當前棧幀,與這個棧幀相關聯的稱爲當前方法。他們的關係如下:
2.2 局部變量表
局部變量表是一組變量值儲存空間,用於存放方法參數和方法內部定義的局部變量。(所以我們所說的局部變量放在棧上就是從這來的)。在Java編譯成Class文件時,這個方法的Code屬性的max_locals數據項中確定了該方法所需要分配的局部變量表的最大容量。
在局部變量表中存放了編譯期可知的各種基本數據類型、對象引用(reference類型,它不同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和returnAddress類型。其中64爲長度的long和double類型佔用兩個空間(slot),其餘類型佔用1個slot
虛擬機通過索引定位的方式使用局部變量表,索引範圍從0開始至最大的slot數。如果訪問32位變量,索引n就代表第n個slot,如果爲64位,則n和n+1兩個slot表示改變量。對於兩個相鄰的slot表示一個數據的時候,不允許單獨訪問其中一個。
索引0號位默認用於保存this指針,其餘參數按照參數表順序排列。
2.3 操作數棧
操作數棧是一個後進先去的棧,其最大深度在編譯時已經確定。當一個方法開始執行的時候,棧爲空。在方法執行過程中,字節碼指令會進行入棧與出棧的操作。例如:進行兩個數相加的操作時,整數加法字節碼iadd在運行的時候棧頂的兩個元素取出,然後相加,把相加的結果入棧。對於兩個棧幀,爲了進行優化,他們的操作棧可能存在重疊的區域,以實現數據共享。
2.4動態鏈接
每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法在調用過程中的動態鏈接。在Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用作爲參數。這些符號引用一部分在類加載或第一次使用的時候轉化爲直接引用,這種稱爲靜態解析,另一部分在每一次運行期間轉換爲直接引用,這部分稱爲動態鏈接。
2.5方法返回地址(方法出口)
當一個方法開始執行後,只有兩個方式可以退出。一種是遇到任意一個返回字節碼指令,這種退出稱爲正常完成出口;另一種是異常,只要方法的異常表中沒有匹配的處理器,將會導致方法退出,這種退出稱爲異常完成出口。無論採用何種方式退出,都會返回到方法被調用的位置,程序才能繼續,方法返回時可能需要在棧幀中保存一些信息,用來恢復它的上層方法的執行狀態。方法退出的過程實際上就等於把當前棧幀出棧。
2.6附加信息
虛擬機規範允許虛擬機可以在棧幀存放額外的信息,例如與調試相關的信息。
3 本地方法棧
本地方法棧幀與虛擬機棧所發揮的作用是相似的,只不過本地方法棧爲Native方法服務。與虛擬機棧一樣,本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。
4 Java堆
Java堆是Java虛擬機所管理的內存中最大的一塊,它是被所有線程共享的一塊區域。虛擬機啓動創建時,此區域唯一的作用就是存放對象實例。幾乎所有的對象實例和數組都要在堆上分配。
Java堆是垃圾收集器管理的主要區域,因此很多時候也稱做“GC堆”。
從內存回收的角度,現在收集器都用分代收集算法,所以Java堆中還可以細分爲:新生代和老年代,在細緻一點有Eden空間,From Survivor空間、To Suivivor空間等。
從內存分配的角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(TLAB)。
Java堆可以處於物理上不連續的內存空間中,只要邏輯上連續的即可,就像我們的磁盤空間一樣。在實現時,既可以實現成固定大小的,也可以是可擴展的。如果在堆中沒有內存完成實例分配,並且堆也無法在擴展時,將會拋出OutOfMemoryError異常。
5 方法區
方法區和Java堆一樣,是各個想成共享的內存區域,它用於儲存已被虛擬機記載的類信息、常量、靜態變量、及時編譯後的代碼等數據。它有一個別名,叫“非堆”,相對於前面的新生代、老年代來說,它可以稱爲“永久代”。因爲相對而言,垃圾回收在這個區域是比較少出現的,但也並非是數據進入這個區域就永久存在了,這個區域的內存回收主要針對常量池的回收和對類型的卸載。
5.1 運行時常量池
運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息以外,還有一項信息是常量池,用於存在編譯期聲場的各種字面量和符號引用,這部分在類加載後進入方法區的運行時常量池中存放。
運行時常量池相對於Class文件常量池的另一個特性是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,運行期也可能將新的常量放入池中。例如String類的intern()方法。
根據Java虛擬機規範中的描述,每個類和接口被加載到虛擬機後,都會在方法區中創建出對應的運行時常量池。JVM爲每個類都維護一個常量池
6 直接內存
直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存。它是一種堆外內存
在JDK1.4中引用了NIO(New Input/Output)類,引入了一種基於通道和緩衝區的IO方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣可以提高應用性能,避免在Java堆和Native堆中來回複製數據。