阿里面試常問的 Java 虛擬機運行時數據區

寫在前面


本文描述的有關於 JVM 的運行時數據區是基於 HotSpot 虛擬機。

概述

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

HotSpot 運行時數據區

運行時數據區在 HotSpot 1.8 之前的版本和 1.8 版本有所不同,主要是 方法區移到元空間 了。

阿里面試常問的 Java 虛擬機運行時數據區

圖 1-1:JDK1.8 之前 JVM 運行時數據區

阿里面試常問的 Java 虛擬機運行時數據區

圖 1-2:JDK1.8 JVM 運行時數據區

線程私有區域

程序計數器(PROGRAM COUNTER REGISTER)

程序計數器是一塊很小的區域,它存儲的是當前線程正在執行的字節碼的地址(在這裡,其實有兩個“當前”,一個是:當前正在被 CPU 執行的線程,另一個是:當前這個被執行的線程中正在被執行的字節碼指令)。字節碼解釋器工作時就是改變程序計數器的值來選取下一條需要執行的字節碼。對於單核心而言,多線程是通過線程輪流切換的方式實現的,在任一時刻只有一個線程能夠得到 CPU 的執行權從而執行線程中的字節碼指令,因此,為了使線程切換後能夠恢復到正在執行的字節碼的位置,每個線程都需要擁有自己的程序計數器。

注意:程序計數器是唯一的一塊在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 的區域。由於它是線程私有的,所以它的生命週期隨著線程的創建而創建,隨著線程的結束而死亡 。

虛擬機棧(VM STACK)

虛擬機棧也是線程私有的,所以它的生命週期與程序計數器相同。虛擬機棧描述的是 Java 方法執行的內存模型。

每個方法在執行的時候都會創建一個棧幀(一個方法對應一個棧幀,棧幀即棧的基本單位)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法被線程執行從開始到結束,就對應著一個棧幀在虛擬機棧中入棧(壓棧)和出棧(彈棧)的過程。局部變量表中存放了編譯可知的各種基本數據類型(byte,short,int,long,float,double,char,boolean)、對象引用(reference 類型,它存儲的是:對象的地址或者是指向代表對象的句柄)。

Java 虛擬機規範中規定了虛擬機棧可能出現的兩種異常狀況:StackOverflowError 和 OutOfMemoryError。

StackOverflowError: 若當線程請求棧的深度超過當前 Java 虛擬機棧的最大深度的時候會拋出 StackOverflowError。

OutOfMemoryError: 若虛擬機棧動態擴展過程中,如果線程請求申請棧空間無法申請到足夠的內存,就會拋出 OutOfMemoryError。

本地方法棧(NATIVE METHOD STACK)

本地方法棧與虛擬機棧類似,虛擬機棧是執行 Java 方法開闢的內存空間,而本地方法棧是執行 Native 方法開闢的內存空間。

與虛擬機棧一樣,本地方法棧也會拋出 StackOverflowError 和 OutOfMemoryError 異常,拋出條件也是類似的。

線程間共享的內存區域

堆(HEAP)

堆是所有線程共享的一塊區域,主要用來存放對象和數組。

在 Java 虛擬機規範中有描述:所有的對象實例和數組都要在堆上分配,但是 隨著 JIT(JUST-IN-TIME)編譯器的發展與逃逸分析技術的逐漸成熟,並不是所有對象都只在堆上分配了,比如:隨著逃逸分析技術的逐漸成熟,在即時能被回收的對象也有可能會在虛擬機棧上分配。

由於現在都採用分代回收算法,所以從內存回收的角度來看,堆還可以細分為:新生代、老年代。新生代又可以分為:Eden 空間、From Survivor 空間、To Survivor 空間。

注意:1.8 中已經徹底將方法區的實現由之前的永久代改為元空間。

方法區(METHOD AREA)

方法區和堆一樣也是所有線程共享的一塊區域,主要用來存儲已經被虛擬機加載的類信息、常量(final 修飾的)、靜態變量、即時編譯器(JIT)編譯後產生的代碼等數據。雖然 Java 虛擬機規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。

永久代就是方法區域?

早些時候,很多開發者更願意稱方法區為“永久代”。其實“永久代”這個稱呼的由來是因為 HotSpot 團隊並不打算為方法區重新設計垃圾回收算法,為了在方法區中能夠沿用堆中的分代回收算法,所以按照堆中的命名方式,將方法去稱為“永久代”。對於 JRocket、J9 而言是不存在“永久代”的概念的,所以當 HotSpot 1.8 和 JRocket 合併時,就徹底放棄了“永久代”的概念(其實從 1.7 就已經開始了),取而代之是元空間,元空間使用的是直接內存。

方法區的垃圾回收很困難!!!

由於 Java 虛擬機規範對方法區的限制非常松,甚至可以不實現垃圾回收,一般而言,這個區域的內存回收很不令人滿意,尤其是類型的卸載,條件非常苛刻,但是由於現代框架大量的依賴於 JIT 技術,導致方法區的佔用比逐漸提高,所以對於方法區的回收至關重要。根據 Java 虛擬機規範規定,當方法區無法滿足內存分配需求時,將拋出 OutOfMemoryError 異常。

運行時常量池(RUNTIME CONSTANT POOL)

JDK1.7 及之後版本的 JVM 已經將運行時常量池從方法區中移了出來,在 Java 堆(Heap)中開闢了一塊區域存放運行時常量池。

這塊區域在 1.7 之前原來是方法區的一部分,Class 文件中有一項信息是常量池(或者說是一張常量表,Class 文件以表存儲數據)。

阿里面試常問的 Java 虛擬機運行時數據區

圖 1-3:Class 文件常量池


運行時常量池存儲的東西較為複雜,主要分為字面量和符號引用

字面量

存放的字面量主要包括 常量(final 修飾的),比如:final int x = 1、靜態變量(static 修飾的),還有一些其他的字面量。

符號引用

符號引用主要包括:類的完全限定名、字段名稱和描述符、方法名稱和描述符,包括很多符號,比如:() 也可以看做符號引用。

字面量和符號引用將在類加載(ClassLoader 加載 Class 字節碼文件)後進入方法區的運行時常量池中存放。不過,除了保存 Class 文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。運行時常量池相對於 Class 文件常量池一個重要的特徵就是具備動態性,Java 語言並不要求常量一定產生於編譯期的 Class 文件的常量池中,也並不是只有 Class 文件常量池中的常量才能夠進入運行時常量池中,在線程執行方法的過程當中可能產生新的常量存放到運行時常量池中,例如:String 類的 intern() 方法。當運行時常量池無法申請到內存的時候就會拋出 OutOfMemoryError 異常。

直接內存(DIRECT MEMORY)

直接內存並不是 JVM 運行時數據區的一部分,也不是虛擬機規範中定義的內存區域,但是這部分內存也被頻繁地使用。而且也可能導致 OutOfMemoryError 錯誤出現。

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

本機直接內存的分配不會受到 Java 堆的限制,但是,既然是內存就會受到本機總內存大小以及處理器尋址空間的限制。

總結

Java 虛擬機包含的內容很多,本篇文章也只是對 Java 內存管理模塊的 Java 虛擬機運行時數據區做了簡要的分析,關於內存管理模塊的其他部分後續會繼續更新,敬請期待!


作者:我們都是小白鼠
鏈接:https://juejin.im/post/5e8df0cd6fb9a03c31761d8c


分享到:


相關文章: