文章目錄
- 1. 運行時資料區域
- 1.1 程式計數器
- 1.2 Java虛擬機堆疊
- 1.3 本地方法堆疊
- 1.4 Java堆
- 1.5 方法區
- 1.6 運行時常量池
- 2. 直接記憶體
本文參考于《深入理解Java虛擬機》
1. 運行時資料區域
Java虛擬機在執行Java程式的程序中會把它所管理的記憶體劃分為若干個不同的資料區域,其包括:程式計數器、Java虛擬機堆疊、本地方法堆疊、Java堆和方法區,

1.1 程式計數器
(1)、什么是程式計數器?
程式計數器(Program Counter Register) 是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器,在物理層面上是由暫存器實作的,它用于存盤下一條所要執行的 JVM 指令的執行地址,
(2)、為什么需要程式計數器?
在Java虛擬機的概念模型里,位元組碼解釋器作業時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,它是程式控制流的指示器,分支、回圈、跳轉、例外處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成,它的核心作用就是:用于存盤下一條所要執行的 JVM 指令的執行地址,由執行引擎執行下一條JVM指令,
圖示說明

(3)、程式計數器的相關特點
- 它是一塊
很小的記憶體空間幾乎可以忽略不計,也是運行速度最快的存盤區域, - 在JVM規范中,每個執行緒都有它自己的程式計數器,是
執行緒私有的,生命周期與執行緒生命周期保持一致, - 如果執行緒正在執行的
是一個Java方法,這個計數器記錄的是正在執行的虛擬機位元組碼指令的地 址;如果正在執行的是本地(Native)方法,這個計數器值則應為空(Undefined), - 此記憶體區域是
唯一一個在《Java虛擬機規范》中沒有規定任何OutOfMemoryError情況的區域,
(4)、使用程式計數器存盤位元組碼指令地址有什么用?為什么使用程式計數器記錄當前執行緒的執行地址呢?
因為CPU需要不停的切換各個執行緒,這時候切換回來以后,就得知道接著從哪兒開始繼續執行,JVM的位元組碼解釋器就需要通過改變程式計數器的值來明確下一條應該執行什么樣的位元組碼指令,
(5)、程式計數器為什么是執行緒私有的?
為了執行緒切換后能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立存盤,我們稱這類記憶體區域為 “執行緒私有”的記憶體,
(6)、Java代碼的執行程序
- 首先
Java代碼會被轉換為JVM指令(二進制位元組碼,進而Java可以實作跨平臺運行) - 然后
解釋器會對JVM指令進行轉換,從而轉換為機器碼給CPU執行, - 與此同時,程式計數器的中的
值指向下一條需要執行的JVM指令的地址,
1.2 Java虛擬機堆疊
(1)、什么是Java虛擬機堆疊?
與程式計數器一樣,Java虛擬機堆疊(Java Virtual Machine Stack)也是執行緒私有的,它的生命周期與執行緒相同(即隨著執行緒的創建而創建,隨著執行緒的消亡而消亡),虛擬機堆疊描述的是Java方法執行的執行緒記憶體模型:每個方法被執行的時候,Java虛擬機都會同步創建一個堆疊幀(Stack Frame)用于存盤區域變數表、運算元堆疊、動態連接、方法出口等資訊,每一個方法被呼叫直至執行完畢的程序,就對應著一個堆疊幀在虛擬機堆疊中從入堆疊到出堆疊的程序,
(2)、為什么需要虛擬機堆疊?
每個方法被執行的時候,Java虛擬機都會同步創建一個堆疊幀(Stack Frame)用于存盤區域變數表、運算元堆疊、動態連接、方法出口等資訊,每一個方法被呼叫直至執行完畢的程序,就對應著一個堆疊幀在虛擬機堆疊中從入堆疊到出堆疊的程序,
(3)、虛擬機堆疊由什么組成?
虛擬機堆疊由一個又一個的堆疊幀組成,但是每個執行緒只有一個活動堆疊幀(即當前正在執行的方法),如果一個方法需要執行,則相應的堆疊幀需要入堆疊;如果一個方法執行結束,則相應的堆疊幀需要出堆疊,

