Java虛擬機在執行Java程序的過程當中會把Java程序所管理的內存劃分爲若干個不一樣的數據區域,這些區域能夠劃分爲5各部分:虛擬機棧、堆、方法區、本地方法棧、程序計數器,如圖:java
Java虛擬機棧(Java Virtual Machine Stack)是線程私有的,它的生命週期與線程相同。也就是,每一個方法被執行的時候,Java虛擬機都會同步建立一個棧幀 (Stack Frame)用於存儲局部變量表、操做數棧、動態鏈接、方法出口等信息。每個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。下面講解一下虛擬機棧中的內容:數組
局部變量表存放了編譯期可知的各類Java虛擬機基本數據類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它並不等同於對象自己,多是一個指向對象起始地址的引用指針,也多是指向一個表明對象的句柄或者其餘與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。緩存
這些數據類型在局部變量表中的存儲空間以局部變量槽(Slot)來表示,其中64位長度的long和double類型的數據會佔用兩個變量槽,其他的數據類型只佔用一個。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法須要在棧幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。請讀者注意,這裏說的「大小」是指變量槽的數量,虛擬機真正使用多大的內存空間(譬如按照1個變量槽佔用32個比特、64個比特,或者更多)來實現一個變量槽,這是徹底由具體的虛擬機實現自行決定的事情。安全
returnAddress類型目前已經不多見了,它是爲字節碼指令jsr、jsr_w和ret服務的,指向了一條字節碼指令的地址。雖然long以及double是分配在兩個變量槽中,可是因爲在線程內部,因此不會有數據競爭和線程安全問題。多線程
操做數棧(Operand Stack)也常被稱爲操做棧,它是一個後入先出(Last In First Out,LIFO)棧。同局部變量表同樣,操做數棧的最大深度也在編譯的時候被寫入到Code屬性的max_stacks數據項之中。當一個方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法的執行過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,也就是出棧和入棧操做。譬如在作算術運算的時候是經過將運算涉及的操做數棧壓入棧頂後調用運算指令來進行的,又譬如在調用其餘方法的時候是經過操做數棧來進行方法參數的傳遞。舉個例子,例如整數加法的字節碼指令iadd,這條指令在運行的時候要求操做數棧中最接近棧頂的兩個元素已經存入了兩個int型的數值,當執行這個指令時,會把這兩個int值出棧並相加,而後將相加的結果從新入棧。函數
寫個小案例:spa
package com.courage; public class DeOperandStack { public static void main(String[] args) { int i = 1; int j = 2; int k = i + j; } }
此時DeOperandStack類中只有一個線程(main),局部變量表中擁有的變量:線程
默認args爲0號變量,因此這個線程中有四個局部變量,那麼是如何利用操做數棧進行加減的呢?指針
首先將第一個常數壓入棧,而後存儲局部變量表1號變量,而後將第二個常數壓入棧,而後存儲局部變量表2號變量,而後將局部變量表1,2兩個數值加載進棧,彈出相加以後將結果壓入棧,在將棧頂數據存儲到3號變量。code
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接(Dynamic Linking)。咱們知道Class文件的常量池中存有大量的符號引用,字節碼中的方法調用指令就以常量池裏指向方法的符號引用做爲參數。這些符號引用一部分會在類加載階段或者第一次使用的時候就被轉化爲直接引用,這種轉化被稱爲靜態解析。另一部分將在每一次運行期間都轉化爲直接引用,這部分就稱爲動態鏈接。
當一個方法開始執行後,只有兩種方式退出這個方法:
第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱爲調用者或者主調方法),方法是否有返回值以及返回值的類型將根據遇到何種方法返回指令來決定,這種退出方法的方式稱爲「正常調用完成」(Normal Method Invocation Completion)。
另一種退出方式是在方法執行的過程當中遇到了異常,而且這個異常沒有在方法體內獲得妥善處理。不管是Java虛擬機內部產生的異常,仍是代碼中使用athrow字節碼指令產生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會致使方法退出,這種退出方法的方式稱爲「異常調用完成(Abrupt Method Invocation Completion)」。
一個方法使用異常完成出口的方式退出,是不會給它的上層調用者提供任何返回值的。不管採用何種退出方式,在方法退出以後,都必須返回到最初方法被調用時的位置,程序才能繼續執行,方法返回時可能須要在棧幀中保存一些信息,用來幫助恢復它的上層主調方法的執行狀態。
Java堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,Java
世界裏「幾乎」全部的對象實例以及數組都在這裏分配內存。
從分配內存的角度看,全部線程共享的Java堆中能夠劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB),以提高對象分配時的效率。不過不管從什麼角度,不管如何劃分,都不會改變Java堆中存儲內容的共性,不管是哪一個區域,存儲的都只能是對象的實例,將Java堆細分的目的只是爲了更好地回收內存,或者更快地分配內存。
方法區(Method Area)與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。
和Java堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,甚至還能夠選擇不實現垃圾收集,這區域的內存回收目標主要是針對常量池的回收和對類型的卸載,通常來講這個區域的回收效果比較難使人滿意,尤爲是類型的卸載,條件至關苛刻,可是這部分區域的回收有時又確實是必要的,此區域未徹底回收會致使內存泄漏。
之因此將這三個放一塊兒,是這兒很容易混淆,對於Hotspot虛擬機,JDK六、JDK7 時方法區是 PermGen
(永久代),JDK8 時,方法區是 Metaspace
(元空間),怎麼回事呢?
方法區 是JVM的規範,全部虛擬機必須遵照的。常見的JVM虛擬機Hotspot 、JRockit(Oracle)、J9(IBM)
PermGen space則是 HotSpot 虛擬機 基於 JVM 規範對 方法區 的一個落地實現, 而且只有 HotSpot 纔有 PermGen space。而如 JRockit(Oracle)、J9(IBM) 虛擬機有 方法區 ,可是就沒有 PermGen space。
PermGen space 是JDK7及以前, HotSpot 虛擬機 對 方法區 的一個落地實現,在JDK8被移除。
Metaspace(元空間)是 JDK8及以後, HotSpot 虛擬機對方法區 的新的實現。
永久代以及元空間,能夠用來存放堆中存活好久的對象。元空間與永久代之間最大的區別在於:元空間並不在虛擬機中,而是使用本地內存
每個類有一個Class對象,編譯期生成,保存在同名的.class文件中。這些Class對象包含了這個類型的父類、接口、構造函數、方法、屬性等詳細信息,這些class文件在程序運行時會被ClassLoader加載到JVM中,在JVM中就表現爲一個Class對象,JVM使用該Class對象建立該類的全部常規對象,而這個對象的信息則保存在方法區的類信息中。
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表(Constant Pool Table),用於存放編譯期生成的各類字面量與符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。既然運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時會拋出OutOfMemoryError異常。
靜態變量也叫類變量,類的全部實例都共享,這個區專門存放靜態變量和靜態塊。
static 修飾的 在JVM運行時就加載到內存中了 因此不須要實例類。
靜態變量在類加載的準備階段分配內存並設置類變量初始值的階段,從概念上講,這些變量所使用的內存都應當在方法區中進行分配,但必須注意到方法區自己是一個邏輯上的區域,在JDK 7及以前,HotSpot使用永久代來實現方法區時,實現是徹底符合這種邏輯概念的;而在JDK 8及以後,類變量則會隨着Class對象一塊兒存放在Java堆中,這時候「類變量在方法區」就徹底是一種對邏輯概念的表述了,關於這部份內容,筆者已在4.3.1節介紹而且驗證過。
程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。在Java虛擬機的概念模型裏,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,它是程序控制流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
因爲Java虛擬機的多線程是經過線程輪流切換、分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。
本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,其區別只是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的本地(Native)方法服務。