【JVM】JVM內存區域你瞭解嗎?

1.JVM 內存區域

該結構圖 JDK 版本:JDK 1.7

JVM 內存區域主要分爲線程私有區域【程序計數器、虛擬機棧、本地方法區】、線程共享區域【Java 堆、方法區】、直接內存。

線程私有數據區域生命週期與線程相同,依賴用戶線程的啓動/結束而創建/銷燬(在 Hotspot VM 內,每個線程都與操作系統的本地線程直接映射,因此這部分內存區域的存/否跟隨系統本地線程的生/死對應)。

線程共享區隨虛擬機的啓動/關閉而創建/銷燬。

直接內存並不是 JVM 運行時數據區的一部分,但也會被頻繁的使用:在 JDK 1.4 引入的 NIO 提供了基於 Channel 與 Buffer 的 IO 方式,它可以使用 Native 函數庫直接分配堆外內存,然後使用 DirectByteBuffer 對象作爲這塊內存的引用進行操作(詳見:Java I/O擴展),這樣就避免了在 Java 堆和 Native 堆中來回複製數據,因此在一些場景中可以顯著提高性能。

2.內存區域思維導圖

需要xmind文件的可從我公衆號加我微信。

3.程序計數器(線程私有)

程序計數器是一個記錄着當前線程所執行的字節碼的行號指示器。

只佔用一塊較小的內存空間(在進行 JVM 計算時,可以忽略不計),每條線程都有一個獨立的程序計數器,這類內存也稱爲“線程私有”的內存。

畫外音:假設程序永遠只有一個線程,我們就不需要程序計數器。

JVM 的多線程是通過 CPU 時間片輪轉(即線程輪流切換並分配處理器執行時間)算法來實現的。

也就是說,某個線程在執行過程中可能會因爲時間片耗盡而被掛起,而另一個線程獲取到時間片開始執行。

當被掛起的線程重新獲取到時間片的時候,它要想從被掛起的地方繼續執行,就必須知道它上次執行到哪個位置,在JVM中,通過程序計數器來記錄某個線程的字節碼執行位置。

正在執行 Java 方法的話,計數器記錄的是虛擬機字節碼指令的地址(當前指令的地址)。如果是執行 Native 方法,則它的值爲空。

這個內存區域是唯一一個在虛擬機中沒有規定任何 OutOfMemoryError 情況的區域。

4.虛擬機棧(線程私有)

描述 Java 方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀(Statkc Frame)用於存儲【局部變量表】、【操作數棧幀】、【動態鏈接】、【方法出口】等信息。

每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

如下圖所示:

畫外音:棧幀隨着方法調用而創建,隨着方法結束而銷燬——無論方法是正常完成還是異常完成(拋出了在方法內未捕獲的異常)都算作方法結束。

