JVM虛擬機器記憶體模型
java虛擬機器執行時記憶體主要包括:程式計數器、虛擬機器棧、本地方法棧、java堆、方法區(包含執行時方法區)以及直接記憶體幾個部分。
下面分別介紹幾個部分:如下圖所示(圖片源自網路)
1. 程式計數器(Program Counter Register) ,很小的一塊記憶體空間,當前執行緒執行的位元組碼的行號指示器,執行命令的指令指標。
位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、
執行緒恢復等基礎功能都需要依賴這個計數器來完成。由於Java 虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現
的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)只會執行一條執行緒中的指令。因此,為了執行緒切換後能
恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間的計數器互不影響,獨立儲存,這類記憶體區域為“
如果執行緒正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Natvie 方法,這個計數器值則為空(Undefined)。此記憶體區域是唯一一個在Java 虛擬機器規範中沒有規定任何OutOfMemoryError 情況的區域。
2. 虛擬機器棧(VM stack),是java方法執行的記憶體模型,也是執行緒私有的。
每個方法被執行的時候都會建立一個棧幀(Stack Frame )用於儲存區域性變量表、操作棧、動態連結、方法出口等資訊。每一個方法被呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。經常有人把Java
機實現,它可能是一個指向物件起始地址的引用指標,也可能指向一個代表物件的控制代碼或者其他與此物件相關的位置)和 returnAddress 型別(指向了一條位元組碼指令的地址)。其中64 位長度的long 和double 型別的資料會佔用2
在Java 虛擬機器規範中,對這個區域規定了兩種異常狀況:如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError 異常;
如果虛擬機器棧可以動態擴充套件(當前大部分的Java 虛擬機器都可動態擴充套件,只不過Java 虛擬機器規範中也允許固定長度的虛擬機器棧),當擴充套件時無法申請到足夠的記憶體時會丟擲OutOfMemoryError 異常。
3. 本地方法棧(Native method stack)虛擬機器棧為虛擬機器執行Java 方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的Native 方法服務。虛擬機器規範中對本地方法棧中的方法使用的語
言、使用方式與資料結構並沒有強制規定,因此具體的虛擬機器可以自由實現它。甚至有的虛擬機器(譬如Sun HotSpot 虛擬機器)直接就把本地方法棧和虛擬機器棧合二為一。與虛擬機器棧一樣,本地方法棧區域也會丟擲StackOverflowError 和OutOfMemoryError異常。
4. 堆(Heap),Java 虛擬機器所管理的記憶體中最大的一塊,所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。這一點在Java 虛擬機器規範中的描述是:所有的物件例項以及陣列都要在堆上分配,但是隨著技術發展現在物件分配在堆上也漸漸變得不是那麼“絕對”了。Java 堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC 堆”。如果從記憶體回收的角度看,由於現在收集器基本都是採用的分代收集演算法,所以Java 堆中還可以細分為:新生代和老年代;再細緻一點的有Eden 空間、From Survivor 空間、To Survivor 空間等。如果從記憶體分配的角度看,執行緒共享的Java 堆中可能劃分出多個執行緒私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。
根據Java 虛擬機器規範的規定,Java 堆可以處於物理上不連續的記憶體空間中,只要邏輯上是連續的即可。在實現時,既可以實現成固定大小的,也可以是可擴充套件的,不過當前主流的虛擬機器都是按照可擴充套件來實現的(通過-Xmx和-Xms 控制)。如果在堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,將會丟擲OutOfMemoryError 異常。
5. 方法區(Method Area),與Java 堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。很多人願意把方法區稱為“永久代”(Permanent Generation),本質上兩者並不等價,僅僅是因為HotSpot 虛擬機器的設計團隊選擇把GC 分代收集擴充套件至方法區,或者說使用永久代來實現方法區而已。對於其他虛擬機器(如BEA JRockit、IBM J9 等)來說是不存在永久代的概念的。即使是HotSpot 虛擬機器本身,根據官方釋出的路線圖資訊,現在也有放棄永久代並“搬家”至Native Memory 來實現方法區的規劃了。Java 虛擬機器規範對這個區域的限制非常寬鬆,除了和Java 堆一樣不需要連續的內
存和可以選擇固定大小或者可擴充套件外,還可以選擇不實現垃圾收集。相對而言,垃圾收集行為在這個區域是比較少出現的,但並非資料進入了方法區就如永久代的名字一樣“永久”存在了。這個區域的記憶體回收目標主要是針對常量池的回收和對型別的解除安裝,一般來說這個區域的回收“成績”比較難以令人滿意,尤其是型別的解除安裝,條件相當苛刻,但是這部分割槽域的回收確實是有必要的。Java 虛擬機器規範的規定,當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError 異常。
執行時常量池(Runtime Constant Pool)是方法區的一部分。Class 檔案中除了有類的版本、欄位、方法、介面等描述等資訊外,還有一項資訊是常量池(Constant PoolTable),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。Java 虛擬機器對Class 檔案的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個位元組用於儲存哪種資料都必須符合規範上的要求,這樣才會被虛擬機器認可、裝載和執行。但對於執行時常量池,Java 虛擬機器規範沒有做任何細節的要求,不同的提供商實現的虛擬機器可以按照自己的需要來實現這個記憶體區域。不過,一般來說,除了儲存Class 檔案中描述的符號引用外,還會把翻譯出來的直接引用也儲存在執行時常量池中。執行時常量池相對於Class 檔案常量池的另外一個重要特徵是具備動態性,Java 語言並不要求常量一定只能在編譯期產生,也就是並非預置入Class 檔案中常量池的內容才能進入方法區執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的便是String 類的intern() 方法。既然執行時常量池是方法區的一部分,無法申請到記憶體時會丟擲OutOfMemoryError 異常。
6. 接記憶體存(Direct Memory)並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域,但是這部分記憶體也被頻繁地使用,而且也可能導致OutOfMemoryError 異常出現,所以我們放到這裡一起講解。在JDK 1.4 中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)
與緩衝區(Buffer)的I/O 方式,它可以使用Native 函式庫直接分配堆外記憶體,然後通過一個儲存在Java 堆裡面的DirectByteBuffer 物件作為這塊記憶體的引用進行
操作。這樣能在一些場景中顯著提高效能,因為避免了在Java 堆和Native 堆中來回複製資料。顯然,本機直接記憶體的分配不會受到Java 堆大小的限制,但是,既然是記憶體,則肯定還是會受到本機總記憶體(包括RAM 及SWAP 區或者分頁檔案)的大小及處理器定址空間的限制。伺服器管理員配置虛擬機器引數時,一般會根據實際記憶體設定-Xmx等引數資訊,但經常會忽略掉直接記憶體,使得各個記憶體區域的總和大於實體記憶體限制(包括物理上的和作業系統級的限制),從而導致動態擴充套件時出現OutOfMemoryError異常。
在Java 語言中,物件訪問是如何進行的?物件訪問在Java 語言中無處不在,是最普通的程式行為,但即使是最簡單的訪問,也會卻涉及Java 棧、Java 堆、方法區這三個最重要記憶體區域之間的關聯關係,如下面的這句程式碼:
Object obj = new Object();
假設這句程式碼出現在方法體中,那“Object obj”這部分的語義將會反映到Java 棧的本地變量表中,作為一個reference 型別資料出現。而“new Object()”這部分的語義
將會反映到Java 堆中,形成一塊儲存了Object 型別所有例項資料值(Instance Data,物件中各個例項欄位的資料)的結構化記憶體,根據具體型別以及虛擬機器實現的物件記憶體佈局(Object Memory Layout)的不同,這塊記憶體的長度是不固定的。另外,在Java 堆中還必須包含能查詢到此物件型別資料(如物件型別、父類、實現的介面、方法等)的地址資訊,這些型別資料則儲存在方法區中。由於reference 型別在Java 虛擬機器規範裡面只規定了一個指向物件的引用,並沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java 堆中的物件的具體位置,因此不同虛擬機器實現的物件訪問方式會有所不同,主流的訪問方式有兩種:使用控制代碼和直接
指標。如果使用控制代碼訪問方式,Java 堆中將會劃分出一塊記憶體來作為控制代碼池,reference中儲存的就是物件的控制代碼地址,而控制代碼中包含了物件例項資料和型別資料各自的具體地址資訊,如下圖所示。
如果使用直接指標訪問方式,Java 堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,reference 中直接儲存的就是物件地址,如下圖所示
這兩種物件的訪問方式各有優勢,使用控制代碼訪問方式的最大好處就是reference 中儲存的是穩定的控制代碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制代碼中的例項資料指標,而reference 本身不需要被修改。
=============================================================
下面部分內容是轉載的,講的挺詳細的。
記憶體區域
Java虛擬機器在執行Java程式的過程中會把他所管理的記憶體劃分為若干個不同的資料區域。Java虛擬機器規範將JVM所管理的記憶體分為以下幾個執行時資料區:程式計數器、Java虛擬機器棧、本地方法棧、Java堆、方法區。下面詳細闡述各資料區所儲存的資料型別。
程式計數器(Program Counter Register)
一塊較小的記憶體空間,它是當前執行緒所執行的位元組碼的行號指示器,位元組碼直譯器工作時通過改變該計數器的值來選擇下一條需要執行的位元組碼指令,分支、跳轉、迴圈等基礎功能都要依賴它來實現。每條執行緒都有一個獨立的的程式計數器,各執行緒間的計數器互不影響,因此該區域是執行緒私有的。
當執行緒在執行一個Java方法時,該計數器記錄的是正在執行的虛擬機器位元組碼指令的地址,當執行緒在執行的是Native方法(呼叫本地作業系統方法)時,該計數器的值為空。另外,該記憶體區域是唯一一個在Java虛擬機器規範中麼有規定任何OOM(記憶體溢位:OutOfMemoryError)情況的區域。
Java虛擬機器棧(Java Virtual Machine Stacks)
該區域也是執行緒私有的,它的生命週期也與執行緒相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法被執行的時候都會同時建立一個棧幀,棧它是用於支援續虛擬機器進行方法呼叫和方法執行的資料結構。對於執行引擎來講,活動執行緒中,只有棧頂的棧幀是有效的,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法,執行引擎所執行的所有位元組碼指令都只針對當前棧幀進行操作。棧幀用於儲存區域性變量表、運算元棧、動態連結、方法返回地址和一些額外的附加資訊。在編譯程式程式碼時,棧幀中需要多大的區域性變量表、多深的運算元棧都已經完全確定了,並且寫入了方法表的Code屬性之中。因此,一個棧幀需要分配多少記憶體,不會受到程式執行期變數資料的影響,而僅僅取決於具體的虛擬機器實現。
在Java虛擬機器規範中,對這個區域規定了兩種異常情況:
1、如果執行緒請求的棧深度大於虛擬機器所允許的深度,將丟擲StackOverflowError異常。
2、如果虛擬機器在動態擴充套件棧時無法申請到足夠的記憶體空間,則丟擲OutOfMemoryError異常。
這兩種情況存在著一些互相重疊的地方:當棧空間無法繼續分配時,到底是記憶體太小,還是已使用的棧空間太大,其本質上只是對同一件事情的兩種描述而已。在單執行緒的操作中,無論是由於棧幀太大,還是虛擬機器棧空間太小,當棧空間無法分配時,虛擬機器丟擲的都是StackOverflowError異常,而不會得到OutOfMemoryError異常。而在多執行緒環境下,則會丟擲OutOfMemoryError異常。
下面詳細說明棧幀中所存放的各部分資訊的作用和資料結構。
1、區域性變量表
區域性變量表是一組變數值儲存空間,用於存放方法引數和方法內部定義的區域性變數,其中存放的資料的型別是編譯期可知的各種基本資料型別、物件引用(reference)和returnAddress型別(它指向了一條位元組碼指令的地址)。區域性變量表所需的記憶體空間在編譯期間完成分配,即在Java程式被編譯成Class檔案時,就確定了所需分配的最大區域性變量表的容量。當進入一個方法時,這個方法需要在棧中分配多大的區域性變數空間是完全確定的,在方法執行期間不會改變區域性變量表的大小。
區域性變量表的容量以變數槽(Slot)為最小單位。在虛擬機器規範中並沒有明確指明一個Slot應占用的記憶體空間大小(允許其隨著處理器、作業系統或虛擬機器的不同而發生變化),一個Slot可以存放一個32位以內的資料型別:boolean、byte、char、short、int、float、reference和returnAddresss。reference是物件的引用型別,returnAddress是為位元組指令服務的,它執行了一條位元組碼指令的地址。對於64位的資料型別(long和double),虛擬機器會以高位在前的方式為其分配兩個連續的Slot空間。
虛擬機器通過索引定位的方式使用區域性變量表,索引值的範圍是從0開始到區域性變量表最大的Slot數量,對於32位資料型別的變數,索引n代表第n個Slot,對於64位的,索引n代表第n和第n+1兩個Slot。
在方法執行時,虛擬機器是使用區域性變量表來完成引數值到引數變數列表的傳遞過程的,如果是例項方法(非static),則區域性變量表中的第0位索引的Slot預設是用於傳遞方法所屬物件例項的引用,在方法中可以通過關鍵字“this”來訪問這個隱含的引數。其餘引數則按照引數表的順序來排列,佔用從1開始的區域性變數Slot,引數表分配完畢後,再根據方法體內部定義的變數順序和作用域分配其餘的Slot。
區域性變量表中的Slot是可重用的,方法體中定義的變數,作用域並不一定會覆蓋整個方法體,如果當前位元組碼PC計數器的值已經超過了某個變數的作用域,那麼這個變數對應的Slot就可以交給其他變數使用。這樣的設計不僅僅是為了節省空間,在某些情況下Slot的複用會直接影響到系統的而垃圾收集行為。
2、運算元棧
運算元棧又常被稱為操作棧,運算元棧的最大深度也是在編譯的時候就確定了。32位資料型別所佔的棧容量為1,64為資料型別所佔的棧容量為2。當一個方法開始執行時,它的操作棧是空的,在方法的執行過程中,會有各種位元組碼指令(比如:加操作、賦值元算等)向操作棧中寫入和提取內容,也就是入棧和出棧操作。
Java虛擬機器的解釋執行引擎稱為“基於棧的執行引擎”,其中所指的“棧”就是運算元棧。因此我們也稱Java虛擬機器是基於棧的,這點不同於Android虛擬機器,Android虛擬機器是基於暫存器的。
基於棧的指令集最主要的優點是可移植性強,主要的缺點是執行速度相對會慢些;而由於暫存器由硬體直接提供,所以基於暫存器指令集最主要的優點是執行速度快,主要的缺點是可移植性差。
3、動態連線
每個棧幀都包含一個指向執行時常量池(在方法區中,後面介紹)中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中的動態連線。Class檔案的常量池中存在有大量的符號引用,位元組碼中的方法呼叫指令就以常量池中指向方法的符號引用為引數。這些符號引用,一部分會在類載入階段或第一次使用的時候轉化為直接引用(如final、static域等),稱為靜態解析,另一部分將在每一次的執行期間轉化為直接引用,這部分稱為動態連線。
4、方法返回地址
當一個方法被執行後,有兩種方式退出該方法:執行引擎遇到了任意一個方法返回的位元組碼指令或遇到了異常,並且該異常沒有在方法體內得到處理。無論採用何種退出方式,在方法退出之後,都需要返回到方法被呼叫的位置,程式才能繼續執行。方法返回時可能需要在棧幀中儲存一些資訊,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,呼叫者的PC計數器的值就可以作為返回地址,棧幀中很可能儲存了這個計數器值,而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會儲存這部分資訊。
方法退出的過程實際上等同於把當前棧幀出站,因此退出時可能執行的操作有:恢復上層方法的區域性變量表和運算元棧,如果有返回值,則把它壓入呼叫者棧幀的運算元棧中,調整PC計數器的值以指向方法呼叫指令後面的一條指令。
本地方法棧(Native Method Stacks)
該區域與虛擬機器棧所發揮的作用非常相似,只是虛擬機器棧為虛擬機器執行Java方法服務,而本地方法棧則為使用到的本地作業系統(Native)方法服務。
Java堆(Java Heap)
Java Heap是Java虛擬機器所管理的記憶體中最大的一塊,它是所有執行緒共享的一塊記憶體區域。幾乎所有的物件例項和陣列都在這類分配記憶體。Java Heap是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”。
根據Java虛擬機器規範的規定,Java堆可以處在物理上不連續的記憶體空間中,只要邏輯上是連續的即可。如果在堆中沒有記憶體可分配時,並且堆也無法擴充套件時,將會丟擲OutOfMemoryError異常。
方法區(Method Area)
方法區也是各個執行緒共享的記憶體區域,它用於儲存已經被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。方法區域又被稱為“永久代”,但這僅僅對於Sun HotSpot來講,JRockit和IBM J9虛擬機器中並不存在永久代的概念。Java虛擬機器規範把方法區描述為Java堆的一個邏輯部分,而且它和Java Heap一樣不需要連續的記憶體,可以選擇固定大小或可擴充套件,另外,虛擬機器規範允許該區域可以選擇不實現垃圾回收。相對而言,垃圾收集行為在這個區域比較少出現。該區域的記憶體回收目標主要針是對廢棄常量的和無用類的回收。執行時常量池是方法區的一部分,Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池(Class檔案常量池),用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類載入後存放到方法區的執行時常量池中。執行時常量池相對於Class檔案常量池的另一個重要特徵是具備動態性,Java語言並不要求常量一定只能在編譯期產生,也就是並非預置入Class檔案中的常量池的內容才能進入方法區的執行時常量池,執行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的是String類的intern()方法。
根據Java虛擬機器規範的規定,當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError異常。
直接記憶體(Direct Memory)
直接記憶體並不是虛擬機器執行時資料區的一部分,也不是Java虛擬機器規範中定義的記憶體區域,它直接從作業系統中分配,因此不受Java堆大小的限制,但是會受到本機總記憶體的大小及處理器定址空間的限制,因此它也可能導致OutOfMemoryError異常出現。在JDK1.4中新引入了NIO機制,它是一種基於通道與緩衝區的新I/O方式,可以直接從作業系統中分配直接記憶體,即在堆外分配記憶體,這樣能在一些場景中提高效能,因為避免了在Java堆和Native堆中來回複製資料。關於NIO的詳細使用可以參考我的Java網路程式設計系列中關於NIO的相關文章。
記憶體溢位
下面給出個記憶體區域記憶體溢位的簡單測試方法
這裡有一點要重點說明,在多執行緒情況下,給每個執行緒的棧分配的記憶體越大,反而越容易產生記憶體溢位異常。作業系統為每個程序分配的記憶體是有限制的,虛擬機器提供了引數來控制Java堆和方法區這兩部分記憶體的最大值,忽略掉程式計數器消耗的記憶體(很小),以及程序本身消耗的記憶體,剩下的記憶體便給了虛擬機器棧和本地方法棧,每個執行緒分配到的棧容量越大,可以建立的執行緒數量自然就越少。因此,如果是建立過多的執行緒導致的記憶體溢位,在不能減少執行緒數的情況下,就只能通過減少最大堆和每個執行緒的棧容量來換取更多的執行緒。
另外,由於Java堆內也可能發生記憶體洩露(Memory Leak),這裡簡要說明一下記憶體洩露和記憶體溢位的區別:
記憶體洩露是指分配出去的記憶體沒有被回收回來,由於失去了對該記憶體區域的控制,因而造成了資源的浪費。Java中一般不會產生記憶體洩露,因為有垃圾回收器自動回收垃圾,但這也不絕對,當我們new了物件,並儲存了其引用,但是後面一直沒用它,而垃圾回收器又不會去回收它,這邊會造成記憶體洩露,
記憶體溢位是指程式所需要的記憶體超出了系統所能分配的記憶體(包括動態擴充套件)的上限。
物件例項化分析
對記憶體分配情況分析最常見的示例便是物件例項化:
Object obj = new Object();
這段程式碼的執行會涉及java棧、Java堆、方法區三個最重要的記憶體區域。假設該語句出現在方法體中,及時對JVM虛擬機器不瞭解的Java使用這,應該也知道obj會作為引用型別(reference)的資料儲存在Java棧的本地變量表中,而會在Java堆中儲存該引用的例項化物件,但可能並不知道,Java堆中還必須包含能查詢到此物件型別資料的地址資訊(如物件型別、父類、實現的介面、方法等),這些型別資料則儲存在方法區中。
另外,由於reference型別在Java虛擬機器規範裡面只規定了一個指向物件的引用,並沒有定義這個引用應該通過哪種方式去定位,以及訪問到Java堆中的物件的具體位置,因此不同虛擬機器實現的物件訪問方式會有所不同,主流的訪問方式有兩種:使用控制代碼池和直接使用指標。
通過控制代碼池訪問的方式如下:
通過直接指標訪問的方式如下:
這兩種物件的訪問方式各有優勢,使用控制代碼訪問方式的最大好處就是reference中存放的是穩定的控制代碼地址,在物件唄移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制代碼中的例項資料指標,而reference本身不需要修改。使用直接指標訪問方式的最大好處是速度快,它節省了一次指標定位的時間開銷。目前Java預設使用的HotSpot虛擬機器採用的便是是第二種方式進行物件訪問的。