本文從概念上介紹 Java 虛擬機記憶體的各個區域,講解這些區域的作用、服務物件以及其中可能產生的問題,
Java 虛擬機在執行 Java 程式的程序中會把它所管理的記憶體劃分為若干個不同的資料區域,這些區域有各自的用途,以及創建和銷毀的時間,有些區域隨著虛擬機行程的啟動而一直存在,有些區域則是依賴用戶執行緒的啟動和結束而建立和銷毀,
根據《Java 虛擬機規范》的規定, Java 虛擬機所管理的記憶體將會包括以下幾個運行時資料區域:程式計數器、Java 虛擬機堆疊、本地方法堆疊、Java 堆、方法區,

程式計數器
程式計數器(Program Counter Register)是一塊較小的記憶體空間,程式計數器可以看作是當前執行緒所執行的位元組碼的行號指示器,在 Java 虛擬機的概念模型里,位元組碼解釋器作業時就是通過改變程式計數器的值來選取下一條需要執行的位元組碼指令,程式計數器是程式控制流的指示器,分支、回圈、跳轉、例外處理、執行緒恢復等基礎功能都需要依賴程式計數器來完成,
“概念模型”這個詞會經常被提及,它代表了所有虛擬機的統一外觀,但各款具體的 Java 虛擬機并不一定要完全照著概念模型的定義來進行設計,具體的 Java 虛擬機可能會通過一些更高效率的等價方式去實作它,
由于 Java 虛擬機的多執行緒是通過執行緒輪流切換、分配處理器執行時間的方式來實作的,在任何一個確定的時刻, 一個處理器(對于多核處理器來說是一個內核)都只會執行一個執行緒中的指令,因此,為了執行緒切換后能恢復到正確的執行位置,每個執行緒都需要有一個獨立的程式計數器,各個執行緒之間的程式計數器互不影響,獨立存盤,我們稱這類記憶體區域為 “執行緒私有” 的記憶體,
如果執行緒正在執行的是一個 Java 方法, 程式計數器記錄的是正在執行的虛擬機位元組碼指令的地址; 如果執行緒正在執行的是本地(Native) 方法,程式計數器值則應為空(Undefined),
程式計數器記憶體區域是唯一一個在《Java 虛擬機規范》中沒有規定任何 OutOfMemoryError 情況的區域,
Java 虛擬機堆疊
Java 虛擬機堆疊(Java Virtual Machine Stack)與程式計數器一樣,也是執行緒私有的記憶體區域,Java 虛擬機堆疊的生命周期與執行緒相同,
Java 虛擬機堆疊描述的是 Java 方法執行的執行緒記憶體模型:每個方法被執行的時候,Java 虛擬機都會同步創建一個堆疊幀(Stack Frame)用于存盤區域變數表、運算元堆疊、動態連接、方法出口等資訊,每個方法被呼叫直至執行完畢的程序,就對應著一個堆疊幀在 Java 虛擬機堆疊中從入堆疊到出堆疊的程序,
每一個堆疊幀中分配多少記憶體基本上是在類結構確定下來時就已知的(盡管在運行期會由即時編譯器進行一些優化, 但在基于概念模型的討論里,大體上可以認為是編譯期可知的)
區域變數表
區域變數表存放了編譯期可知的各種 Java 虛擬機基本資料型別(boolean、 byte、 char、 short、 int、float、 long、 double) 、物件參考(reference 型別,物件參考并不等同于物件本身,物件參考可能是一個指向物件起始地址的參考指標,也可能是指向一個代表物件的句柄或者其他與此物件相關的位置)和 returnAddress 型別(指向了一條位元組碼指令的地址),
這些資料型別在區域變數表中的存盤空間以區域變數槽(Slot)來表示, 其中 64 位長度的 long 和 double 型別的資料會占用兩個變數槽,其余的資料型別只占用一個變數槽,區域變數表所需的記憶體空間在編譯期間完成分配,當進入一個方法時, 這個方法需要在堆疊幀中分配多大的區域變數空間是完全確定的,在方法運行期間區域變數表的大小不會改變,
請讀者注意,這里說的 “大小” 指的是變數槽的數量,虛擬機真正使用多大的記憶體空間(譬如按照 1 個變數槽占用 32 個位元、 64 個位元, 或者更多)來實作一個變數槽,這是完全由具體的虛擬機實作自行決定的事情,
在《Java 虛擬機規范》中, 對 Java 虛擬機堆疊記憶體區域規定了兩類例外狀況:StackOverflowError、OutOfMemoryError
- 如果執行緒請求的堆疊深度大于虛擬機所允許的深度, 將拋出 StackOverflowError 例外(堆疊深度溢位例外);
- 如果 Java 虛擬機堆疊容量可以動態擴展,當堆疊擴展時無法申請到足夠的記憶體會拋出 OutOfMemoryError 例外,
通過引數 -Xss 來設定單個執行緒堆疊的大小,堆疊的大小直接決定了函式呼叫的最大深度,
HotSpot 虛擬機的堆疊容量是不可以動態擴展的,以前的 Classic 虛擬機倒是可以,所以在 HotSpot 虛擬機上是不會由于虛擬機堆疊無法擴展而導致 OutOfMemoryError 例外,只要執行緒申請堆疊空間成功了就不會有 OOM,但是如果執行緒申請堆疊空間失敗了,仍然是會出現 OOM 例外的,
本地方法堆疊
本地方法堆疊(Native Method Stacks)與虛擬機堆疊所發揮的作用非常相似,它們兩個的區別是:虛擬機堆疊為虛擬機執行 Java 方法(也就是位元組碼) 服務,而本地方法堆疊則是為虛擬機使用到的本地(Native)方法服務,
《Java 虛擬機規范》對本地方法堆疊中方法使用的語言、使用方式與資料結構并沒有任何強制規定,因此具體的虛擬機可以根據需要自由實作它,甚至有的 Java 虛擬機(譬如 HotSpot 虛擬機) 直接就將本地方法堆疊和虛擬機堆疊合二為一,
與虛擬機堆疊一樣,本地方法堆疊也會在堆疊深度溢位或者堆疊擴展失敗時分別拋出 StackOverflowError 和 OutOfMemoryError 例外,
Java 堆
Java 堆是一塊被所有執行緒共享的記憶體區域,Java 堆在虛擬機啟動時被創建,
Java 堆記憶體區域的唯一目的就是存放物件實體,Java 世界里 “幾乎” 所有的物件實體都在 Java 堆分配記憶體,
在《Java 虛擬機規范》中對 Java 堆的描述是:“所有的物件實體以及陣列都應當在堆上分配”,而這里筆者寫的“幾乎”是指從實作角度來看,隨著 Java 語言的發展,現在已經能看到些許跡象表明日后可能出現值型別的支持,即使只考慮現在,由于即時編譯技術的進步,尤其是逃逸分析技術的日漸強大,堆疊上分配、標量替換優化手段已經導致一些微妙的變化悄然發生,所以說 Java 物件實體都分配在堆上也漸漸變得不是那么絕對了,
根據《Java 虛擬機規范》的規定,Java 堆可以處于物理上不連續的記憶體空間中,但在邏輯上它應該被視為連續的,這點就像我們用磁盤空間去存盤檔案一樣,并不要求每個檔案都連續存放,但對于大物件(典型的如陣列物件),多數虛擬機實作出于實作簡單、存盤高效的考慮,很可能會要求連續的記憶體空間,
Java 堆既可以被實作成固定大小的,也可以是可擴展的,不過當前主流的 Java 虛擬機都是按照可擴展來實作的(通過引數 -Xmx 和 -Xms 設定), 如果 Java 堆無法滿足新的記憶體分配需求,并且堆也無法再擴展時,Java 虛擬機將會拋出 OutOfMemoryError 例外,
固定大小的 Java 堆指的是:只在虛擬機啟動時,向作業系統申請固定大小的堆記憶體空間,
可擴展的 Java 堆指的是:在虛擬機啟動時,向作業系統申請固定大小的初始堆記憶體空間,在空閑的 Java 堆記憶體空間無法滿足新的記憶體分配需求時,再向作業系統申請堆記憶體空間,
方法區
方法區(Method Area)與 Java 堆一樣, 也是被所有執行緒共享的記憶體區域,
方法區用于存盤已被虛擬機加載的型別資訊(如類名、訪問修飾符、欄位描述、方法描述等)、常量、靜態變數、即時編譯器編譯后的代碼快取等資料,
雖然《Java 虛擬機規范》中把方法區描述為堆的一個邏輯部分,但是方法區它卻有一個別名叫作 “非堆”(Non-Heap) ,目的是與 Java 堆區分開來,
根據《Java 虛擬機規范》的規定,如果方法區無法滿足新的記憶體分配需求時,Java 虛擬機將會拋出 OutOfMemoryError 例外,
《Java 虛擬機規范》對方法區的約束是非常寬松的,除了和 Java 堆一樣不需要連續的記憶體和可以選擇固定大小或者可擴展外,甚至還可以選擇不實作垃圾收集,相對而言,垃圾收集行為在方法區這個區域的確是比較少出現的,但并非資料進入了方法區就如永久代的名字一樣 “永久” 存在了,方法區這個區域的記憶體回收目標主要是針對常量池的回收和對型別的卸載, 一般來說方法區這個區域的回收效果比較難令人滿意,尤其是型別的卸載,條件相當苛刻,但是方法區這個區域的回收有時又確實是必要的, 以前 Sun 公司的 Bug 串列中,曾出現過的若干個嚴重的 Bug 就是由于低版本的 HotSpot 虛擬機對方法區這個區域未完全回收而導致記憶體泄漏,
永久代
說到方法區,不得不提一下 “永久代” 這個概念,尤其是在 JDK8 以前,許多 Java 程式員都習慣在 HotSpot 虛擬機上開發、部署程式,很多人都更愿意把方法區稱為 “永久代”(Permanent Generation),或者將這兩者(方法區、永久代)混為一談,本質上這兩者(方法區、永久代)并不是等價的,因為僅僅是當時的 HotSpot 虛擬機設計團隊選擇把垃圾收集器的分代設計擴展至方法區,或者說使用永久代來實作方法區而已, 這樣使得 HotSpot 的垃圾收集器能夠像管理 Java 堆一樣管理方法區這部分記憶體,省去專門為方法區撰寫記憶體管理代碼的作業,但是對于其他的虛擬機實作, 譬如 BEA JRockit、IBM J9 等來說,是不存在永久代這個概念的,
原則上如何實作方法區屬于虛擬機的實作細節,不受《Java 虛擬機規范》管束, 并不要求統一,但現在回頭來看,當年使用永久代來實作方法區的決定并不是一個好主意,這種設計導致了 Java 應用更容易遇到記憶體溢位的問題(永久代有 -XX:MaxPermSize 的上限,即使不設定也有默認大小,而 J9 和 JRockit 只要沒有觸碰到行程可用記憶體的上限, 例如32位系統中的4GB限制, 就不會出問題) ,而且有極少數方法(例如String::intern()) 會因永久代的原因而導致不同虛擬機下有不同的表現,
當 Oracle 收購 BEA 獲得了JRockit 的所有權后, 準備把 JRockit 中的優秀功能,譬如 Java Mission Control 管理工具, 移植到 HotSpot 虛擬機時,但因為兩者對方法區實作的差異而面臨諸多困難,
考慮到 HotSpot 未來的發展,在 JDK6 的時候 HotSpot 開發團隊就有放棄永久代,逐步改為采用本地記憶體(Native Memory)來實作方法區的計劃了,到了 JDK7 的 HotSpot,已經把原本放在永久代的字串常量池、靜態變數等移出, 而到了 JDK8 , 終于完全廢棄了永久代的概念, 改用與 JRockit、J9 一樣在本地記憶體中實作的元空間(Metaspace)來代替,把 JDK7 中永久代還剩余的內容(主要是型別資訊) 全部移到元空間中,
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分,
Class 檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池表(Constant Pool Table),常量池表用于存放編譯期生成的各種字面量與符號參考,這部分內容將在類加載后存放到方法區的運行時常量池中,
Java 虛擬機對于 Class 檔案每一部分(自然也包括常量池)的格式都有嚴格規定,如每一個位元組用于存盤哪種資料都必須符合規范上的要求才會被虛擬機認可、加載和執行,但對于運行時常量池,《Java 虛擬機規范》并沒有做任何細節的要求,不同提供商實作的虛擬機可以按照自己的需要來實作這個記憶體區域,不過一般來說,除了保存 Class 檔案中描述的符號參考外,還會把由符號參考翻譯出來的直接參考也存盤在運行時常量池中,
運行時常量池相對于 Class 檔案常量池的另外一個重要特征是具備動態性,Java 語言并不要求常量一定只有編譯期才能產生,也就是說,并非預置入 Class 檔案中常量池的內容才能進入方法區的運行時常量池, 運行期間也可以將新的常量放入池中,這種特性被開發人員利用得比較多的便是 String 類的 intern() 方法,
直接記憶體
直接記憶體(Direct Memory)并不是虛擬機運行時資料區域的一部分,也不是《Java 虛擬機規范》中定義的記憶體區域,但是這部分記憶體也被頻繁地使用,而且也可能導致 OutOfMemoryError 例外出現,所以我們放到這里一起講解,
在 JDK1.4 中新加入了 NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(Buffer)的I/O方式, NIO 它可以使用 Native 函式庫直接分配堆外記憶體,然后通過一個存盤在 Java 堆里面的 DirectByteBuffer 物件作為這塊記憶體的參考進行操作,這樣能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆中來回復制資料,
顯然,本機直接記憶體的分配不會受到 Java 堆大小的限制,但是,既然是記憶體,則肯定還是會受到本機總記憶體(包括物理記憶體、SWAP 磁區或者分頁檔案)大小以及處理器尋址空間的限制,一般服務器管理員配置虛擬機引數時,會根據實際記憶體去設定 -Xmx 等引數資訊,但經常忽略掉直接記憶體,使得各個記憶體區域總和大于物理記憶體限制(包括物理的和作業系統級的限制),從而導致動態擴展時出現 OutOfMemoryError 例外,
總結
運行時資料區域
程式計數器
程式計數器是一塊較小的記憶體空間,程式計數器是“執行緒私有”的資料區域,
如果一個執行緒正在執行的是一個 Java 方法, 程式計數器記錄的是正在執行的虛擬機位元組碼指令的地址,
在 Java 虛擬機的概念模型里,位元組碼解釋器作業時就是通過改變程式計數器的值來選取下一條需要執行的位元組碼指令,程式計數器是程式控制流的指示器,分支、回圈、跳轉、例外處理、執行緒恢復等基礎功能都需要依賴程式計數器來完成,
Java 虛擬機堆疊、本地方法堆疊
HotSpot 虛擬機將本地方法堆疊和虛擬機堆疊合二為一,
- Java 虛擬機堆疊描述的是 Java 方法執行的執行緒記憶體模型:每個方法被執行的時候,Java 虛擬機都會同步創建一個堆疊幀(Stack Frame)用于存盤區域變數表、運算元堆疊、動態連接、方法出口等資訊,每個方法被呼叫直至執行完畢的程序,就對應著一個堆疊幀在 Java 虛擬機堆疊中從入堆疊到出堆疊的程序,
- 本地方法堆疊(Native Method Stacks) 與虛擬機堆疊所發揮的作用非常相似,它們兩個的區別是:虛擬機堆疊為虛擬機執行 Java 方法(也就是位元組碼) 服務,而本地方法堆疊則是為虛擬機使用到的本地(Native)方法服務,
【Java 堆】記憶體區域的唯一目的就是存放物件實體,Java 世界里 “幾乎” 所有的物件實體都在【Java 堆】區域分配記憶體,
【方法區】記憶體區域用于存盤已被虛擬機加載的型別資訊(如類名、訪問修飾符、欄位描述、方法描述等)、常量、靜態變數、即時編譯器編譯后的代碼快取等資料,
“執行緒私有” 的區域
“執行緒私有” 的記憶體區域:每個執行緒都有一個獨立的記憶體區域,各個執行緒之間的記憶體區域互不影響, 獨立存盤,我們稱這類記憶體區域為 “執行緒私有” 的記憶體區域,
- “執行緒私有” 的記憶體區域有:程式計數器、Java 虛擬機堆疊、本地方法堆疊;
- 被所有執行緒共享的記憶體區域有:Java 堆、方法區,
垃圾收集的區域
程式計數器、Java 虛擬機堆疊、本地方法堆疊這三個運行時資料區域隨執行緒而生,隨執行緒而滅,堆疊中的堆疊幀隨著方法的進入和退出而有條不紊地執行著入堆疊和出堆疊操作,每一個堆疊幀中分配多少記憶體基本上是在類結構確定下來時就已知的(盡管在運行期會由即時編譯器進行一些優化,但在基于概念模型的討論里,大體上可以認為是編譯期可知的),因此這三個運行時資料區域的記憶體分配和回收都具備確定性,在這三個運行時資料區域內就不需要過多考慮如何回收的問題,當方法結束或者執行緒結束時,記憶體自然就跟隨著回收了,
而 Java 堆和方法區這兩個運行時資料區域則有著很顯著的不確定性:一個介面的多個實作類需要的記憶體可能會不一樣, 一個方法所執行的不同條件分支所需要的記憶體也可能不一樣,只有處于運行期間,我們才能知道程式究竟會創建哪些物件,創建多少個物件,這部分(Java 堆、方法區)記憶體的分配和回收是動態的,垃圾收集器所關注的正是這部分(Java 堆、方法區)記憶體該如何管理,
記憶體區域的例外狀況
【程式計數器】記憶體區域是唯一一個在《Java 虛擬機規范》中沒有規定任何 OutOfMemoryError 情況的區域,
【Java 虛擬機堆疊】、【本地方法堆疊】記憶體區域:在【Java 虛擬機堆疊】、【本地方法堆疊】記憶體區域中,可能出現的例外狀況有:OutOfMemoryError、StackOverflowError:
- 創建執行緒時,需要申請堆疊空間,如果執行緒申請堆疊空間失敗了,那么 Java 虛擬機就會拋出 OutOfMemoryError 例外,
- 執行緒申請堆疊空間成功后,如果執行緒請求的堆疊深度大于虛擬機所允許的深度,那么 Java 虛擬機就會拋出 StackOverflowError 例外,
【Java 堆】記憶體區域:如果 Java 堆無法滿足新的記憶體分配需求,并且堆也無法再擴展時,Java 虛擬機將會拋出 OutOfMemoryError 例外,
【方法區】記憶體區域:如果方法區無法滿足新的記憶體分配需求時,Java 虛擬機將會拋出 OutOfMemoryError 例外,
【直接記憶體】:如果各個記憶體區域的總和大于物理記憶體限制(包括物理的和作業系統級的限制),Java 虛擬機將會拋出 OutOfMemoryError 例外,
參考資料
《深入理解 Java 虛擬機》第 2 章:Java 記憶體區域與記憶體溢位例外 2.2 運行時資料區域
本文來自博客園,作者:真正的飛魚,轉載請注明原文鏈接:https://www.cnblogs.com/feiyu2/p/17279905.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/548945.html
標籤:Java
