本地方法棧、JVM棧、本地內存和JVM Heap的區別與關係

在Java出現之前,像C/C++這樣的編譯型語言寫出來的代碼經過編譯後,得到的是可直接在某平臺(Windows或Linux)上執行的機器碼,即machine code,machine code其實就是native code,它直接和操作系統交互。

對於內存,主要分三部分:

1)存儲可執行代碼(馮·諾依曼的存儲程序的思想),即編譯後的machine code;

2)用來保存代碼執行時用到的局部變量,即stack;

3)代碼執行時,動態找操作系統申請(最終要歸還給操作系統)的heap;

編譯型語言寫出現的程序,對於Heap的分配和歸還都是由程序代碼手工維護的。如下圖所示,寫一段C++代碼,GCC編譯後就成爲了可以在某具體平臺上運行的機器碼。Native的代碼和內存管理主要帶來兩個問題:一是編譯後的代碼無法跨平臺,畢竟是native的,只能支持被編譯平臺的操作系統API和指令集。二是堆空間無法自動GC,因爲內存管理是手工和操作系統交互,申請與釋放的內存的操作交給程序員來做,操作系統並不支持GC。

Java是一種解釋型語言,解釋型語言是相對於編譯型語言存在的,它的源代碼不是直接翻譯成機器語言,而是先翻譯成中間代碼,再由解釋器對中間代碼進行解釋運行。Java爲了解決以上兩個問題,它提出了虛擬機的思想,在原來的"Native Heap"裏大作文章。

JVM的ByteCode在任何平臺都是一樣的。所以到了某個具體的平臺,被特定平臺的JVM Runtime解釋成本平臺的machine code,得到可執行代碼,存儲到Native Code區,machine code運行起來之後就會用到Native Stack和Native Heap,這種把源代碼先翻譯成中間代碼(即ByteCode)再由解釋器解釋成機器碼供運行的模式,就實現了“Write Once,Run Anywhere”。這就解決了代碼無法跨平臺的問題。

因爲Native Heap中相當一部分內存是供Java應用程序存儲對象實例的,完全由JVM管理,就可以對JVM管理的Heap裏的數據的引用關係做記錄,然後用GC來自動釋放內存,這就解決了上面提到的堆空間無法自動GC的問題。所以一個Java進程啓動時,JVM向操作系統要的內存(-Xms與-Xmx),和程序向JVM要的內存是兩件不同的事情了。JVM Heap的內部結構與用什麼GC算法有關,比如對於傳統分代就是由Eden(包括S0與S1)、Tenured和PermGen組成。

被JVM管理的內存可以總體劃分爲兩部分:Heap Memory和Native Memory。前者我們比較熟悉,其實就是被分成新生代老年代等的JVM heap,是供Java應用程序使用的;後者也稱爲C-Heap,是供JVM自身進程使用的。Native Memory沒有相應的參數來控制大小,其大小依賴於操作系統進程的最大值(對於32位系統就是3~4G,各種系統的實現並不一樣)。

Native Memory裏存儲了什麼呢,主要是

  • JNI調用,也就是Native Stack;
  • JIT(即使編譯器)編譯時使用Native Memory,並且JIT的輸入(Java字節碼)和輸出(可執行代碼)也都是保存在Native Memory;
  • NIO direct buffer。對於IBM JVM和Hotspot,都可以通過-XX:MaxDirectMemorySize來設置nio直接緩衝區的最大值。默認是64M。超過這個時,會按照32M自動增大。
  • 用於保存類加載器和類信息的MetaSpace,在Native Memory中的。

本地方法棧就是Native Stack,與Java虛擬機棧所發揮的作用是非常相似的,其區別不過是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務。Navtive方法是Java通過JNI直接調用本地C/C++庫,可以認爲是Native方法相當於C/C++暴露給Java的一個接口,Java通過調用這個接口從而調用到C/C++方法。

JVM也是在不斷髮展的,永久代(PermGen區)是對JVM規範中方法區的實現,JDK 7的永久代就在JVM Heap中,與新生代老年代一起構成了JVM Heap。在HotSpot JVM中,永久代(PermGen區)用於存放類和方法的元數據以及常量池,比如Class和Method。每當一個類初次被加載的時候,它的元數據都會放到永久代(PermGen區)中。永久代(PermGen區)是有大小限制的,因此如果加載的類太多,很有可能導致永久代內存溢出,即
java.lang.OutOfMemoryError: PermGen,由於PermGen內存經常會溢出,因此JVM的開發者希望這一塊內存可以更靈活地被管理,不要再經常出現這樣的OOM。於是JDK 8開始把類的元數據放到本地堆內存(native heap)中,這一塊區域就叫Metaspace,中文名叫元空間。之前永久代的類的元數據存儲在新的元空間,原永久代的靜態變量以及運行時常量池則轉移到了JVM Heap中。

Metaspace空間的分配具有和JVM Heap相同的地址空間,使用本地內存有什麼好處呢?最直接的表現就是OOM問題將不復存在,本地內存剩餘多少理論上Metaspace就可以有多大(容量取決於是32位或是64位操作系統的可用虛擬內存大小),這解決了空間不足的問題。

如果從線程執行的角度,大概可以這麼理解。每個線程從JVM ByteCode開始執行,記錄JVM Stack和PC Register,並被解釋成Native Code,在Native Stack真正執行。這些線程共享一個JVM Heap,所以訪問共享數據時才需要加鎖保證安全。

在G1之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而G1打破了原有的分代模型,將堆劃分爲一個個區域。G1將堆分成許多相同大小的區域單元,每個單元稱爲Region,Region是一塊地址連續的內存空間。每個Region被標記了E、S、O和H,說明每個Region在運行時都充當了一種角色。其中H是以往算法中沒有的,它代表Humongous,這表示這些Region存儲的是巨型對象(humongous object,H-obj),當新建對象大小超過Region大小一半時,直接在新的一個或多個連續Region中分配,並標記爲H。

這麼劃分的目的是在進行收集時不必在全堆範圍內進行,這是它最顯著的特點。區域劃分的好處就是帶來了停頓時間可預測的收集模型:用戶可以指定收集操作在多長時間內完成。G1垃圾收集算法主要應用在多CPU大內存的服務中,在滿足高吞吐量的同時,儘可能地縮短垃圾回收時的暫停時間。

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