1. 程式人生 > >JVM——記憶體模型(三):堆與方法區

JVM——記憶體模型(三):堆與方法區

前兩篇部落格我們認識了程式計數器、虛擬機器棧與本地方法棧。今天我們來一起認識一下堆與方法區。

關於堆記憶體,我之前有寫過一篇關於堆外記憶體的部落格,裡面有詳細介紹堆記憶體。這裡為了觀看方便,就直接把關於堆內記憶體的部分拿過來咯。(想了解堆內記憶體與堆外記憶體的夥伴們,可以參考:Java——堆外記憶體詳解。)

1.Java堆記憶體

那什麼東西是堆記憶體呢?我們來看看官方的說法。

“Java 虛擬機器具有一個堆(Heap),堆是執行時資料區域,所有類例項和陣列的記憶體均從此處分配。堆是在 Java 虛擬機器啟動時建立的。”


也就是說,平常我們老遇見的那位,JVM啟動時分配的,就叫作堆記憶體(即堆內記憶體)。

Java堆是Java虛擬機器所管理的記憶體中最大的一塊,它是被所有執行緒共享的一塊記憶體空間,在虛擬機器啟動的時候建立。而這塊記憶體用來幹嘛呢?此記憶體區域唯一的目的就是用來存放物件例項,幾乎所有的物件例項都要在這裡分配。

(不過這裡需要注意,由於JIT編譯器的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的物件都分配在堆上就變得不是那麼“絕對的”)。

當然,正是因為Java堆用來存放物件例項,所以該區域也就成了垃圾收集器管理的主要區域了。物件的堆記憶體由稱為垃圾回收器的自動記憶體管理系統回收。

由於現在的垃圾收集器基本都採用分代收集演算法,因此從記憶體回收

的角度來看,理解jvm的堆還需要知道下面這個公式:

堆內記憶體 = 新生代+老年代+持久代


 如下圖:

而從記憶體分配的角度來看,執行緒共享的Java堆中可能劃分出多個執行緒私有的分配緩衝區,但是不論它如何劃分,都與存放的內容無關,無論是在哪一個區域,存放的都是物件的例項。(進一步劃分的目的是為了更好地回首記憶體,或者更好地分配記憶體) 

在使用堆內記憶體(on-heap memory)的時候,完全遵守JVM虛擬機器的記憶體管理機制,採用垃圾回收器(GC)統一進行記憶體管理,GC會在某些特定的時間點進行一次徹底回收,也就是Full GC,GC會對所有分配的堆內記憶體進行掃描。

注意:在這個過程中會對JAVA應用程式的效能造成一定影響,還可能會產生Stop The World

此外,堆的記憶體不需要是連續空間,因此堆的大小沒有具體要求,既可以固定,也可以擴大和縮小。我們在jvm引數中只要使用-Xms,-Xmx等引數就可以設定堆的大小和最大值。

 

2.方法區

方法區與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已經被虛擬機器載入的類位元組碼、class/method/field等元資料物件、static-final常量、static變數、即時編譯後的程式碼等資料。

雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但為了與堆區別開開來,它有一個別名叫做“Non-Heap”,即非堆。

另外,方法區包含了一個特殊的區域“執行時常量池”,它們的關係如下圖所示:

(注意,在JDK1.7之前,字串常量池存放在執行時常量池中,JDK1.7將字串常量池從執行時常量池分離出來,放到了堆裡。JDK1.8由於取消了“永久代”,方法區放在了元空間。)

注:想要了解字串建立和儲存的機制的夥伴可以參考:雜談——字串建立和儲存的機制及相關的例子

下面來了解一下方法區中儲存的這些東西。

(1)載入的類位元組碼:要使用一個類,首先需要將其位元組碼載入到JVM的記憶體中。至於類的位元組碼來源,可以多種多樣,如.class檔案、網路傳輸、或cglib位元組碼框架直接生成。
(2)class/method/field等元資料物件:位元組碼載入之後,JVM會根據其中的內容,為這個類生成Class/Method/Field等物件,它們用於描述一個類,通常在反射中用的比較多。不同於儲存在堆中的java例項物件,這兩種物件儲存在方法區中。
(3)static-final常量、static變數:對於這兩種型別的類成員,JVM會在方法區為它們建立一份資料,因此同一個類的static修飾的類成員只有一份;
(4)JIT編譯器的編譯結果:以hotspot虛擬機器為例,其在執行時會使用JIT即時編譯器對熱點程式碼進行優化,優化方式為將位元組碼編譯成機器碼。通常情況下,JVM使用“解釋執行”的方式執行位元組碼,即JVM在讀取到一個位元組碼指令時,會將其按照預先定好的規則執行棧操作,而棧操作會進一步對映為底層的機器操作;通過JIT編譯後,執行的機器碼會直接和底層機器打交道。如下圖所示:

