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

寫在前面

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

概述

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

HotSpot 運行時數據區

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

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

線程私有區域

程序計數器(PROGRAM COUNTER REGISTER)

程序計數器是一塊很小的區域,它存儲的是當前線程正在執行的字節碼的地址(在這裏,其實有兩個「當前」,一個是:當前正在被 CPU 執行的線程,另外一個是:當前這個被執行的線程中正在被執行的字節碼指令)。字節碼解釋器工做時就是改變程序計數器的值來選取下一條須要執行的字節碼。對於單核心而言,多線程是經過線程輪流切換的方式實現的,在任一時刻只有一個線程可以獲得 CPU 的執行權從而執行線程中的字節碼指令,所以,爲了使線程切換後可以恢復到正在執行的字節碼的位置,每一個線程都須要擁有本身的程序計數器。面試

注意:程序計數器是惟一的一塊在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 的區域。因爲它是線程私有的,因此它的生命週期隨着線程的建立而建立,隨着線程的結束而死亡 。算法

虛擬機棧(VM STACK)

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

每一個方法在執行的時候都會建立一個棧幀(一個方法對應一個棧幀,棧幀即棧的基本單位)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法被線程執行從開始到結束,就對應着一個棧幀在虛擬機棧中入棧(壓棧)和出棧(彈棧)的過程。局部變量表中存放了編譯可知的各類基本數據類型(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 文件以表存儲數據)。

圖 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 虛擬機運行時數據區作了簡要的分析,關於內存管理模塊的其餘部分後續會繼續更新,敬請期待!

參考

公衆號

若是你們想要實時關注我更新的文章以及我分享的乾貨的話,能夠關注個人公衆號 咱們都是小白鼠。公衆號內有一些整理過的 原創精品腦圖,不只包含技術點的知識脈絡,更多的底層原理的梳理,目前涵蓋 Redis,RabbitMQ,Mysql,Java 虛擬機等 ,這些都是博主本身的學習筆記,整理的過程花費了不少心血,除此以外還有一些整理過的 面試題 以及平常開發經常使用到的一些 開發工具 等,在公衆號內分別回覆【技術腦圖】、【面試題】、【開發工具】便可獲取。

幹活分享

相關文章
相關標籤/搜索