Java記憶體區域和常量池的總結
本文用最簡潔的描述,來總結出Java記憶體區域和常量池的相關知識,如需更加深入學習Java記憶體區域以及常量池,可參考閱讀《深入Java虛擬機器》或者網上優秀博文。
執行時資料區
執行資料區包含以下幾個區域:
- 方法區(Method Area)
- Java堆(Heap)
- 本地方法棧(Native Method Stack)
- 虛擬機器棧(VM Stack)
- 程式計數器(Program Conter Register)
其中方法區和堆是所有執行緒共享的資料區,而其他三個區收拾執行緒隔離的資料區。
Java虛擬機器在執行Java程式的過程中將它管理的記憶體劃分為若干個不同的區域,這些區域都擁有各自的用途以及建立和銷燬的時間。
方法區和堆都是依賴虛擬機器執行緒的啟動而建立;本地方法棧、虛擬機器棧和程式計數器都依賴使用者執行緒的啟動和結束而建立和銷燬。
程式計數器
- 該區是一塊較小的記憶體空間。
- 可以看作是位元組碼的行號指示器。
- 分支、迴圈、跳轉、異常處理、執行緒恢復等基礎工作都是基於程式計數器來完成的,因為位元組碼直譯器工作的時候,就是根據程式計數器的值來確認下一條需要執行的位元組碼子令。
- 該區是執行緒獨立的,被稱為“執行緒私有”的記憶體。
- 該區域是唯一一個在Java虛擬機器規範中沒有規定任何OOM(OutOfMemoryError)情況的區域。
Java虛擬機器棧
- 該區是執行緒私有的。
- Java虛擬機器棧描述的是Java方法的記憶體模型。
- 該區就是所謂的“棧記憶體”,實際上Java記憶體不可粗糙的分為堆記憶體和棧記憶體,因為Java記憶體區域的劃分比這複雜的多。
- 該區會丟擲StackOverflowError異常和OOM(OutOfMemoryError)異常;當執行緒請求的棧深度大於虛擬機器所允許的深度時,會丟擲棧溢位異常。因為虛擬機器棧是可以動態擴充套件的,所以當虛擬機器棧無法申請到足夠的記憶體時,就會丟擲OOM異常。
關於Java方法的記憶體模型
每個Java方法在執行的同時,都會建立一個棧幀存放於棧中,而該棧幀中會儲存區域性變數表、運算元棧、動態連線、方法出口等資訊。當方法結束呼叫,棧幀就會出棧。
區域性變數表
區域性變數表存放:
- 編譯期可知的基本資料型別(boolean、char、byte、short、long、flag、int、double)。
- 物件引用。
- returnAddress(指向一條位元組碼指令的地址)。
對於區域性變數表,需要注意:
- 區域性變數表所需的記憶體空間在編譯期就已經完成分配了,當進入一個方法時這個方法需要在棧幀中分配多大的區域性變數空間是完全確定的,在方法執行期間不能改變區域性變數表的大小。
本地方法棧
- 本地方法棧使用到的是Native方法服務,基本的原理和Java虛擬機器棧非常相似。
- Sun HotSpot將本地方法棧和Java虛擬機器棧合二為一。
- 本地方法棧也會丟擲StackOverflowErrot和OOM異常。
Java堆
- Java堆是虛擬機器所管理的記憶體中最大的一塊。並且是執行緒共享的,隨著虛擬機器執行緒的啟動而建立。
- 此區域的唯一目的就是儲存物件,幾乎所有的例項物件都在該區分配記憶體,為什麼不是全部呢?因為一個類的java.lang.Object類物件是在方法區中分配記憶體的。
- 幾乎所有的物件例項以及陣列都要在堆上分配記憶體。
- 此區是垃圾收集器管理的主要區域,所以此區也被稱為GC堆。
- Java堆可分為新生代、老年代,新生代又可細分為Eden空間、From Survivor空間、To Survivor空間。
- 堆記憶體中的區域是物理上不連續的,但邏輯上是連續的記憶體區域。
- 此區域可擴充,也可固定,一般都是定為可擴充(通過-Xmx和-Xms控制)。
- 堆中如果沒有記憶體分配給例項,並且無法再擴充記憶體區域了,就會丟擲OOM異常。
方法區
- 方法區是執行緒共享的區域。
- 方法區儲存虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。
- 方法區被很多人稱為“永久代”,因為HotSpot團隊選擇把GC延伸至方法區。不過現在已經有放棄永久代並逐步改為採用Native Memory來實現方法區的規劃了,在JDK1.7的HotSpot中,一把原本放在永久代中的字串常量池移出。
- 記憶體區域可以是物理上不連續的,但邏輯上是連續的,與堆記憶體是一致的。
- 方法區因為總是存放不會輕易改變的內容,故又被稱之為“永久代”。HotSpot也選擇把GC分代收集擴充套件至方法區,但也容易遇到記憶體溢位問題。可以選擇不實現垃圾回收,但如果回收就主要涉及常量池的回收和類的解除安裝。
- 該區域無法滿足記憶體分配需求時,會丟擲OOM異常。
非執行時資料區——直接記憶體
- 直接記憶體不是執行時資料區的一部分,並不是Java定義規範中的記憶體區域,但是不合理的使用也會導致OOM異常。
- JDK 1.4中新加入了NIO類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過一個儲存再Java堆中的DirectByteBuffer物件作為這塊記憶體的引用進行操作。
執行時常量池?靜態常量池(class檔案常量池)?字串常量池?有什麼區別?下面一去來看看這個讓人容易混淆的三個概念。
Java中的常量池
class檔案常量池
我們都知道,class檔案中除了包含類的版本、欄位、方法、介面等描述資訊外,還有一項資訊就是常量池(constant pool table),用於存放編譯器生成的各種字面量(Literal)和符號引用(Symbolic References),這就是我們所說的class檔案常量池。 字面量就是我們所說的常量概念,如文字字串、被宣告為final的常量值等。 符號引用是一組符號來描述所引用的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可(它與直接引用區分一下,直接引用一般是指向方法區的本地指標,相對偏移量或是一個能間接定位到目標的控制程式碼)。一般包括下面三類常量:
- 類和介面的全限定名
- 欄位的名稱和描述符
- 方法的名稱和描述符
(每種常量型別的資料結構可以檢視《深入理解java虛擬機器》第六章的內容)
- class常量池是在編譯的時候每個class都有的,在編譯階段,存放的是常量的符號引用。
執行時常量池
- 執行時常量池是方法區中的一部分。
- Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有常量池(Constant Pool Table)。
- 常量池中儲存的是編譯期生成的各種字面量和符號引用,在JDK1.6及其之前的版本,這部分內容都將在類載入完後進入到方法區的執行時常量池中存放。
- 執行時常量池相比於類常量池的不同特徵在於,執行時常量池具備動態性,也就是說不一定要編譯期的時候才能產生常量,也可以在執行時產生常量,比如在執行期間呼叫String.intern()方法,也可以將新的常量放入執行時常量池中。
- 執行時常量池是在類載入完成之後,將每個class常量池中的符號引用值轉存到執行時常量池中,也就是說,每個class都有一個執行時常量池,類在解析之後,將符號引用替換成直接引用,與全域性常量池中的引用值保持一致。
string.intern()作用:
檢查字串常量池中是否存在String並返回池裡的字串引用;若池中不存在,則將其加入池中,並返回其引用。 這樣做主要是為了避免在堆中不斷地建立新的字串物件。
字串常量池
- 字串常量池在每個VM中只有一份,存放的是字串常量的引用值。
- 字串常量池——string pool,也叫做string literal pool。
- 字串池裡的內容是在類載入完成,經過驗證,準備階段之後在堆中生成字串物件例項,然後將該字串物件例項的引用值存到string pool中。
- string pool中存的是引用值而不是具體的例項物件,具體的例項物件是在堆中開闢的一塊空間存放的。
對於string pool:
在HotSpot VM裡實現的string pool功能的是一個StringTable類,它是一個雜湊表,裡面存的是駐留字串(也就是我們常說的用雙引號括起來的)的引用(而不是駐留字串例項本身),也就是說在堆中的某些字串例項被這個StringTable引用之後就等同被賦予了”駐留字串”的身份。這個StringTable在每個HotSpot VM的例項只有一份,被所有的類共享。
JDK不同版本與常量池位置的變化
- 在JDK1.6版本以前,執行時常量池在方法區中;JDK1.7版本是,執行時常量池是在堆中;JDK1.8時,執行時常量池是在方法區和堆區相對獨立的元空間(Metaspace),而不是在堆區。
- 不同版本因為OOM導致的問題: JDK1.6版本——java.lang.OutOfMemoryError: PermGen space; JDK1.7版本——java.lang.OutOfMemoryError:Java heap space; JDK1.8版本——java.lang.OutOfMemoryError: Metaspace;