深入理解Java虛擬機(一):Java虛擬機內存區域介紹

引言

對於Java程序員來說,在虛擬機自動內存管理機制的幫助下,不再需要爲每一個new操作去寫配對的delete/free代碼,不容易出現內存泄漏和內存溢出問題,看起來由虛擬機管理內存一切都很美好。

不過,也正是因爲Java程序員把控制內存的權力交給了Java虛擬機,一旦出現內存泄漏和溢出方面的問題,如果不瞭解虛擬機是怎樣使用內存的,那排查錯誤、修正問題將會成爲一項異常艱難的工作。

本篇博客開始,將在《深入理解Java虛擬機:JVM高級特性與最佳實踐(第3版)》一書的引導下,開啓Java虛擬機學習之旅。

運行時數據區域介紹

Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分爲若干個不同的數據區域。這些區域有各自的用途,以及創建和銷燬的時間,有的區域隨着虛擬機進程的啓動而一直存在,有些區域則是依賴用戶線程的啓動和結束而建立和銷燬。

Java虛擬機所管理的內存將會包括以下幾個運行時數據區域,如下圖所示:
在這裏插入圖片描述

1. 程序計數器

關鍵詞:線程私有、唯一一個沒有規定任何OutOfMemoryError情況的區域

程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。

它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

由於Java虛擬機的多線程是通過線程輪流切換、分配處理器執行時間的方式來實現的。爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。

如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是本地(Native)方法,這個計數器值則應爲空(Undefined)。

2. Java虛擬機棧

關鍵詞:線程私有、描述java方法執行過程、存儲局部變量等信息

與程序計數器一樣,Java虛擬機棧(Java Virtual Machine Stack)也是線程私有的,它的生命週期與線程相同。

虛擬機棧描述的是Java方法執行的線程內存模型:每個方法被執行的時候,Java虛擬機都會同步創建一個棧幀用於存儲局部變量表、操作數棧、動態連接、方法出口等信息。每一個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

局部變量表存放了編譯期可知的各種Java虛擬機基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它並不等同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。

異常類型:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果Java虛擬機棧容量可以動態擴展,當棧擴展時無法申請到足夠的內存會拋出OutOfMemoryError異常。

3. 本地方法棧

關鍵詞:線程私有、描述Native方法執行過程

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的作用是非常相似的,其區別只是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的本地(Native)方法服務。

異常類型:與虛擬機棧一樣,本地方法棧也會在棧深度溢出或者棧擴展失敗時分別拋出StackOverflowError和OutOfMemoryError異常。

4. Java堆

關鍵詞:線程共享、描述Native方法執行過程

Java堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例。

從分配內存的角度看,所有線程共享的Java堆中可以劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB),以提升對象分配時的效率。

Java堆可以處於物理上不連續的內存空間中,但在邏輯上它應該被視爲連續的。

Java堆既可以被實現成固定大小的,也可以是可擴展的(通過參數-Xmx和-Xms設定)。

異常類型:如果在Java堆中沒有內存完成實例分配,並且堆也無法再擴展時,Java虛擬機將會拋出OutOfMemoryError異常。

5. 方法區

關鍵詞:線程共享、存儲常量/靜態變量等數據

別名叫作“非堆”(Non-Heap),目的是與Java堆區分開來。方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。

說到方法區,還會想到另一個名詞“永久代”,其實兩者並不是一個概念。準確地說,永久代最初是因爲希望能夠像垃圾收集器管理Java堆內存那樣而設計的,這樣可以省去專門爲方法區編寫內存管理代碼的工作,但這種設計導致了Java應用更容易遇到內存溢出的問題。

因此,JDK 7的HotSpot,已經把原本放在永久代的字符串常量池、靜態變量等移出,而到了JDK 8,終於完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地內存中實現的元空間(Meta-space)來代替,把JDK 7中永久代還剩餘的內容(主要是類型信息)全部移到元空間中。

異常類型:如果方法區無法滿足新的內存分配需求時,將拋出OutOfMemoryError異常。

5.1 運行時常量池

關鍵詞:屬於方法區、具有動態性

運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表(Constant Pool Table),用於存放編譯期生成的各種字面量與符號引用,這部分內容將在類加載後存放到方法區的運行時常量池中。

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

異常類型:既然運行時常量池是方法區的一部分,自然受到方法區內存的限制,當常量池無法再申請到內存時會拋出OutOfMemoryError異常。

總結

通過本篇博客的總結,Java虛擬機的內存區域我們已經很清楚了,每個區域都單獨標註了關鍵詞和對應可能發生的異常類型,也是有助於後期回顧。

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