棧幀(Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接(Dynamic Linking)、方法返回值和異常分派(Dispatch Exception)。

棧幀結構圖:

5.本地方法棧(線程私有)

本地方法棧和虛擬機棧(Java Stack)作用類似,區別是虛擬機棧爲執行 Java 方法服務,而本地方法棧則爲 Native 方法服務,如果一個 VM 實現使用 C-likage 模型來支持 Native 調用,那麼該棧將會是一個 C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機棧合二爲一。

畫外音:在HotSpot 虛擬機中未對本地方法棧和虛擬機棧作區分,統稱爲棧。

6. 堆(Heap-線程共享)-運行時數據區

堆是被線程共享的一塊內存區域,創建的對象和數組都保存在 Java 堆內存中,也是垃圾收集器進行垃圾收集的最重要的內存區域。

由於現代 VM 採用分代收集算法,因此 Java 堆從 GC 的角度還可以細分爲:新生代(Eden 區、From Survivor 區和 To Survivor 區)和老年代

如下圖:

畫外音:新生代劃分也有這樣的叫法:伊甸園(Eden space),倖存者0區(Survivor 0 space)和倖存者1區(Survivor 1 space)

7.方法區(線程共享)

JDK1.7 及之前版本的方法區和 Java 堆一樣,是各個線程共享的內存區域,也稱非堆(Non-Heap),用於存儲已經被虛擬機加載的【類信息】、【常量】、【靜態常量】、【即時編譯器(JIT)編譯後的代碼】等數據,它同樣存在垃圾回收,這個區域的內存回收目標主要是針對常量池的回收和對類型的卸載。當方法區無法滿足內存分配需求時,將拋出 OutOfMemoryError 異常。

畫外音:對於方法區,很多人更願意稱爲:“永久代(Permanent Generation)”,不過本質上兩者並不等價,僅僅是因爲習慣使用 HotSpot 虛擬機的設計團隊選擇把 GC 分代收集擴展至方法區,或者說使用永久代來實現方法區而已,這樣HotSpot的垃圾收集器就可以像管理Java堆一樣管理這部分內存,能夠省去專門爲方法區變編寫內存管理代碼的工作。不過對於其他虛擬機(如BEA JRockit、IBM J9等)來說並不存在永久代的概念

方法區存儲的是每個 class 的信息:

è¿éåå¾çæè¿°

畫外音:類型的常量池,也叫運行時常量池,每一個Class文件中,都維護着一個常量池(這個保存在類文件裏面,不要與方法區的運行時常量池搞混),裏面存放着編譯時期生成的各種字面值和符號引用;這個常量池的內容,在類加載的時候,被複制到方法區的運行時常量池 。

運行時常量池

除了保存已加載的類信息,還有一個特殊的部分——運行時常量池(Runtime Constant Pool)。

運行時常量是方法區一個特殊的部分,相對於常量來說的,它具備一個重要特徵是:動態性。也就是說,除了類加載時將常量池寫入其中,Java 程序運行期間也可以向其中寫入常量:

//使用StringBuilder在堆上創建字符串abc,再使用intern將其放入運行時常量池
String str = new StringBuilder("abc");
str.intern();

//直接使用字符串字面量xyz,其被放入運行時常量池
String str2 = "xyz";

常量池是爲了避免頻繁的創建和銷燬對象而影響系統性能,其實現了對象的共享。例如字符串常量池,在編譯階段就把所有的字符串文字放到一個常量池中。

  • 節省內存空間:常量池中所有相同的字符串常量被合併,只佔用一個空間。
  • 節省運行時間:比較字符串時,==比equals()快。對於兩個引用變量,只用==判斷引用是否相等,也就可以判斷實際值是否相等。

方法區的實現

方法區只是JVM規範定義,而永久代爲具體的實現,方法區的實現在虛擬機規範中並未明確規定,目前有2種比較主流的實現方式:

(1)HotSpot 虛擬機1.7-:在 JDK1.6 及之前版本,HotSpot 使用“永久代(permanent generation)”的概念作爲實現,即將 GC 分代收集擴展至方法區。這種實現比較偷懶,可以不必爲方法區編寫專門的內存管理,但帶來的後果是容易碰到內存溢出的問題(因爲永久代有 -XX:MaxPermSize 的上限)。

在 JDK1.7+ 之後,HotSpot 逐漸改變方法區的實現方式,如 1.7 版本移除了方法區中的字符串常量池,放到了堆中,符號引用(Symbols)轉移到了 Native Heap;字面量(interned strings)轉移到了 Java heap;類的靜態變量(class statics)轉移到了 Java heap。

畫外音:什麼是字符串常量池?

在 JAVA 語言中有8中基本類型和一種比較特殊的類型String。這些類型爲了使他們在運行過程中速度更快,更節省內存,都提供了一種常量池的概念。常量池就類似一個JAVA系統級別提供的緩存。

8種基本類型的常量池都是系統協調的,String類型的常量池比較特殊。它的主要使用方法有兩種:

  • 直接使用雙引號聲明出來的String對象會直接存儲在常量池中。
  • 如果不是用雙引號聲明的String對象,可以使用String提供的intern方法。intern 方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中。

(2)HotSpot 虛擬機 1.8+:1.8 版本中移除了方法區並使用 MetaSpace(元數據空間)作爲替代實現,MetaSpace 存儲類的元數據信息。MetaSpace 佔用系統內存,也就是說,只要不碰觸到系統內存上限,方法區會有足夠的內存空間。但這不意味着我們不對方法區進行限制,如果方法區無限膨脹,最終會導致系統崩潰

畫外音:什麼是類的元數據?

元數據是指用來描述數據的數據,更通俗一點,就是描述代碼間關係,或者代碼與其他資源(例如數據庫表)之間內在聯繫的數據。

在一些技術框架,如struts、EJB、hibernate就不知不覺用到了元數據。

對struts來說,元數據指的是struts-config.xml;對EJB來說,就是ejb-jar.xml和廠商自定義的xml文件;對hibernate來說就是hbm文件。

JDK 1.8 結構圖如下:

畫外音:直接內存是什麼?

它並不是虛擬機運行時的數據區的一部分。是在NIO中基於通道和緩衝區的I/O方式,使用Native函數庫直接分配堆外內存。避免了JAVA堆和Native堆中來回複製數據。和(操作系統中內存頁的用戶空間和系統空間的虛擬映象類似)

我們思考一個問題,爲什麼使用“永久代”並將 GC 分代收集擴展至方法區這種實現方式不好,會導致OOM?

首先要明白方法區的內存回收目標是什麼,方法區存儲了類的元數據信息和各種常量,它的內存回收目標理應當是對這些類型的卸載和常量的回收。

但由於這些數據被類的實例引用,卸載條件變得複雜且嚴格,回收不當會導致堆中的類實例失去元數據信息和常量信息。

因此,回收方法區內存不是一件簡單高效的事情,往往 GC 在做無用功。

另外隨着應用規模的變大,各種框架的引入,尤其是使用了字節碼生成技術的框架,會導致方法區內存佔用越來越大,最終 OOM。

兩者結構上的區別

爲什麼 JDK 1.8 要把方法區從 JVM 裏移到直接內存?

原因一:因爲直接內存,JVM將會在 IO 操作上具有更高的性能,因爲它直接作用於本地系統的 IO 操作。而非直接內存,也就是堆內存中的數據,如果要作 IO 操作,會先複製到直接內存,再利用本地 IO 處理。

  • 從數據流的角度,非直接內存是下面這樣的作用鏈:本地 IO --> 直接內存 --> 非直接內存 --> 直接內存 --> 本地 IO。
  • 而直接內存是:本地 IO --> 直接內存 --> 本地 IO。

原因二:整個永久代有一個 JVM 本身設置固定大小上線,無法進行調整,而元空間使用的是直接內存,受本機可用內存的限制,並且永遠不會得到 java.lang.OutOfMemoryError。

  • 可以使用 -XX:MaxMetaspaceSize 標誌設置最大元空間大小,默認值爲 unlimited,這意味着它只受系統內存的限制。
  • -XX:MetaspaceSize 調整標誌定義元空間的初始大小如果未指定此標誌,則 Metaspace 將根據運行時的應用程序需求動態地重新調整大小。

畫外音:而且應該爲 PermGen 分配多大的空間很難確定,因爲 PermSize 的大小依賴於很多因素,比如 JVM 加載的 class 總數,常量池的大小,方法的大小等。

原因三: PermGen 中類的元數據信息在每次 FullGC 的時候可能被收集,但成績很難令人滿意

原因四:官方文檔表示,移除永久代是爲融合 HotSpot JVM 與 JRockit VM 而做出的努力,因爲 JRockit 沒有永久代,不需要配置永久代。

 

PS:文章內容和圖片通過網上收集、整理、潤色,侵刪!

不管做什麼,只要堅持下去就會不一樣!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章