深入理解JVM內存模型(運行時數據區域)

Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分爲若干個不同的數據區域。這些區域有各自的用途,以及創建和銷燬的時間,有的區域隨着虛擬機進程的啓動而一直存在,有些區域則是依賴用戶線程的啓動和結束而建立和銷燬。根據《Java虛擬機規範》的規定,Java虛擬機所管理的內存將會包括以下幾個運行時數據區域,如圖所示。
JVM內存模型

JVM內存模型指的是Java虛擬機在運行時所管理的內存的模型;
Java內存模型指的是Java內存規範概念,實際並不存在。

程序計數器

一句話介紹: 程序控制流的指示器。線程私有。

程序計數器是一塊較小的內存空間。它可以看做是當前程序執行所指向的行號指示器。在Java虛擬機的概念模型裏,字節碼解釋器在工作時就是通過改變這個計數器的值來獲取下個執行的指令。分支、循環、跳轉、異常、線程恢復等基礎功能都需要依賴這個計數器來完成。
程序計數器是線程私有的。

Java的線程執行是搶佔式的方式,因此在任何一個確定時刻、一個處理器(對於多核處理器而言是一個內核)只會執行一個線程中的指令。爲了在線程切換後能夠恢復正常運行,每一個線程都需要一個獨立的程序計數器。
各個線程的計數器獨立工作,互不影響。

如果當前執行的是Java方法,程序計數器記錄的是當前執行的指令的地址;
如果當前執行的Native方法,程序計數器的值爲空(Undefined)。

程序計數器所在的區域是《Java虛擬機規範》中唯一一個沒有規定任何OutOfMemeryError異常的區域。

Java虛擬機棧

一句話介紹:Java方法執行的線程模型。線程私有。

一個Java方法對應一個棧楨;每執行一個方法,Java虛擬機都會創建一個對應的棧楨,用於存在局部變量表、操作數棧、動態連接、方法出口等信息。每一個方法被調用直至執行完畢的過程,就對應一個棧楨從入棧到出棧的過程。

棧楨是方法執行時非常重要的基礎數據結構,以後有重點介紹。

在《Java虛擬機規範中》此內存區域規定了兩個內存異常:
線程請求的棧深度大於虛擬機所允許的最大深度,則拋出StackOrderFlowError異常;
如果虛擬機支持棧深度動態擴展,則當棧無法進行擴展時,拋出OutOfMemeryError異常。

本地方法棧

一句話介紹:在使用上與Java虛擬機一樣,只不過服務於Native方法。

本地方法棧只服務於Native方法,《Java虛擬機規範》對於本地方法棧中的方法使用的語言、使用的方式以及數據結構任何規定。但StackOverFlowError和OutOfMemeryError異常規定和虛擬機棧是一樣的。

Java堆

一句話介紹:Java堆是虛擬機管理的最大的一塊內存,所有線程共享,虛擬機啓動時創建。

在Java的世界裏,“幾乎”所有的對象實例都在這裏分配。《Java虛擬機規範》中對Java堆的描述是:“所有的對象實例以及數組都應該分配在堆內存中”。而我們說的“幾乎”是指從現實的角度來看,隨着Java技術的發展,已經有一些跡象表明日後可能會出現值類型的支持;即使是現在,由於及時編譯器的進步,尤其是逃逸分析技術日漸強大,站上分配、標量替換已經導致一些微妙的變化正在悄然發生。因此,所有的對象實例都分配在堆內存上也漸漸地變得不是那麼絕對了。

及時編譯器的優化、逃逸分析技術等都是近代Java技術發展的重要里程碑。以後有重點介紹。

從內存回收的角度來看,由於經典的垃圾回收器都是基於分代理論設計的,因此Java堆中經常會出現一些名詞:年輕代、老年代、永久代、Eden區、Surviver區等。
在G1垃圾收集器出現之前,由於垃圾收集器都是基於分代理論實現的,需要年輕代、老年代垃圾收集器配合工作,因此以上名詞劃分非嚴格意義上來說是正確的。但是隨着垃圾收集器技術的發展,從G1垃圾收集器的誕生開始,以上的說法就需要區別對待了!