一個堆疊幀包含以下幾部分:
-
區域變數表:區域變數表存放了編譯期可知的各種Java虛擬機
基本資料型別(boolean、byte、char、short、int、float、long、double)、物件參考(reference型別,它并不等同于物件本身,可能是一個指向物件起始地址的參考指標,也可能是指向一個代表物件的句柄或者其他與此物件相關的位置)和returnAddress型別(指向了一條位元組碼指令的地址),這些資料型別在區域變數表中的存盤空間以區域變數槽(Slot)來表示,其中64位長度的long和double型別的資料會占用兩個變數槽,其余的資料型別只占用一個,區域變數表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在堆疊幀中分配多大的區域變數空間是完全確定的,在方法運行期間不會改變區域變數表的小,請讀者注意,這里說的“大小”是指變數槽的數量,虛擬機真正使用多大的記憶體空間(譬如按照1個變數槽占用32個位元、64個位元,或者更多)來實作一個變數槽, -
運算元堆疊:也可以稱之為運算式堆疊(Expression Stack),在方法執行程序中,根據位元組碼指令,
往堆疊中寫入資料或提取資料,即入堆疊(push)和 出堆疊(pop),某些位元組碼指令將值壓入運算元堆疊,其余的位元組碼指令將運算元取出堆疊,使用它們后再把結果壓入堆疊,比如:執行復制、交換、求和等操作,運算元堆疊,主要用于保存計算程序的中間結果,同時作為計算程序中變數臨時的存盤空間,

- 動態鏈接
- 方法出口等資訊
(4)、該區域常出現的兩種例外狀況
- StackOverflowError例外:如果Java虛擬機堆疊的
容量不可以動態拓展,執行緒請求的堆疊深度大于虛擬機所允許的深度,將拋出StackOverflowError例外, - OutOfMemoryError例外:如果Java虛擬機堆疊的
容量可以動態拓展,當堆疊擴展時無法申請到足夠的記憶體會拋出OutOfMemoryError例外
(5)、Java方法的結束方式
- return陳述句
- 拋出例外
1.3 本地方法堆疊
(1)、什么是本地方法堆疊?
Java虛擬機堆疊為虛擬機執行Java方法(也就是位元組碼)服務,而本地方法堆疊則是為虛擬機使用到的本地(Native)方法服務的,
補充概念
本地方法:當Java虛擬機需要和作業系統底層進行互動的時候需要呼叫C或者C++方法,而所呼叫的C或者C++方法就是本地方法,

(2)、本地方法堆疊的作用
和Java虛擬機堆疊作用類似,本地方法堆疊用于存放相關的本地方法所被呼叫產生的堆疊幀,當一個本地方法被呼叫時,相應的堆疊幀入堆疊;當一個本地方法呼叫結束時,相應的堆疊幀出堆疊,而本地方法堆疊的堆疊幀存放的是區域變數表、運算元堆疊、動態鏈接和方法出口等資訊,
(3)、本地方法堆疊可能出現的例外狀況
本地方法堆疊也會在堆疊深度溢位或者堆疊擴展失敗時分別拋出StackOverflowError和OutOfMemoryError例外
1.4 Java堆
(1)、什么是Java堆?
Java堆(Java Heap) 是虛擬機所管理的記憶體中最大的一塊,Java堆是被所有執行緒共享的一塊記憶體區域,所以此處的物件會涉及執行緒安全問題,在虛擬機啟動時創建,此記憶體區域的唯一目的就是存放物件實體,Java世界里“幾乎”所有的物件實體都在這里分配記憶體,
幾乎一詞的說明
而這里筆者寫的“幾乎”是指從實作角度來看,隨著Java語言的發展,現在已經能看到些許跡象表明日后可能出現值型別的支持,即使只考慮現在,由于
即時編譯技術的進步,尤其是逃逸分析技術的日漸強大,堆疊上分配、標量替換優化手段已經導致一些微妙的變化悄然發生,所以說Java物件實體都分配在堆上也漸漸變得不是那么絕對了,
(2)、GC堆說明
Java堆是垃圾收集器管理的記憶體區域,因此一些資料中它也被稱作“GC堆”,從回收記憶體的角度看,由于現代垃圾收集器大部分都是基于分代收集理論設計的,所以Java堆中經常會出現“新生代”“老年代”“永久代”“Eden空間”“From Survivor空間”“To Survivor空間”等名詞,這些區域劃分僅僅是一部分垃圾收集器的共同特性或者說設計風格而已,而非某個Java虛擬機具體實作的固有記憶體布局,更不是《Java虛擬機規范》里對Java堆的進一步細致劃分,

(3)、常見的例外:OutOfMemoryError
Java堆既可以被實作成固定大小的,也可以是可擴展的,不過當前主流的Java虛擬機都是按照可擴
展來實作的(通過引數-Xmx和-Xms設定),如果在Java堆中沒有記憶體完成實體分配,并且堆也無法再擴展時,Java虛擬機將會拋出OutOfMemoryError例外,
1.5 方法區
(1)、什么是方法區?
方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用于存盤已被虛擬機加載
的型別資訊、常量、靜態變數、即時編譯器編譯后的代碼快取等資料,
(2)、方法區的實作方式
方法只是一個抽象概念,只是定義了相關的規范,就好比方法區是抽象類,定義了相應的規范,而它的實作方式(永久代或者元空間)就好比是實作了該抽象類的子類,
- 永久代(JDK1.7之前)

- 元空間(JDK1.8之后)

(3)、永久代到元空間的變化
到了JDK 7的HotSpot,已經把原本放在永久代的字串常量池、靜態變數等移出到堆中,而到了JDK 8,終于完全廢棄了永久代的概念,改用與JRockit、J9一樣在本地記憶體中實作的元空間(Metaspace)(主要是型別資訊)來代替,
(4)、為什么要將永久代改換為元空間?
當年使用永久代來實作方法區的決定并不是一個好主意,這種設計導致了Java應用更容易遇到 記憶體溢位的問題(永久代有-XX:MaxPermSize的上限,即使不設定也有默認大小),無法進行調整;而元空間使用的是直接記憶體(作業系統的記憶體),雖然還是可能會導致元空間記憶體溢位,但是概率變低了,
元空間溢位時的錯誤:
java.lang.OutOfMemoryError: MetaSpace
1.6 運行時常量池
(1)、什么是運行時常量池?
運行時常量池(Runtime Constant Pool) 是方法區的一部分,Class檔案中除了有類的版本、字 段、方法、介面等描述資訊外,還有一項資訊是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號參考,這部分內容將在類加載后存放到方法區的運行時常量池中,
(2)、常見例外狀況
既然運行時常量池是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法再申請到記憶體 時會拋出OutOfMemoryError例外,
(3)、運行時常量池的位置
- JDK1.7之前,方法區是由永久代實作的,所以
運行時常量池也位于永久代內部, - JDK1.7時,方法區中的運行時常量池中
字串常量池從方法區移出到堆中,而運行時常量池的剩余部分還留在方法區中,方法區的實作方式為永久代, - JDK1.8時,
永久代被元空間替代,而此時字串常量池還留在堆中,運行時常量池還留在方法區中,
所以運行時常量池一直處于方法區中,只不過方法區的實作方式發生了改變,同時隨著實作方式發生了改變,它的實際物理地址由堆中轉移到了本地記憶體中,
2. 直接記憶體
(1)、什么是直接記憶體?
直接記憶體(Direct Memory) 并不是虛擬機運行時資料區的一部分,也不是《Java虛擬機規范》中
定義的記憶體區域,但是這部分記憶體也被頻繁地使用,而且也可能導致OutOfMemoryError例外出現,本機直接記憶體的分配不會受到 Java 堆的限制,但是,既然是記憶體就會受到本機總記憶體大小以及處理器尋址空間的限制,
(2)、新引入的NIO類
在JDK 1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區 (Buffer)的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然后通過一個存盤在Java堆里面的DirectByteBuffer物件作為這塊記憶體的參考進行操作,這樣能在一些場景中顯著提高性能,因為避免了在Java堆和Native堆中來回復制資料,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/356107.html
標籤:java
