JVM方法區

2023-07-24 21:00:17

JVM方法區

《Java虛擬機器器規範》中明確說明:「儘管所有的方法區在邏輯上是屬於堆的一部分,但一些簡單的實現可能不會選擇去進行垃圾收集或者進行壓縮。「但對於HotSpotJVM而言,方法區還有一個別名叫做Non-Heap(非堆),目的就是要和堆分開。方法區看作是一塊獨立於Java堆的記憶體空間。

1.特點

  • 方法區(Method Area)與Java堆一樣,是各個執行緒共用的記憶體區域。

  • 方法區在JVM啟動的時候被建立,並且它的實際的實體記憶體空間中和Java堆區一樣都可以是不連續的。

  • 方法區的大小,跟堆空間一樣,可以選擇固定大小或者可延伸。

  • 方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導致方法區溢位,虛擬機器器同樣會丟擲記憶體溢位錯誤: java.lang.OutofMemoryError:PermGen space 或者 java.lang.OutOfMemoryError: Metaspace載入大量的第三方的jar包; Tomcat部署的工程過多 (30-50個)大量動態的生成反射類

  • 關閉JVM就會釋放這個區域的記憶體

  • 在jdk7及以前,習慣上把方法區,稱為永久代。jdk8開始,使用元空間取代了永久代。

  • 本質上,方法區和永久代並不等價。僅是對hotspot而言的。《Java虛擬機器器規範》對如何實現方法區,不做統一要求。例如:BEA JRockit/ IBM J9中不存在永久代的概念。

  • 現在來看,當年使用永久代,不是好的idea。導致Java程式更容易OOM(超過-XX:MaxPermSize上限)

到了JDK 8,終於完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地記憶體中實現的元空間 (Metaspace) 來代替。元空間的本質和永久代類似,都是對JVM規範中方法區的實現。不過元空間與永久代最大的區別在於:元空間不在虛擬機器器設定的記憶體中,而是使用本地記憶體,內部結構也調整了。

2.內部結構

《深入理解Java 虛擬機器器》書中對方法區(Method Area)儲存內容描述如下:它用於儲存已被虛擬機器器載入的型別資訊、常數、靜態變數、即時編譯器編譯後的程式碼快取等。

對每個載入的型別(類class、介面interface、列舉enum、註解annotation),JVM必須在方法區中儲存以下型別資訊:

  1. 這個型別的完整有效名稱 (全名=包名.類名)
  2. 這個型別直接父類別的完整有效名(對於interface或是java.lang.object,都沒有父類別)
  3. 這個型別的修飾符(public,abstract, final的某個子集)
  4. 這個型別直接介面的一個有序列表

2.1域(Fieid)資訊

  • JVM必須在方法區中儲存型別的所有域的相關資訊以及域的宣告順序
  • 域的相關資訊包括: 域名稱、域型別、域修飾符(public,privateprotected,static,final,volatile,transient的某個子集)

2.2方法(Method)資訊

JVM必須儲存所有方法的以下資訊,同域資訊一樣包括宣告順序 :

  • 方法名稱
  • 方法的返回型別(或 void)
  • 方法引數的數量和型別(按順序)
  • 方法的修飾符(public,private, protected,static, final,synchronized,native, abstract的一個子集)
  • 方法的位元組碼(bytecodes)、運算元棧、區域性變數表及大小 (abstract和native方法除外)
  • 異常表(abstract和native方法除外)每個例外處理的開始位置、結束位置、程式碼處理在程式計數器中的偏移地址、被捕獲的異常類的常數池索引

2.3non-final的類變數

  • 靜態變數和類關聯在一起,隨著類的載入而載入,它們成為類資料在邏輯上的一部分

  • 類變數被類的所有範例共用,即使沒有類範例時你也可以存取它。

  • 全域性常數:static final,被宣告為final的類變數的處理方法則不同,每個全域性常數在編譯的時候就會被分配了。

2.4常數池

  • 方法區,內部包含了執行時常數池。

  • 位元組碼檔案,內部包含了常數池。

  • 類的資訊都在方法區要弄清楚方法區的執行時常數池,需要理解清楚ClassFile中的常數池。

  • 一個有效的位元組碼檔案中除了包含類的版本資訊、欄位、方法以及介面等描述資訊外,還包含一項資訊那就是常數池表 (Constant Pool Table),包括各種字面量和對型別域和方法的符號參照。

一個java原始檔中的類、介面,編譯後產生一個位元組碼檔案。而Java中的位元組碼需要資料支援,通常這種資料會很大以至於不能直接存到位元組碼裡,換另一種方式,可以存到常數池這個位元組碼包含了指向常數池的參照。在動態連結的時候會用到執行時常數池,之前有介紹。

2.4.1常數池有什麼

  • 數量值

  • 字串值

  • 類參照

  • 欄位參照

  • 方法參照

下面這段程式碼將會編譯成


常數池,可以看做是一張表,虛擬機器器指令根據這張常數表找到要執行的類名、方法名、引數型別、字面量等型別

2.5執行時常數池

  • 執行時常數池(Runtime Constant Pool)是方法區的一部分。

  • 常數池表(Constant Pool Table) 是class檔案的一部分,用於存放編譯期生成的各種字面量與符號參照,這部分內容將在類載入後存放到方法區的執行時常數池中。

  • 執行時常數池,在載入類和介面到虛擬機器器後,就會建立對應的執行時常數池。JVM為每個已載入的型別 (類或介面) 都維護一個常數池。池中的資料項像陣列項一樣,是通過索引存取的。

  • 執行時常數池中包含多種不同的常數,包括編譯期就已經明確的數值字面量,也包括到執行期解析後才能夠獲得的方法或者欄位參照。此時不再是常數池中的符號地址了,這裡換為真實地址。執行時常數池,相對於class檔案常數池的另一重要特徵是: 具備動態性。

  • 執行時常數池類似於傳統程式語言中的符號表(symbol table),但是它所包含的資料卻比符號表要更加豐富一些。

  • 當建立類或介面的執行時常數池時,如果構造執行時常數池所需的記憶體空間超過了方法區所能提供的最大值,則JVM會拋OutofMemoryError異常。