G1垃圾收集器:垃圾回收技術的重要里程碑,因爲它沒有采取經典的分代理論而是採取了基於Region理念設計的,這是從底層設計思想上的轉變,因此說它是重要里程碑。
沒有采取經典的分代理論,並不是說G1垃圾收集中沒有年輕代、老年代的說法,以後有重點介紹。

從內存分配的角度來看,通常情況下,對象實例是直接分配在Java堆中的,但分配的方式有兩種:指針碰撞、空閒列表。

指針碰撞
假設Java堆內存是絕對規整的,使用過的內存在一邊,未使用過的內存在另一邊,中間使用一個指示器作爲分界點,那麼在分配對象內存時,僅僅需要將指示器向未使用的一邊挪動與對象實例大小相等的距離即可,這種方式稱爲“指針碰撞”。

空閒列表
如果已使用內存和未使用內存是交互在一起的,那麼就沒有辦法使用“指針碰撞”的方式來分配內存了,虛擬機必須維護一個列表,用於記錄哪些內存是可用的,在分配內存時,從列表中找到可用的內存空間分配給對象實例,並更新列表的可用空間。這種方式就稱爲“空閒列表”。

Java堆內存大小設置
Java堆既可以被實現成固定大小的,也可以是可擴展的,不過當前主流的Java虛擬機都是按照可擴展來實現的(通過參數-Xmx和-Xms設定)。如果在Java堆中沒有內存完成實例分配,並且堆也無法再擴展時,Java虛擬機將會拋出OutOfMemoryError異常。

方法區

一句話介紹:用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。線程共享。

方法區還有一個別名-非堆(Non-Heap)。目的是用於和堆區分開來。

方法區和永久代

在JDK8以前,很多人都將方法區稱呼爲永久代,將二者混爲一談。其實方法區和永久代本質上是完全不同的概念。永久代的概念是在JDK8以前,HotSpot虛擬機實現方法區的一種方式。目的是爲了將分代垃圾收集擴展時方法區從而省去爲專門爲方法區的內存管理的代碼工作。但是其他虛擬機入:J9、JRockit等就沒有永久代的概念。

回過頭來看,HotSpot虛擬機採用永久代來實現方法區並不是一個好主意,這種設計導致了方法區大小受限(永久代有-XX:MaxPermSize的上限),同時使得Java應用更容易出現內存溢出的風險。因此到了JDK8,HotSpot虛擬機完全放棄了永久代,與J9、JRockit虛擬機一樣,採用元空間(Meta Data)來代替。

方法區的垃圾回收

《JVM虛擬機規範》對方法區的約束是非常寬鬆的。除了和Java堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,甚至還可以選擇不實現垃圾收集。
事實上,垃圾收集在這個區域的的效果是非常不理想的,尤其是類型的卸載,條件是非常苛刻的。但是對這個區域的收回也是有一定的必要性(Sun公司的Bug列表證實了這一點)。很糾結!

類型回收的條件:
1.當前類型已經沒有任何實例存在;
2.當前類型的類加載器已經被卸載;
3.當前類型的Class對象沒有被任何地方使用。
要同時滿足以上三個條件,真的很難,尤其是第二條。

運行時常量池

一句話介紹:運行時常量池是方法區中的一部分,用於存儲Class文件編譯手產生的給中符號引用和字面量。

運行時常量池相對於Class文件常量池的另外一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是說,並非預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可以將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的intern()方法。
既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出OutOfMemoryError異常。

直接內存

在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆裏面的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,因爲避免了在Java堆和Native堆中來回複製數據。

一般服務器管理員配置虛擬機參數時,會根據實際內存去設置-Xmx等參數信息,但經常忽略掉直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操作系統級的限制),從而導致動態擴展時出現OutOfMemoryError異常。

總結

到這裏,Java虛擬機運行時的數據區域(JVM內存模型)就學習完畢。

參考文章
《深入理解Java虛擬機:JVM高級特性與最佳實踐》
特別說明:本文沒有任何商業目的,僅供交流。

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