大多數 JVM 將內存區域划分為 Method Area(Non-Heap)(方法區),Heap(堆),Program Counter Register(程序計數器), VM Stack(虛擬機棧,也有翻譯成JAVA 方法棧的),Native Method Stack (本地方法棧)
其中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上)的原因。
1、程序計數器
程序計數器是一塊較小的區域,它的作用可以看做是當前線程所執行的字節碼的位置指示器。在虛擬機的模型里,字節碼指示器就是通過改變程序計數器的值來指定下一條需要執行的指令。分支,循環等基礎功能就是依賴程序計數器來完成的。
由於java虛擬機的多線程是通過輪流切換並分配處理器執行時間來完成,一個處理器同一時間只會執行一條線程中的指令。為了線程恢復后能夠恢復正確的執行位置,每條線程都需要一個獨立的程序計數器,以確保線程之間互不影響。所以程序計數器是“線程私有”的內存。
如果虛擬機正在執行的是一個Java方法,則計數器指定的是字節碼指令對應的地址,如果正在執行的是一個本地方法,則計數器指定問空undefined。程序計數器區域是Java虛擬機中唯一沒有定義OutOfMemory異常的區域。
2、Java虛擬機棧——VM Stack
和程序計數器一樣也是線程私有的,生命周期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會創建一個棧幀用於存儲局部變量表,操作棧,動態鏈接,方法出口等信息。每一個方法被調用的過程就對應一個棧幀在虛擬機棧中從入棧到出棧的過程。
通常所說的虛擬機運行時分為棧和堆,這里的棧指的就是虛擬機棧或者說虛擬機棧中的局部變量表部分。
局部變量表存放了編譯器可知的各種基本數據類型、對象引用和returnAddress類型(指向一條字節碼指令的地址)。局部變量表所需的內存空間在編譯器完成分配,當進入一個方法時這個方法需要在幀中分配多大的內存空間是完全確定的,運行期間不會改變局部變量表的大小。(64位長度的long和double會占用兩個局部變量空間,其他的數據類型占用一個)
Java虛擬機棧可能出現兩種類型的異常:
1. 線程請求的棧深度大於虛擬機允許的棧深度,將拋出StackOverflowError。
2.虛擬機棧空間可以動態擴展,當動態擴展是無法申請到足夠的空間時,拋出OutOfMemory異常。
3、本地方法棧
本地方法棧和虛擬機棧基本類似,只不過Java虛擬機棧執行的是Java代碼(字節碼),本地方法棧中執行的是本地方法的服務。本地方法棧中也會拋出StackOverflowError和OutOfMemory異常。
4、堆
堆是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異常。
5、方法區
方法區也是線程共享的區域,用於存儲已經被虛擬機加載的類信息,常量,靜態變量和即時編譯器(JIT)編譯后的代碼等數據。Java虛擬機把方法區描述為堆的一個邏輯分區,不過方法區有一個別名Non-Heap(非堆),用於區別於Java堆區。
Java虛擬機規范對這個區域的限制也非常寬松,除了可以是物理不連續的空間外,也允許固定大小和擴展性,還可以不實現垃圾收集。相對而言,垃圾收集行為在這個區域是比較少出現的(所以常量和靜態變量的定義要多注意)。方法區的內存收集還是會出現,不過這個區域的內存收集主要是針對常量池的回收和對類型的卸載。
一般來說方法區的內存回收比較難以令人滿意。當方法區無法滿足內存分配需求時將拋出OutOfMemoryError異常。
5.1、運行時常量池
運行時常量池是方法區的一部分,Class文件中除了有類的版本,字段,方法,接口等信息以外,還有一項信息是常量池用於存儲編譯器生成的各種字面量和符號引用,這部分信息將在類加載后存放到方法區的運行時常量池中。Java虛擬機對類的每 一部分(包括常量池)都有嚴格的規定,每個字節用於存儲哪種數據都必須有規范上的要求,這樣才能夠被虛擬機認可,裝載和執行。一般來說,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
運行時常量池相對於Class文件常量池的另外一個重要特征是具備動態性,Java虛擬機並不要求常量只能在編譯期產生,也就是並非預置入Class文件常量池的內容才能進入方法區的運行時常量池中,運行期間也可將新的常量放入常量池中。
常量池是方法區的一部分,所以受到內存的限制,當無法申請到足夠內存時會拋出OutOfMemoryError異常。
6、對象訪問
對象訪問在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使用的是直接指針方式,不過從整個軟件開發的范圍來看,各種語言和框架使用句柄訪問方式的情況十分常見。