3.方法區的演進

首先明確: 只有HotSpot才有永久代。BEA JRockit、IBM J9等來說,是不存在永久代的概念的。原則上如何實現方法區屬於虛擬機器器實現細節,不受《Java虛擬機器器規範》管束,並不要求統一

Hotspot中方法區的變化:


3.1永久代為什麼會被元空間替換

Oracle官方是這麼說的

地址:https://openjdk.org/jeps/122

隨著Java8的到來,HotSpot VM 中再也見不到永久代了。但是這並不意味著類的後設資料資訊也消失了。這些資料被移到了一個與堆不相連的本地記憶體區域,這個區域叫做元空間( Metaspace )。

由於類的後設資料分配在本地記憶體中,元空間的最大可分配空間就是系統可用記憶體空間。

主要原因:

  • 記憶體限制和溢位問題:永久代的大小是固定的,並且不會自動擴充套件。在載入大量類或者動態生成類時,容易導致永久代記憶體溢位。這種情況下,需要手動調整永久代大小,並且很難確定合適的大小。在某些場景下,如果動態載入類過多,容易產生Perm 區的OOM 。比如某個實際web工程中,因為功能點比較多,在執行過程中,要不斷動態載入很多類,經常出現致命錯誤。

  • 對永久代進行調優是很困難的。垃圾回收效率低:永久代使用傳統的垃圾回收器進行垃圾回收,而傳統垃圾回收器主要針對堆記憶體進行優化。由於永代中存在大量不會被回收的類和後設資料資訊,導致垃圾回收效率低下。

  • 類解除安裝困難:在永久代中載入的類一般不會被解除安裝,即使它們已經不再使用。這可能導致記憶體漏失和佔用大量記憶體空間。

而元空間和永久代之間最大的區別在於:元空間並不在虛擬機器器中,而是使用本地記憶體。
因此,預設情況下,元空間的大小僅受本地記憶體限制。

4.方法區的垃圾回收

有些人認為方法區瞭如Hotspot虛擬機器器中的元空間或者永久代)是沒有垃圾收集行為的,其實不然。《Java虛擬機器器規範》對方法區的約束是非常寬鬆的,提到過可以不要求虛擬機器器在方法區中實現垃圾收集。事實上也確實有未實現或未能完整實現方法區型別解除安裝的收集器存在(如JDK 11時期的ZGC收集器就不支援類解除安裝)

一般來說這個區域的回收效果比較難令人滿意,尤其是型別的解除安裝,條件相當苛刻。但是這部分割區域的回收有時又確實是必要的。以前Sun公司的Bug列表中,曾出現過的若干個嚴重的Bug就是由於低版本的HotSpot虛擬機器器對此區域未完全回收而導致記憶體漏失。

方法區的垃圾收集主要回收兩部分內容:常數池中廢棄的常數和不再使用的型別。

在JVM的永久代(Permanent Generation)中,是存在垃圾回收的。然而,與堆記憶體中的垃圾回收機制不同,永久代的垃圾回收機制相對簡單,並且不會像堆記憶體那樣頻繁進行垃圾回收。

在早期的JVM版本中,永久代使用傳統的垃圾回收器進行垃圾回收。但是由於永久代中存放的是類的後設資料、常數池、靜態變數等資訊,這些物件一般不會被釋放或解除安裝。因此,永久代的垃圾回收主要針對無效或廢棄的類進行清理。

先來說說方法區內常數池之中主要存放的兩大類常數:字面量和符號參照。字面量比較接近Java語言層次的常數概念,如文字字串、被宣告為final的常數值等。而符號參照則屬於編譯原理方面的概念,包括下面三類常數

  • 類和介面的全限定名

  • 欄位的名稱和描述符

  • 方法的名稱和描述符

4.1永久代會進行垃圾回收嗎

在JVM的永久代(Permanent Generation)中,是存在垃圾回收的。然而,與堆記憶體中的垃圾回收機制不同,永久代的垃圾回收機制相對簡單,並且不會像堆記憶體那樣頻繁進行垃圾回收。

在早期的JVM版本中,永久代使用傳統的垃圾回收器進行垃圾回收。但是由於永久代中存放的是類的後設資料、常數池、靜態變數等資訊,這些物件一般不會被釋放或解除安裝。因此,永久代的垃圾回收主要針對無效或廢棄的類進行清理。

具體來說,在永久代中發生垃圾回收時,主要執行以下兩個操作:

  1. 清理無效類:當一個類不再被參照或者沒有被任何物件參照時,它就成為無效類。在垃圾回收過程中,JVM會掃描永久代中的類資訊,並清理掉這些無效類。
  2. 解除安裝廢棄常數:如果一個常數不再被任何類或者物件參照時,它就成為廢棄常數。在垃圾回收過程中,JVM也會清理掉這些廢棄常數。

需要注意的是,永久代的垃圾回收相對較少,並且在一些JVM實現中,甚至可以禁用永久代的垃圾回收。這是因為永久代中的物件一般不會頻繁建立和銷燬,而且它們的生命週期很長。然而,在JDK 1.8及以後的版本中,永久代被元空間(Metaspace)所取代,元空間使用與堆記憶體相同的垃圾回收機制進行記憶體回收。因此,在這些版本中,元空間會進行更頻繁和徹底的垃圾回收。