3.方法區的實現

方法區的實現,虛擬機器規範中並未明確規定,目前有2種比較主流的實現方式:

(1)HotSpot虛擬機器1.7-:在JDK1.6及之前版本,HotSpot使用“永久代(permanent generation)”的概念作為實現,即將GC分代收集擴充套件至方法區。這種實現比較偷懶,可以不必為方法區編寫專門的記憶體管理,但帶來的後果是容易碰到記憶體溢位的問題(因為永久代有-XX:MaxPermSize的上限)。在JDK1.7+之後,HotSpot逐漸改變方法區的實現方式,如1.7版本移除了方法區中的字串常量池(上文匯中有說到)。

(2)HotSpot虛擬機器1.8+:1.8版本中移除了方法區並使用metaspace(元資料空間)作為替代實現。metaspace佔用系統記憶體,也就是說,只要不碰觸到系統記憶體上限,方法區會有足夠的記憶體空間。但這不意味著我們不對方法區進行限制,如果方法區無限膨脹,最終會導致系統崩潰。

由此我們可以引申:為什麼使用“永久代”並將GC分代收集擴充套件至方法區這種實現方式不好,會導致OOM?

首先要明白方法區的記憶體回收目標是什麼。方法區儲存了類的元資料資訊和各種常量,它的記憶體回收目標理應當是對這些型別的解除安裝和常量的回收。但由於這些資料被類的例項引用,解除安裝條件變得複雜且嚴格,回收不當會導致堆中的類例項失去元資料資訊和常量資訊。因此,回收方法區記憶體不是一件簡單高效的事情,往往GC在做無用功。另外隨著應用規模的變大,各種框架的引入,尤其是使用了位元組碼生成技術的框架,會導致方法區記憶體佔用越來越大,最終OOM。

4.執行時常量池

上文中說到,類的位元組碼在載入時會被解析並生成不同的東西存入方法區。類的位元組碼中不僅包含了類的版本、欄位、方法、介面等描述資訊,還包含了一個常量池。常量池用於存放在位元組碼中使用到的所有字面量和符號引用(如字串字面量),在類載入時,它們進入方法區的執行時常量池存放。

Class關鍵常量池與執行時常量池的區別如下:

  • Java虛擬機器對於Class檔案的每一部分(包括常量池)的格式都有嚴格的規定,每一個位元組用於儲存哪種資料都必須符合規範上的要求才會被虛擬機器認可、裝載和執行,但是對於執行時常量池,Java虛擬機器卻沒有做任何細節的要求,因此不同的提供商實現的虛擬機器可以按照自己的需要來實現這個記憶體區域。一般來說,除了儲存Class檔案中描述的符號引用,翻譯出來的直接引用也會直接儲存在執行時常量池中。
  • 執行時常量池對於Class檔案常量池的另外一個重要特徵就是具備動態性。Java語言並不要求常量一定只有編譯期才能產生,並非預置入Class檔案中常量池的內容才能進入方法區執行時常量池,即執行期間也可能將新的常量放入池中。這種特性被開發人員利用的比較多的就是String類的intern()方法。

其實我們也可以理解為:class檔案常量池只是.class檔案中的、靜態的;而執行時常量池,是在執行時將所有class檔案常量池中的東西載入進來的,它是動態的,可以在執行時將新的常量放入執行時常量池中。

而關於字串常量池,上文有說到,這裡就不提了。

由於執行時常量池方法區的一部分,它會收到方法區記憶體的限制,當常量池無法申請到記憶體的時候就會丟擲OutOfMemoryError異常。

 

 

 

好啦,以上就是堆與方法區的相關知識總結,如果大家有什麼更具體的發現或者發現文中有描述錯誤的地方,歡迎留言評論,我們一起學習呀~~

 

Biu~~~~~~~~~~~~~~~~~~~~宫å´éªé¾ç«è¡¨æå|é¾ç«gifå¾è¡¨æåä¸è½½å¾ç~~~~~~~~~~~~~~~~~~~~~~pia!

參考文章:《深入理解Java虛擬機器》周志明著

https://www.cnblogs.com/manayi/p/9651500.html