因爲Java程序是交由JVM執行的,因此咱們在談Java內存區域劃分的時候事實上是指JVM內存區域劃分。在討論JVM內存區域劃分以前,先來看一下Java程序具體執行的過程:html
如上圖所示,首先Java源代碼文件(.java後綴)會被Java編譯器編譯爲字節碼文件(.class後綴),而後由JVM中的類加載器加載各個類的字節碼文件,加載完畢以後,交由JVM執行引擎執行。在整個程序執行過程當中,JVM會用一段空間來存儲程序執行期間須要用到的數據和相關信息,這段空間通常被稱做爲Runtime Data Area(運行時數據區),也就是咱們常說的JVM內存。所以,在Java中咱們經常說到的內存管理就是針對這段空間進行管理(如何分配和回收內存空間)。java
根據《Java虛擬機規範》的規定,運行時數據區一般包括這幾個部分:程序計數器(Program Counter Register)、Java棧(VM Stack)、本地方法棧(Native Method Stack)、方法區(Method Area)、堆(Heap)。程序員
如上圖所示,JVM中的運行時數據區應該包括這些部分。在JVM規範中雖然規定了程序在執行期間運行時數據區應該包括這幾部分,可是至於具體如何實現並無作出規定,不一樣的虛擬機廠商能夠有不一樣的實現方式。數組
程序計數器(Program Counter Register),也有稱做爲PC寄存器。想必學過彙編語言的朋友對程序計數器這個概念並不陌生,在彙編語言中,程序計數器是指CPU中的寄存器,它保存的是程序當前執行的指令的地址(也能夠說保存下一條指令的所在存儲單元的地址),當CPU須要執行指令時,須要從程序計數器中獲得當前須要執行的指令所在存儲單元的地址,而後根據獲得的地址獲取到指令,在獲得指令以後,程序計數器便自動加1或者根據轉移指針獲得下一條指令的地址,如此循環,直至執行完全部的指令。數據結構
雖然JVM中的程序計數器並不像彙編語言中的程序計數器同樣是物理概念上的CPU寄存器,可是JVM中的程序計數器的功能跟彙編語言中的程序計數器的功能在邏輯上是等同的,也就是說是用來指示 執行哪條指令的。多線程
因爲在JVM中,多線程是經過線程輪流切換來得到CPU執行時間的,所以,在任一具體時刻,一個CPU的內核只會執行一條線程中的指令,所以,爲了可以使得每一個線程都在線程切換後可以恢復在切換以前的程序執行位置,每一個線程都須要有本身獨立的程序計數器,而且不能互相被幹擾,不然就會影響到程序的正常執行次序。所以,能夠這麼說,程序計數器是每一個線程所私有的。jvm
在JVM規範中規定,若是線程執行的是非native方法,則程序計數器中保存的是當前須要執行的指令的地址;若是線程執行的是native方法,則程序計數器中的值是undefined。函數
因爲程序計數器中存儲的數據所佔空間的大小不會隨程序的執行而發生改變,所以,對於程序計數器是不會發生內存溢出現象(OutOfMemory)的。佈局
Java棧也稱做虛擬機棧(Java Vitual Machine Stack),也就是咱們經常所說的棧,跟C語言的數據段中的棧相似。事實上,Java棧是Java方法執行的內存模型。爲何這麼說呢?下面就來解釋一下其中的緣由。線程
Java棧中存放的是一個個的棧幀,每一個棧幀對應一個被調用的方法,在棧幀中包括局部變量表(Local Variables)、操做數棧(Operand Stack)、指向當前方法所屬的類的運行時常量池(運行時常量池的概念在方法區部分會談到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加信息。當線程執行一個方法時,就會隨之建立一個對應的棧幀,並將創建的棧幀壓棧。當方法執行完畢以後,便會將棧幀出棧。所以可知,線程當前執行的方法所對應的棧幀一定位於Java棧的頂部。講到這裏,你們就應該會明白爲何 在 使用 遞歸方法的時候容易致使棧內存溢出的現象了以及爲何棧區的空間不用程序員去管理了(固然在Java中,程序員基本不用關係到內存分配和釋放的事情,由於Java有本身的垃圾回收機制),這部分空間的分配和釋放都是由系統自動實施的。對於全部的程序設計語言來講,棧這部分空間對程序員來講是不透明的。下圖表示了一個Java棧的模型:
局部變量表,顧名思義,想必不用解釋你們應該明白它的做用了吧。就是用來存儲方法中的局部變量(包括在方法中聲明的非靜態變量以及函數形參)。對於基本數據類型的變量,則直接存儲它的值,對於引用類型的變量,則存的是指向對象的引用。局部變量表的大小在編譯器就能夠肯定其大小了,所以在程序執行期間局部變量表的大小是不會改變的。
操做數棧,想必學過數據結構中的棧的朋友想必對錶達式求值問題不會陌生,棧最典型的一個應用就是用來對錶達式求值。想一想一個線程執行方法的過程當中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。所以能夠這麼說,程序中的全部計算過程都是在藉助於操做數棧來完成的。
指向運行時常量池的引用,由於在方法執行的過程當中有可能須要用到類中的常量,因此必需要有一個引用指向運行時常量。
方法返回地址,當一個方法執行完畢以後,要返回以前調用它的地方,所以在棧幀中必須保存一個方法返回地址。
因爲每一個線程正在執行的方法可能不一樣,所以每一個線程都會有一個本身的Java棧,互不干擾。
本地方法棧與Java棧的做用和原理很是類似。區別只不過是Java棧是爲執行Java方法服務的,而本地方法棧則是爲執行本地方法(Native Method)服務的。在JVM規範中,並無對本地方發展的具體實現方法以及數據結構做強制規定,虛擬機能夠自由實現它。在HotSopt虛擬機中直接就把本地方法棧和Java棧合二爲一。
Java中的堆是用來存儲對象自己的以及數組(固然,數組引用是存放在Java棧中的)。只不過和C語言中的不一樣,在Java中,程序員基本不用去關心空間釋放的問題,Java的垃圾回收機制會自動進行處理。所以這部分空間也是Java垃圾收集器管理的主要區域。另外,堆是被全部線程共享的,在JVM中只有一個堆。
方法區在JVM中也是一個很是重要的區域,它與堆同樣,是被線程共享的區域。在方法區中,存儲了每一個類的信息(包括類的名稱、方法信息、字段信息)、靜態變量、常量以及編譯器編譯後的代碼等。
在Class文件中除了類的字段、方法、接口等描述信息外,還有一項信息是常量池,用來存儲編譯期間生成的字面量和符號引用。
在方法區中有一個很是重要的部分就是運行時常量池,它是每個類或接口的常量池的運行時表示形式,在類和接口被加載到JVM後,對應的運行時常量池就被建立出來。固然並不是Class文件常量池中的內容才能進入運行時常量池,在運行期間也可將新的常量放入運行時常量池中,好比String的intern方法。
在JVM規範中,沒有強制要求方法區必須實現垃圾回收。不少人習慣將方法區稱爲「永久代」,是由於HotSpot虛擬機以永久代來實現方法區,從而JVM的垃圾收集器能夠像管理堆區同樣管理這部分區域,從而不須要專門爲這部分設計垃圾回收機制。不過自從JDK7以後,Hotspot虛擬機便將運行時常量池從永久代移除了。
對象訪問在Java語言中是無處不在的,即便最簡單的對象訪問也會涉及到Java棧、Java堆、方法區這三個最重要的區域之間的關聯關係,好比下邊的代碼,咱們看它在虛擬機中發生了什麼:
Object object = new Object();
假設這段代碼是出如今方法體中,那麼「Object object」 這部分的語義會反映到Java棧的本地變量中,做爲一個Refrence類型數據出現。而 「new Object()」這部分的語義就會反映到堆中,造成了一塊存儲了Object類型全部實例數據值(Instance Data,對象中各個實例字段的數據)的結構化內存,根據具體類型以及虛擬機實現實現的對象內存佈局的不一樣,這塊內存的長度也不一樣。 另外 Java對中還包含的此對象的類型數據(如對象類型、父類、實現的接口、方法等)的地址信息,這些類型數據則存放在方法區中。
因爲Refrece類型在Java虛擬機規範中只規定了一個指向對象的引用,並無指定這個引用應該經過哪一種方式去定位,以及方法Java堆中對象的具體位置,因此不一樣虛擬機的不一樣,主流的訪問方式是:使用句柄和直接方法:
句柄訪問,Java 堆中會劃分出一塊內存來做爲句柄池,Refrence中存儲的是對象的句柄地址,句柄中包含了對象實例數據和數據類型各自的具體地址信息,如圖:
refrence中直接存儲的就是對象地址,如圖所示:
兩種方式的對比:
參考資料:
http://www.cnblogs.com/dolphin0520/p/3613043.html
《深刻理解Java虛擬機》