以下基本不是原創,都是轉載。 JVM運行時,首先需要類載入器(ClassLoader) 載入所需類的位元組碼,載入完畢交由執行引擎執行,執行過程中需要一段空間來存儲數據(類比CPU與主存)。這段記憶體空間的分配和釋放過程正是我們所關心的,稱為運行時數據區。 運行時數據區 如上圖所示,運行時數據區包括:程 ...
以下基本不是原創,都是轉載。
JVM運行時,首先需要類載入器(ClassLoader) 載入所需類的位元組碼,載入完畢交由執行引擎執行,執行過程中需要一段空間來存儲數據(類比CPU與主存)。這段記憶體空間的分配和釋放過程正是我們所關心的,稱為運行時數據區。
運行時數據區
如上圖所示,運行時數據區包括:程式計數器(即PC寄存器),Java 虛擬機棧(VM Stack),Java 堆(Heap),方法區(Method Area),本地方法棧(Native Method Stack)。下麵帶領大家深入理解各個數據區域。
JVM實際上就是一臺虛擬的電腦,目的是為了實現"一次編譯,處處執行"。所以,在理解運行時數據區時,完全可以與操作系統系統 記憶體,寄存器類比學習。
2.2.1 程式計數器
程式計數器是一塊較小的記憶體區域,作用可以看做是當前線程執行的位元組碼的位置指示器。分支、迴圈、跳轉、異常處理和線程恢復等基礎功能都需要依賴這個計算器來完成
註意,Java虛擬機中的程式計數器指向正在執行的位元組碼地址,而不是下一條。
每條線程都有獨立的計數器,保證線程切換恢復正確位置,因此程式計數器這一塊記憶體區域是線程隔離的。該區域是唯一一個沒有規定任何OutOfMemoryError的區域
2.2.2 Java虛擬機棧(VM Strack)
虛擬機棧也叫棧記憶體,是線上程創建時創建,是線程私有的,它的生命期是跟隨線程的生命期,線程結束棧記憶體也就釋放,對於棧來說不存在垃圾回收問題,只要線程一結束,該棧就 Over,所以不存在垃圾回收。也有一些資料翻譯成JAVA方法棧,大概是因為它所描述的是java方法執行的記憶體模型。
每個方法執行的同時創建幀棧(Strack Frame)用於存儲局部變數表(包含了對應的方法參數和局部變數),操作棧(Operand Stack,記錄出棧、入棧的操作),動態鏈接、方法出口等信息。每個方法被調用直到執行完畢的過程,對應這幀棧在虛擬機棧的入棧和出棧的過程。
棧幀是一個記憶體區塊,是一個數據集,是一個有關方法(Method)和運行期數據的數據集,我們看一個圖來理解一下 Java棧,遵循“先進後出”原則。如下圖所示:
大多數人說的stack棧記憶體,就是指虛擬機棧中局部變數表部分
局部變數表存放了編譯期可知的各種基本數據類型(boolean、byte、char、short、int、float、long、double)、對象的引用(reference類型,不等同於對象本身,根據不同的虛擬機實現,可能是一個指向對象起始地址的引用指針,也可能是一個代表對象的句柄或者其他與對象相關的位置)和 returnAdress類型(指向下一條位元組碼指令的地址)。局部變數表所需的記憶體空間在編譯期間完成分配,在方法在運行之前,該局部變數表所需要的記憶體空間是固定的,運行期間也不會改變。
異常
- 當線程請求的棧深度大於所允許的深度,拋出StackOverflowError異常。
- 長度不夠時,虛擬機棧可進行動態擴展,申請記憶體。若無法申請到足夠的記憶體,拋出OutOfMemoryError異常。
2.2.3 本地方法棧(Native Method Stack)
1.本地方法棧與虛擬機棧類似,區別是虛擬機棧記錄執行的Java方法(也就是位元組碼),本地方法棧則記錄Native方法。
2.在HotSpot虛擬機將本地方法棧和虛擬機棧合二為一。
3.本地方法棧同樣會拋出StackOverflowError與OutOfMemoryError異常。
2.2.4 Java堆(Heap)
1.Java 堆是被所有線程共用的一塊記憶體區域,在虛擬機啟動時創建。
2.JVM里所管理記憶體最大的一塊,幾乎所有對象實例以及數組都在堆上,類的成員變數也是在堆上。(類的成員變數單指基本變數的引用??還沒弄清晰等更正)
3/Java堆可以處於物理上不連續的記憶體空間中,只要邏輯上是連續的即可,就像我們的磁碟空間一樣。
記憶體模型
這塊區域是垃圾收集器管理的主要區域("GC 堆 ")。現在收集器基本都是採用分代收集演算法:新生代採用複製演算法,老年代採用標記清理演算法。從記憶體回收的角度,Java堆可以分為新生代(Young Generation)與老生代(Old Generation)。這種劃分的方式,是為了更好的回收記憶體(老生代記憶體會被優先回收)。
如圖,新生代還可以分為Eden空間、From Survivor空間、To Survivor空間。
永久代(Permanent Generation)用於存儲靜態類型數據,與垃圾收集器關係不大。
註意:本圖展示的是JVM堆的記憶體模型,JVM堆記憶體包括Java堆區域 和 永久代區域。因此,永久代不屬於Java堆。
異常
Java堆同樣可擴展(-Xmx與-Xms參數)。若堆中記憶體已無法為對象實例分配且無法再擴展,拋出OutOfMemoryError異常。
2.2.5 方法區(Method Area)
方法區也叫永久代,在過去(自定義類載入器還不是很常見的時候),類大多是”static”的,很少被卸載或收集,因此被稱為“永久的(Permanent)”。
也被稱為Non-Heap(非堆),被所有的線程共用的一塊記憶體區域。它用於存儲已被虛擬機載入的類信息(Object Class Data(載入類的類定義數據) )、常量、靜態變數、即時編譯器(JIT)編譯後的代碼等數據。
方法區也可以是記憶體不連續的區域組成的,並且可設置為固定大小,也可以設置為可擴展的,這點與堆一樣。垃圾回收在這個區域會比較少出現,這個區域記憶體回收的目的主要針對常量池的回收和類的卸載(後面會提到)。
2.2.6 運行時常量池(Runtime Constant Pool)
運行時常量池是方法區的一部分。在位元組碼文件(Class文件)中,除了有類的版本、欄位、方法、介面等先關信息描述外,還有常量池(Constant Pool Table)信息,用於存儲編譯器產生的字面量和符號引用。這部分內容在類被載入後,都會存儲到方法區中的RCP。 值得註意的是,運行時常量池相對於CLass文件常量池的另外一個重要特征是具備動態性,所以運行時產生的新常量也可以被放入常量池中,比如 String 類中的 intern() 方法產生的常量。package intern; public class Main1 { public static void main(String[] args) { String s0= "I'm coding"; String s1=new String("I'm coding"); String s2=new String("I'm coding"); System.out.println( s0==s1 ); System.out.println( s0==s1.intern()); s2=s2.intern(); System.out.println( s0==s2 ); } }
輸出結果
false true true
本例中,s0直接保存在常量池,s1與s2的對象實例存儲在Java堆中。==直接比較對象的hashCode,因此第一行輸出false。s1.intern()方法返回s1在常量池中的引用,沒有則創建。
s1存放的字元串已經在常量池中存在,直接返回s0的引用,第二行輸出true。
同理,s2接收了s2.intern()的返回值,字元串值與s0相同,第三行輸出true。
2.2.7 直接記憶體
直接記憶體區並不是 JVM 管理的記憶體區域的一部分,而是其之外的。該區域也會在 Java 開發中使用到,並且存在導致記憶體溢出的隱患,如果你對 NIO 有所瞭解,可能會知道 NIO 是可以使用 Native Methods 來使用直接記憶體區的。
在 JDK 1.4 中新加入了 NIO 類,引入了一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,它可以使用 Native 函數庫直接分配堆外記憶體,然後通過一個存儲在 Java 堆里的 DirectByteBuffer 對象作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高性能,因為避免了在 Java 堆和 Native 堆中來回覆制數據。