1. 程式人生 > 其它 >執行時資料區03--方法區

執行時資料區03--方法區

棧、堆、方法區的互動關係

從執行緒共享與否的角度

  • 執行緒共享:堆、元空間
  • 執行緒私有:虛擬機器棧、本地方法棧、程式計數器

互動關係圖1

互動關係圖2

方法區的理解

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

所以,方法區看作是一塊獨立於 Java 堆的記憶體空間

方法區的基本理解

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

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

方法區的大小,跟堆空間一樣,可以選擇固定大小或者可擴充套件。

方法區的大小決定了系統可以儲存多少個類,如果系統定義了太多的類,導致方法區溢位,虛擬機器同樣會丟擲記憶體溢位錯誤:java.lang. OutOfMemoryError: PermGen space或者 java.lang. Outofmemoryerror: Metaspace

  • 載入大量的第三方的 Jar 包
  • Tomcat 部署的工程過多(30-50 個)
  • 大量動態的生成反射類

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

Hotspot中方法區的演進

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

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

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

而到了 JDK8, 終於完全廢棄了永久代的概念,改用與 JRockit、J9 一樣在本地記憶體中實現的元空間(Metaspace)來代

元空間的本質和永久代類似,都是對 JVM 規範中方法區的實現。不過元空間與永久代最大的區別在於:元空間不在虛擬機器設定的記憶體中,而是使用本地記憶體。

永久代、元空間二者並不只是名字變了,內部結構也調整了

根據《Java 虛擬機器規範》的規定,如果方法區無法滿足新的記憶體分配需求時,將丟擲 OOM 異常

設定方法區大小與OOM

設定方法區記憶體大小

方法區的大小不必是固定的,jvm可以根據應用的需要動態調整

  • jdk7及以前

    • -XX:PermSize來設定永久代初始分配空間。預設值是20.75M
    • -XX:MaxPermSize來設定永久代最大可分配空間。32位機器預設是64M,64位機器預設是82M
    • 當JVM載入的類資訊容量超過了這個值,會報異常OutOfMemoryError:PremGen space
  • jdk8及以後

    • 元資料區大小可以使用引數-XX: MetaspacesSize 和-XX: MaxMetaspaceSize:指定,替代上述原有的兩個引數
    • 預設值依賴於平臺。windows 下,-XX: Metaspacesize 是 21M, -XX: Maxmetaspacesize 的值是-1, 即沒有限制
    • 與永久代不同,如果不指定大小,預設情下,虛擬機器會耗盡所有的可用系統記憶體。如果元資料區發生溢位,虛擬機器一樣會丟擲異常 OutOfMemoryError: Metaspace
    • -XX: Metaspacesize:設定初始的元空間大小。對於一個 64 位的伺服器端 JVM 來說其預設的-XX: MetaspaceSize 值為 21MB。這就是初始的高水位線,一旦觸及這個水位線,Full GC 將會被觸發並解除安裝沒用的類(即這些類對應的類載入器不再存活),然後這個高水位線將會重置。新的高水位線的值取決於 GC 後釋放了多少元空間。如果釋放的空間不足,那麼在不超過 MaxMetaspaceSize 時,適當提高該值。如果釋放空間過多,則適當降低該值。
    • 如果初始化的高水位線設定過低,上述高水位線調整情況會發生很多次。通過垃圾回收器的日誌可以觀察到 Full GC 多次呼叫。為了避免頻繁地 GC,建議將- XX: Metaspacesize 設定為一個相對較高的值。

如何解決這些OOM

1、要解決 OOM 異常或 heap space 的異常,一般的手段是首先通過記憶體映像分析工具(如 Eclipse Memory Analyzer)對 dump 出來的堆轉儲快照進行分析,重點是確認記憶體中的物件是否是必要的,也就是要先分清楚到底是出現了記憶體洩漏(Memory Leak)還是記憶體溢位(Memory Overflow)。

2、如果是記憶體洩漏,可進一步通過工具看洩漏物件到 GC Roots 的引用鏈。於是就能找到洩漏物件是通過怎樣的路徑與 GC Roots 相關聯並導致垃圾收集器無法自動回收它們的。掌握了洩漏物件的型別資訊,以及 GC Roots 引用鏈的資訊,就可以比較準確地定位出洩漏程式碼的位置。

3、如果不存在記憶體洩漏,換句話說就是記憶體中的物件確實都還必須存活著,那就應當檢査虛擬機器的堆引數(-Xmx 與-Xms),與機器實體記憶體對比看是否還可以調大,從程式碼上檢是否存在某些物件生命週期過長、持有狀態時間過長的情,嘗試減少程式執行期的記憶體消耗。

方法區的內部結構

方法區儲存什麼?

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

型別資訊:

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

①這個型別的完整有效名稱(全名=包名.類名)

②這個型別直接父類的完整有效名(對於 interface 或是 java.lang.Object,都沒有父類)

③這個型別的修飾符(public, abstract, final 的某個子集)

④這個型別直接介面的一個有序列表

域(Field)資訊

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

方法(Method)資訊

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

  • 方法名稱

  • 方法的返回型別(或 void)

  • 方法引數的數量和型別(按順序)

  • 方法的修飾符(public, private, protected, static, final, synchronized, native, abstract 的一個子集)

  • 方法的位元組碼(bytecodes)、運算元棧、區域性變量表及大小(abstract 和 native 方法除外)

  • 異常表(abstract 和 native 方法除外)

    • 每個異常處理的開始位置、結束位置、程式碼處理在程式計數器中的偏移地址、被捕獲的異常類的常量池索引

