大多數 JVM 將內存區域劃分爲 Method Area(Non-Heap)(方法區),Heap(堆),Program Counter Register(程序計數器), VM Stack(虛擬機棧,也有翻譯成JAVA 方法棧的),Native Method Stack (本地方法棧)java
其中Method Area(方法區)和Heap(堆)是線程共享的,VM Stack,Native Method Stack 和Program Counter Register是非線程共享的。算法
爲何分爲線程共享和非線程共享的呢?請繼續往下看。編程
首先咱們熟悉一下一個通常性的 Java 程序的工做過程。一個 Java 源程序文件,會被編譯爲字節碼文件(以 class 爲擴展名),每一個java程序都須要運行在本身的JVM上,而後告知 JVM 程序的運行入口,再被 JVM 經過字節碼解釋器加載運行。那麼程序開始運行後,都是如何涉及到各內存區域的呢?數據結構
歸納地說來,JVM初始運行的時候都會分配好Method Area(方法區)和Heap(堆),而JVM 每遇到一個線程,就爲其分配一個程序計數器, 虛擬機棧和本地方法棧,當線程終止時,三者(虛擬機棧,本地方法棧和程序計數器)所佔用的內存空間也會被釋放掉。這也是爲何我把內存區域分爲線程共享和非線程共享的緣由,非線程共享的那三個區域的生命週期與所屬線程相同,而線程共享的區域與JAVA程序運行的生命週期相同,因此這也是系統垃圾回收的場所只發生在線程共享的區域(實際上對大部分虛擬機來講只發生在Heap上)的緣由。多線程
一、程序計數器框架
程序計數器是一塊較小的區域,它的做用能夠看作是當前線程所執行的字節碼的位置指示器。在虛擬機的模型裏,字節碼指示器就是經過改變程序計數器的值來指定下一條須要執行的指令。分支,循環等基礎功能就是依賴程序計數器來完成的。編程語言
因爲java虛擬機的多線程是經過輪流切換並分配處理器執行時間來完成,一個處理器同一時間只會執行一條線程中的指令。爲了線程恢復後可以恢復正確的執行位置,每條線程都須要一個獨立的程序計數器,以確保線程之間互不影響。因此程序計數器是「線程私有」的內存。佈局
若是虛擬機正在執行的是一個Java方法,則計數器指定的是字節碼指令對應的地址,若是正在執行的是一個本地方法,則計數器指定問空undefined。程序計數器區域是Java虛擬機中惟一沒有定義OutOfMemory異常的區域。spa
二、Java虛擬機棧——VM Stack.net
和程序計數器同樣也是線程私有的,生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法被執行的時候都會建立一個棧幀用於存儲局部變量表,操做棧,動態連接,方法出口等信息。每個方法被調用的過程就對應一個棧幀在虛擬機棧中從入棧到出棧的過程。
一般所說的虛擬機運行時分爲棧和堆,這裏的棧指的就是虛擬機棧或者說虛擬機棧中的局部變量表部分。
局部變量表存放了編譯器可知的各類基本數據類型、對象引用和returnAddress類型(指向一條字節碼指令的地址)。局部變量表所需的內存空間在編譯器完成分配,當進入一個方法時這個方法須要在幀中分配多大的內存空間是徹底肯定的,運行期間不會改變局部變量表的大小。(64位長度的long和double會佔用兩個局部變量空間,其餘的數據類型佔用一個)
Java虛擬機棧可能出現兩種類型的異常:
1. 線程請求的棧深度大於虛擬機容許的棧深度,將拋出StackOverflowError。
2.虛擬機棧空間能夠動態擴展,當動態擴展是沒法申請到足夠的空間時,拋出OutOfMemory異常。
三、本地方法棧
本地方法棧和虛擬機棧基本相似,只不過Java虛擬機棧執行的是Java代碼(字節碼),本地方法棧中執行的是本地方法的服務。本地方法棧中也會拋出StackOverflowError和OutOfMemory異常。
四、堆
堆是Java虛擬機所管理的內存中最大的一塊。堆是全部線程共享的一塊區域,在虛擬機啓動時建立。堆的惟一目的是存放對象實例,幾乎全部的對象實例都在這裏分配,不過隨着JIT編譯器的發展和逃逸技術的成熟,棧上分配和標量替換技術使得這種狀況發生着微妙的變化,對上分配正變得不那麼絕對。
附:在Java編程語言和環境中,即時編譯器(JIT compiler,just-in-time compiler)是一個把Java的字節碼(包括須要被解釋的指令的程序)轉換成能夠直接發送給處理器的指令的程序。當你寫好一個Java程序後,源語言的語句將由Java編譯器編譯成字節碼,而不是編譯成與某個特定的處理器硬件平臺對應的指令代碼(好比,Intel的Pentium微處理器或IBM的System/390處理器)。字節碼是能夠發送給任何平臺而且能在那個平臺上運行的獨立於平臺的代碼。
Java堆是垃圾收集器管理的主要區域,因此也稱爲「GC堆」。因爲如今的垃圾收集器基本上都是採用分代收集算法,因此Java堆還可細分爲:新生代和老生代。在細緻一點可分爲Eden空間,From Survivor空間,To Survivor空間。若是從內存分配的角度看,線程共享的Java堆可劃分出多個線程私有的分配緩衝區。不過不管如何劃分,都與存放內容無關,不管哪一個區域,都是用來存放對象實例。細分的目的是爲了更好的回收內存或者更快的分配內存。
Java堆能夠是物理上不連續的空間,只要邏輯上連續便可,主流的虛擬機都是按照可擴展的方式來實現的。若是當前對中沒有內存完成對象實例的建立,而且不能在進行內存擴展,則會拋出OutOfMemory異常。
五、方法區
方法區也是線程共享的區域,用於存儲已經被虛擬機加載的類信息,常量,靜態變量和即時編譯器(JIT)編譯後的代碼等數據。Java虛擬機把方法區描述爲堆的一個邏輯分區,不過方法區有一個別名Non-Heap(非堆),用於區別於Java堆區。
Java虛擬機規範對這個區域的限制也很是寬鬆,除了能夠是物理不連續的空間外,也容許固定大小和擴展性,還能夠不實現垃圾收集。相對而言,垃圾收集行爲在這個區域是比較少出現的(因此常量和靜態變量的定義要多注意)。方法區的內存收集仍是會出現,不過這個區域的內存收集主要是針對常量池的回收和對類型的卸載。
通常來講方法區的內存回收比較難以使人滿意。當方法區沒法知足內存分配需求時將拋出OutOfMemoryError異常。
5.一、運行時常量池
運行時常量池是方法區的一部分,Class文件中除了有類的版本,字段,方法,接口等信息之外,還有一項信息是常量池用於存儲編譯器生成的各類字面量和符號引用,這部分信息將在類加載後存放到方法區的運行時常量池中。Java虛擬機對類的每 一部分(包括常量池)都有嚴格的規定,每一個字節用於存儲哪一種數據都必須有規範上的要求,這樣纔可以被虛擬機承認,裝載和執行。通常來講,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
運行時常量池相對於Class文件常量池的另一個重要特徵是具有動態性,Java虛擬機並不要求常量只能在編譯期產生,也就是並不是預置入Class文件常量池的內容才能進入方法區的運行時常量池中,運行期間也可將新的常量放入常量池中。
常量池是方法區的一部分,因此受到內存的限制,當沒法申請到足夠內存時會拋出OutOfMemoryError異常。
六、對象訪問
對象訪問在Java語言中無處不在,即便是最簡單的訪問,也會涉及到Java棧,java堆,方法區這三個最重要的內存區域之間的關聯關係。以下面的代碼:
Object obj = new Object();
假設這段代碼出如今方法體中,那麼「Object obj」部分的語義將會反映到Java棧的本地變量表中,做爲一個reference類型的數據存在。而「new Object();」部分的語義將會反應到Java堆中,造成一塊存儲Object類型全部實例數據值(Instance Data)的結構化內存,根據具體類型以及虛擬機實現的對象分佈的不一樣,這塊內存的長度是不固定的。另外,在JAVA堆中還必須包含能查找到此對象內存數據的地址信息,這些類型數據則存儲在方法區中。
因爲reference類型在Java虛擬機中之規定了指向對象的引用,並無規定這個引用要經過哪一種方式去定位,以及訪問到Java堆中的對象的具體位置,所以虛擬機實現的對象訪問方式會有所不一樣。主流的訪問方式有兩種:句柄訪問方式和直接指針。
1. 若是使用句柄訪問方式,Java堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息。
2. 若是經過直接指針方式訪問,Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,reference中直接存儲的就是對象的地址。
兩種方式各有優點,局並訪問方式最大的好處是reference中存放的是穩定的句柄地址,在對象被移動時,只會改變句柄中的實例數據指針,而reference自己不須要被修改。而指針訪問的最大優點是速度快,它節省了一次指針定位的開銷,因爲對象訪問在Java中很是頻繁,一次這類開銷聚沙成塔後也是一項很是可觀的成本。
具體的訪問方式都是有虛擬機指定的,虛擬機Sun HotSpot使用的是直接指針方式,不過從整個軟件開發的範圍來看,各類語言和框架使用句柄訪問方式的狀況十分常見。