Java虛擬機運行時數據區
Java 虛擬機在執行Java程序過程中會把它管理的內存劃分為若干個不同的數據區域。這些區域有著各自的用途,以及創建和銷毀時間,有的區域隨著虛擬機進程的啟動而存在,有些區域則依賴用戶線程的啟動和結束建立和銷毀。
線程私有:程序計數器,本地方法棧,虛擬機棧
線程隔離:方法區,堆區
線程私有數據區
程序計數器
程序計數器是一塊較小的內存空間,它的作用可以看作是當前線程所執行的字節碼的行號指示器,字節碼解釋器工作時通過改變該計數器的值來選擇下一條需要執行的字節碼指令,分支、跳轉、循環等基礎功能都要依賴它來實現。
由于Java虛擬機的多線程是通過線程輪流切換并分配處理器執行時間的方式來實現的,在任何一個時刻,一個處理器只會執行一條線程中的指令。為了線程切換后能恢復到正確的位置,每條線程都有一個獨立的的程序計數器,各線程間的計數器互不影響,獨立存儲,因此該區域是線程私有的。
當線程在執行一個 Java 方法時,該計數器記錄的是正在執行的虛擬機字節碼指令的地址,當線程在執行的是 Native 方法(調用本地操作系統方法)時,該計數器的值為空。另外,該內存區域是唯一一個在 Java 虛擬機規范中沒有規定任何 OOM(內存溢出:OutOfMemoryError)情況的區域。
Java 虛擬機棧
Java虛擬機棧 是Java方法執行的內存模型,是線程私有的。每個方法在執行的時候都會創建一個棧幀,用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,而且 每個方法從調用直至完成的過程,對應一個棧幀在虛擬機棧中入棧到出棧的過程。 對于執行引擎來講,活動線程中,只有棧頂的棧幀是有效的,稱為當前棧幀,這個棧幀所關聯的方法稱為當前方法,執行引擎所運行的所有字節碼指令都只針對當前棧幀進行操作。
在 Java 虛擬機規范中,對這個區域規定了兩種異常情況:
如果線程請求的棧深度大于虛擬機所允許的深度,將拋出StackOverflowError異常。
如果虛擬機在動態擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError異常。
每個線程擁有一個自己的棧,這個棧的大小決定了方法調用的可達深度(遞歸多少層次,或嵌套調用多少層其他方法,-Xss 參數可以設置虛擬機棧大小),若線程請求的棧深度大于虛擬機允許的深度,則拋出 StackOverFlowError 異常。此外,棧的大小可以是固定的,也可以是動態擴展的,若虛擬機棧可以動態擴展(大多數虛擬機都可以),但擴展時無法申請到足夠的內存(比如沒有足夠的內存為一個新創建的線程分配棧空間時),則拋出 OutofMemoryError 異常。
局部變量
局部變量表主要存放一些基本數據類型(int, short, long, byte, float, double, boolean, char), 對象引用(reference類型,它不等同于對象本身,可能是一個指向對象起始地址的引用指針,或者代表對象的句柄)和return Adreess 類型(指向了一條字節碼指令的地址)。局部變量表所需的內存空間在編譯期間完成分配,即在 Java 程序被編譯成 Class 文件時,就確定了所需分配的最大局部變量表的容量。當進入一個方法時,這個方法需要在棧中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
局部變量表的容量以變量槽(Slot)為最小單位。在虛擬機規范中并沒有明確指明一個 Slot 應占用的內存空間大小(允許其隨著處理器、操作系統或虛擬機的不同而發生變化),一個 Slot 可以存放一個32位以內的數據類型:boolean、byte、char、short、int、float、reference 和 returnAddresss。reference 是對象的引用類型,returnAddress 是為字節指令服務的,它執行了一條字節碼指令的地址。對于 64 位的數據類型(long和double),虛擬機會以高位在前的方式為其分配兩個連續的 Slot 空間。
操作數棧
操作數棧又常被稱為操作棧,操作數棧的最大深度也是在編譯的時候就確定了。32 位數據類型所占的棧容量為 1,64 位數據類型所占的棧容量為 2。當一個方法開始執行時,它的操作棧是空的,在方法的執行過程中,會有各種字節碼指令(比如:加操作、賦值元算等)向操作棧中寫入和提取內容,也就是入棧和出棧操作。
Java 虛擬機的解釋執行引擎稱為“基于棧的執行引擎”,其中所指的“棧”就是操作數棧。因此我們也稱 Java 虛擬機是基于棧的,這點不同于 Android 虛擬機,Android 虛擬機是基于寄存器的。
基于棧的指令集最主要的優點是可移植性強,主要的缺點是執行速度相對會慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的優點是執行速度快,主要的缺點是可移植性差。
動態連接
每個棧幀都包含一個指向運行時常量池(在方法區中,后面介紹)中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態連接。Class 文件的常量池中存在有大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用為參數。這些符號引用,一部分會在類加載階段或第一次使用的時候轉化為直接引用(如 final、static 域等),稱為靜態解析,另一部分將在每一次的運行期間轉化為直接引用,這部分稱為動態連接。
方法返回地址
當一個方法被執行后,有兩種方式退出該方法:執行引擎遇到了任意一個方法返回的字節碼指令或遇到了異常,并且該異常沒有在方法體內得到處理。無論采用何種退出方式,在方法退出之后,都需要返回到方法被調用的位置,程序才能繼續執行。方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。一般來說,方法正常退出時,調用者的 PC 計數器的值就可以作為返回地址,棧幀中很可能保存了這個計數器值,而方法異常退出時,返回地址是要通過異常處理器來確定的,棧幀中一般不會保存這部分信息。
方法退出的過程實際上等同于把當前棧幀出站,因此退出時可能執行的操作有:恢復上層方法的局部變量表和操作數棧,如果有返回值,則把它壓入調用者棧幀的操作數棧中,調整 PC 計數器的值以指向方法調用指令后面的一條指令。
本地方法棧
該區域與虛擬機棧所發揮的作用非常相似,只是虛擬機棧為虛擬機執行 Java 方法服務,而本地方法棧則為使用到的本地操作系統(Native)方法服務。
線程共享數據區
堆(GC堆)
Java Heap 是 Java 虛擬機所管理的內存中最大的一塊,它是所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例和數組都在這類分配內存。Java Heap 是垃圾收集器管理的主要區域,因此很多時候也被稱為“GC堆”。
從內存回收的角度看,由于現在收集器基本是采用分代收集算法,所以Java 堆可以細分為:新生代(Eden,),老年代
分配緩沖區(Thread Local Allocation Buffer ,TLAB
Sun Hotspot JVM 為了提升對象內存分配的效率,對于所創建的線程都會分配一塊獨立的空間 TLAB,其大小由JVM根據運行的情況計算而得。在TLAB上分配對象時不需要加鎖(相對于CAS配上失敗重試方式 ),因此JVM在給線程的對象分配內存時會盡量的在TLAB上分配,在這種情況下JVM中分配對象內存的性能和C基本是一樣高效的,但如果對象過大的話則仍然是直接使用分配到老年代。
根據 Java 虛擬機規范的規定,Java 堆可以處在物理上不連續的內存空間中,只要邏輯上是連續的即可。在實現時,既可以實現成固定大小的,也可以是可擴展的,如果在堆中沒有內存可分配時,并且堆也無法擴展時,將會拋出 OutOfMemoryError 異常。
方法區
方法區 與Java堆一樣,是各個線程共享的內存區域,而且不需要連續的內存和可以選擇固定大小或者可擴展的。它用與存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯后的代碼等數據。 雖然方法句被Java虛擬機規范描述為堆的一個邏輯部分,但是他卻有一個別名叫Non-Heap(非堆) 。根據 Java 虛擬機規范的規定,當方法區無法滿足內存分配需求時,將拋出 OutOfMemoryError 異常 。
運行時常量池
常量池(Class文件常量池),用于存放編譯器生成的各種字面量和符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。
運行時常量池相對于 Class 文件常量池的另一個重要特征是具備動態性,Java 語言并不要求常量一定只能在編譯期產生,也就是并非預置入 Class 文件中的常量池的內容才能進入方法區的運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用比較多的是 String 類的 intern()方法。同樣作為方法區的一部分,當常量池無法申請到內存時會拋出OutOfMemoryError 異常 。
方法區的回收
相對而言,垃圾收集行為在方法區是比較少出現的。這個區域回收的主要目標廢棄常量和無用的類。
回收廢棄常量
與Java堆中的對象非常類似。 假如常量池中的字符串"abc",沒有被任何String對象,也沒有其他地方引用,如果發生垃圾回收,"abc"就會被回收。常量池中其他類(接口),方法,字段的符號引用類似。
回收無用的類
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是“無用的類”:
該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例;
加載該類的ClassLoader已經被回收;
該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
虛擬機可以對滿足上述3個條件的無用類進行回收(卸載),這里說的僅僅是“可以”,而不是和對象一樣,不使用了就必然會回收。
在大量使用反射、動態代理、CGLib等bytecode框架的場景,以及動態生成JSP和OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出
直接內存
直接內存并不是虛擬機運行時數據區的一部分,也不是Java虛擬久規范中定義的內存區域,由于頻繁的內使用,且可能導致OutOfMemoryError異常。
在 JDK1.4 中新引入了 NIO 機制,它是一種基于通道與緩沖區的新 I/O 方式,可以直接從操作系統中分配直接內存,即在堆外分配內存,這樣能在一些場景中提高性能,因為避免了在 Java 堆和 Native 堆中來回復制數據。