non-final的類變數

  • 靜態變數和類關聯在一起,隨著類的載入而載入,它們成為類資料在邏輯上的一部分
  • 類變數被類的所有例項共享,即使沒有類例項時你也可以訪問它
  • 補充說明:全域性常量:static final
    被宣告為final的類變數的處理方法則不同,每個全域性常量在編譯的時候就會被分配了

執行時常量池

  • 執行時常量池VS常量池

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

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

      • 一個有效的位元組碼檔案中除了包含類的版本資訊、欄位、方法以及介面等描述資訊外,還包含一項資訊那就是常量池表(COnstant pool table),包括各種字面量和對型別、域和方法的符號引用
    • 要弄清楚方法區,需要理解清楚 classfile,因為載入類的資訊都在方法區

    • 要弄清楚方法區的執行時常量池,需要理解清楚 Classfile 中的常量池

  • 為什麼需要常量池?

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

    • 數量值
    • 字串值
    • 類引用
    • 欄位引用
    • 方法引用
  • 執行時常量池

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

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

    • 執行時常量池,在載入類和介面到虛擬機器後,就會建立對應的執行時常量池。

    • JVM 為每個已載入的型別(類或介面)都維護一個常量池。池中的資料項像陣列項一樣,是通過索引訪問的

    • 執行時常量池中包含多種不同的常量,包括編譯期就已經明確的數值字面量,也包括到執行期解析後才能夠獲得的方法或者欄位引用。此時不再是常量池中的符號地址了,這裡換為真實地址。

      • 執行時常量池,相對於 class 檔案常量池的另一重要特徵是:具備動態性

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

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

方法區使用舉例

...

方法區的演進細節

方法區演進細節

1.首先明確:只有HotSpot才有永久代

2.HotSpot中方法區的變化

  • Jk1.6 及之前有永久代(permanent generation),靜態變數(物件)存放在永久代上(物件例項永遠放在堆空間)
  • jdk1.7有永久代,但己經逐步“去永久代”,字串常量池、靜態變數移除,儲存在堆中
  • jk1.8 及之後無永久代,型別資訊、欄位、方法、常量儲存在本地記憶體的元空間,但字串常量池、靜態變數仍在堆

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

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

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

  • 這項改動是很有必要的,原因有:

    1. 為永久代設定空間大小是很難確定的。

    在某些場景下,如果動態載入類過多,容易產生 Perm 區的 OOM。比如某個實際 Web 工程中,因為功能點比較多,在執行過程中,要不斷動態載入很多類,經常出現致命錯誤。

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

    1. 對永久代進行調優是很困難的。

String Table 為什麼要調整

  • Jdk7 中將 stringtable 放到了堆空間中。因為永久代的回收效率很低,在 full gc 的時候オ會觸發。而 full gc 是老年代的空間不足、永久代不足時會觸發。這就導致 Stringtable 回收效率不高。而我們開發中會有大量的字串被建立,回收效率低,導致永久代記憶體不足。放到堆裡,能及時回收記憶體。

靜態變數放在哪裡

  • 從《Java 虛擬機器規範》所定義的概念模型來看,所有 Class 相關的資訊都應該存放在方法區之中,但方法區該如何實現,《Java 虛擬機器規範》並未做出規定,這就成了一件允許不同虛擬機器自己靈活把握的事情。JDK7 及其以後版本的 Hotspot 虛擬機器選擇把靜態變數與型別在 Java 語言一端的對映 class 物件存放在一起,儲存於Java 堆之中,從我們的實驗中也明確驗證了這一點

方法區的垃圾回收

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

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

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

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

    1、類和介面的全限定名

    2、欄位的名稱和描述符

    3、方法的名稱和描述符

  • Hotspot 虛擬機器對常量池的回收策略是很明確的,只要常量池中的常量沒有被任何地方引用,就可以被回收。

  • 回收廢棄常量與回收Java 堆中的物件非常類似。

  • 判定一個常量是否“廢棄”還是相對簡單,而要判定一個型別是否屬於“不再被使用的類”的條件就比較苛刻了。需要同時滿足下面三個條件:

    • 該類所有的例項都經被回收,也就是 Java 堆中不存在該類及其任何派生子類的例項。
    • 載入該類的類載入器已經被回收,這個條件除非是經過精心設計的可替換類載入器的場景,如 OSGi 、JSP 的重載入等,否則通常是很難達成的
    • 該類對應的 java.lang.Class 物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
  • Java 虛擬機器被允許對滿足上述三個條件的無用類進行回收,這裡說的僅僅是“被允許”,而並不是和物件一樣,沒有引用了就必然會回收。關於是否要對型別進行回收, Hotspot 虛擬機器提供了-Xnoclassgc 引數進行控制,還可以使用- verbose: class 以及 -XX: + TracedClass- Loading、-XX: + TracedClassUnLoading 檢視類載入和解除安裝資訊

  • 在大量使用反射、動態代理、CGLib等位元組碼框架,動態生成 JSP 以及 OSGi 這類頻繁自定義類載入器的場景中,通常都需要 Java 虛擬機器具備型別解除安裝的能力,以保證不會對方法區造成過大的記憶體壓力。

總結

常見面試題

  • 說一下JVM記憶體模型吧,有哪些區?分別幹什麼的?
  • Java8的記憶體分代改進
  • JVM記憶體分為哪幾個區,每個區的作用是什麼?
  • JVM記憶體分佈/記憶體結構?堆和棧的區別?堆的結構?為什麼有兩個survivor區?
  • Eden和Survivor的比例分配?
  • jvm記憶體分割槽,為什麼要有新生代和老年代
  • 講講JVM執行時資料區
  • 什麼時候物件會進入老年代
  • JVM的永久代中會發生垃圾回收嗎?