標籤:相同 stat 規範 動態性 log 入棧 產生 尋找 區別
運行時資料區域
Java虛擬機器在執行Java的過程中會把管理的記憶體劃分為若干個不同的資料區域。這些地區有各自的用途,以及建立和銷毀的時間,有的地區隨著虛擬機器進程的啟動而存在,而有的地區則依賴線程的啟動和結束而建立和銷毀。
Java虛擬機器包括下面幾個運行時資料區域:
程式計數器
程式計數器是一塊較小的地區,它的作用可以看做是當前線程所執行的位元組碼的行號指標。在虛擬機器的模型裡,位元組碼指標就是通過改變程式計數器的值來指定下一條需要執行的指令。分支,迴圈等基礎功能就是依賴程式計數器來完成的。
由於java虛擬機器的多線程是通過輪流切換並分配處理器執行時間來完成,一個處理器同一時間只會執行一條線程中的指令。為了線程恢複後能夠恢複正確的執行位置,每條線程都需要一個獨立的程式計數器,以確保線程之間互不影響。所以程式計數器是“線程私人”的記憶體。
如果虛擬機器正在執行的是一個Java方法,則計數器指定的是位元組碼指令對應的地址,如果正在執行的是一個本地方法,則計數器指定問空undefined。程式計數器地區是Java虛擬機器中唯一沒有定義OutOfMemory異常的地區。
Java虛擬機器棧
和程式計數器一樣也是線程私人的,生命週期與線程相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法被執行的時候都會建立一個棧幀用於儲存局部變數表,操作棧,動態連結,方法出口等資訊。每一個方法被調用的過程就對應一個棧幀在虛擬機器棧中從入棧到出棧的過程。
通常所說的虛擬機器運行時分為棧和堆,這裡的棧指的就是虛擬機器棧或者說虛擬機器棧中的局部變數表部分。
局部變數表存放了編譯器可知的各種基礎資料型別 (Elementary Data Type)、對象引用和returnAddress類型(指向一條位元組碼指令的地址)。局部變數表所需的記憶體空間在編譯器完成分配,當進入一個方法時這個方法需要在幀中分配多大的記憶體空間是完全確定的,運行期間不會改變局部變數表的大小。(64為長度的long和double會佔用兩個局部變數空間,其他的資料類型佔用一個)
Java虛擬機器棧可能出現兩種類型的異常:1. 線程請求的棧深度大於虛擬機器允許的棧深度,將拋出StackOverflowError。2.虛擬機器棧空間可以動態擴充,當動態擴充是無法申請到足夠的空間時,拋出OutOfMemory異常。
堆疊溢位錯誤一般是遞迴調用嘛。下面的代碼就可以出現:
package T20131009;public class StackOverflowTest { public static void main(String[] args) { method(); } public static void method(){ for(;;) method(); }}
運行結果:
2.OutOfMemoryError
記憶體溢出一般是出現在申請了較多的記憶體空間沒有釋放的情形。下面的代碼就可以出現:
package T20131009;import java.util.ArrayList;import java.util.List;public class OutOfMemoryTest { public static void main(String[] args){ List list=new ArrayList(); for(;;){ int[] tmp=new int[1000000]; list.add(tmp); } }}
運行結果:
本地方法棧
本地方法棧和虛擬機器棧基本類似,只不過Java虛擬機器棧執行的是Java代碼(位元組碼),本地方法棧中執行的是本地方法的服務。本地方法棧中也會拋出StackOverflowError和OutOfMemory異常。
堆
堆是Java虛擬機器所管理的記憶體中最大的一塊。堆是所有線程共用的一塊地區,在虛擬機器啟動時建立。堆的唯一目的是存放對象執行個體,幾乎所有的對象執行個體都在這裡分配,不過隨著JIT編譯器的發展和逃逸技術的成熟,棧上分配和標量替換技術使得這種情況發生著微妙的變化,對上分配正變得不那麼絕對。
附:在Java程式設計語言和環境中,即時編譯器(JIT compiler,just-in-time compiler)是一個把Java的位元組碼(包括需要被解釋的指令的程式)轉換成可以直接發送給處理器的指令的程式。當你寫好一個Java程式後,源語言的語句將由Java編譯器編譯成位元組碼,而不是編譯成與某個特定的處理器硬體平台對應的指令代碼(比如,Intel的Pentium微處理器或IBM的System/390處理器)。位元組碼是可以發送給任何平台並且能在那個平台上啟動並執行獨立於平台的代碼。
Java堆是垃圾收集器管理的主要區域,所以也稱為“GC堆”。由於現在的垃圾收集器基本上都是採用分代收集演算法,所以Java堆還可細分為:新生代和老生代。在細緻一點可分為Eden空間,From Survivor空間,To Survivor空間。如果從記憶體配置的角度看,線程共用的Java堆可劃分出多個線程私人的分配緩衝區。不過無論如何劃分,都與存放內容無關,無論哪個地區,都是用來存放對象執行個體。細分的目的是為了更好的回收記憶體或者更快的分配記憶體。
Java堆可以是物理上不連續的空間,只要邏輯上連續即可,主流的虛擬機器都是按照可擴充的方式來實現的。如果當前對中沒有記憶體完成對象執行個體的建立,並且不能在進行記憶體擴充,則會拋出OutOfMemory異常。
方法區
方法區也是線程共用的地區,用於儲存已經被虛擬機器載入的類資訊,常量,靜態變數和即時編譯器(JIT)編譯後的代碼等資料。Java虛擬機器把方法區描述為堆的一個邏輯分區,不過方法區有一個別名Non-Heap(非堆),用於區別於Java堆區。
Java虛擬機器規範對這個地區的限制也非常寬鬆,除了可以是物理不連續的空間外,也允許固定大小和擴充性,還可以不實現垃圾收集。相對而言,垃圾收集行為在這個地區是比較少出現的(所以常量和靜態變數的定義要多注意)。方法區的記憶體收集還是會出現,不過這個地區的記憶體收集主要是針對常量池的回收和對類型的卸載。
一般來說方法區的記憶體回收比較難以令人滿意。當方法區無法滿足記憶體配置需求時將拋出OutOfMemoryError異常。
運行時常量池
運行時常量池是方法區的一部分,Class檔案中除了有類的版本,欄位,方法,介面等資訊以外,還有一項資訊是常量池用於儲存編譯器產生的各種字面量和符號引用,這部分資訊將在類載入後存放到方法區的運行時常量池中。Java虛擬機器對類的每一部分(包括常量池)都有嚴格的規定,每個位元組用於儲存哪種資料都必須有規範上的要求,這樣才能夠被虛擬機器認可,裝載和執行。一般來說,除了儲存Class檔案中描述的符號引用外,還會把翻譯出來的直接引用也儲存在運行時常量池中。
運行時常量池相對於Class檔案常量池的另外一個重要特徵是具備動態性,Java虛擬機器並不要求常量只能在編譯期產生,也就是並非預置入Class檔案常量池的內容才能進入方法區的運行時常量池中,運行期間也可將新的常量放入常量池中。
常量池是方法區的一部分,所以受到記憶體的限制,當無法申請到足夠記憶體時會拋出OutOfMemoryError異常。
對象訪問
對象訪問在Java語言中無處不在,即使是最簡單的訪問,也會涉及到Java棧,java堆,方法區這三個最重要的記憶體地區之間的關聯關係。如下面的代碼:
Object obj = new Object();
假設這段代碼出現在方法體中,那麼“Object obj”部分的語義將會反映到Java棧的本地變數表中,作為一個reference類型的資料存在。而“new Object();”部分的語義將會反應到Java堆中,形成一Block StorageObject類型所有執行個體資料值(Instance Data)的結構化記憶體,根據具體類型以及虛擬機器實現的對象分布的不同,這塊記憶體的長度是不固定的。另外,在JAVA堆中還必須包含能尋找到此對象記憶體資料的地址資訊,這些類型資料則儲存在方法區中。
由於reference類型在Java虛擬機器中之規定了指向對象的引用,並沒有規定這個引用要通過哪種方式去定位,以及訪問到Java堆中的對象的具體位置,因此虛擬機器實現的對象訪問方式會有所不同。主流的訪問方式有兩種:控制代碼訪問方式和直接指標。
1. 如果使用控制代碼訪問方式,Java堆中將會劃分出一塊記憶體來作為控制代碼池,reference中儲存的就是對象的地址,而控制代碼中包含了對象執行個體資料和類型資料各自的具體地址資訊。
2. 如果通過直接指標方式訪問,Java堆對象的布局中就必須考慮如何放置訪問類型資料的相關資訊,reference中直接儲存的就是對象的地址。
兩種方式各有優勢,局並訪問方式最大的好處是reference中存放的是穩定的控制代碼地址,在對象被移動時,只會改變控制代碼中的執行個體資料指標,而reference本身不需要被修改。而指標訪問的最大優勢是速度快,它節省了一次指標定位的開銷,由於對象訪問在Java中非常頻繁,一次這類開銷積少成多後也是一項非常可觀的成本。
具體的訪問方式都是有虛擬機器指定的,虛擬機器Sun HotSpot使用的是直接指標方式,不過從整個軟體開發的範圍來看,各種語言和架構使用控制代碼訪問方式的情況十分常見。
java記憶體劃分