Java內存區域(運行時數據區域)
Java 內存區域和內存模型是不同的東西,內存區域是指 Jvm 運行時將數據分區域存儲,強調對內存空間的劃分。java
而內存模型(Java Memory Model,簡稱 JMM )是定義了線程和主內存之間的抽象關係,即 JMM 定義了 JVM 在計算機內存(RAM)中的工做方式,若是咱們要想深刻了解Java併發編程,就要先理解好Java內存模型。算法
Java運行時數據區域
衆所周知,Java 虛擬機有自動內存管理機制,若是出現內存泄漏和溢出方面的問題,排查錯誤就必需要了解虛擬機是怎樣使用內存的。編程
下圖是 JDK8 以後的 JVM 內存佈局。安全
JDK8 以前的內存區域圖以下:服務器
java內存區域變化圖數據結構
在 HotSpot JVM 中,永久代中用於存放類和方法的元數據以及常量池,好比
Class
和Method
。每當一個類初次被加載的時候,它的元數據都會放到永久代中。 永久代是有大小限制的,所以若是加載的類太多,頗有可能致使永久代內存溢出,即萬惡的 java.lang.OutOfMemoryError: PermGen ,爲此咱們不得不對虛擬機作調優。 那麼,Java 8 中 PermGen 爲何被移出 HotSpot JVM 了?我總結了兩個主要緣由:多線程
- 因爲 PermGen 內存常常會溢出,引起惱人的 java.lang.OutOfMemoryError: PermGen,所以 JVM 的開發者但願這一塊內存能夠更靈活地被管理,不要再常常出現這樣的 OOM
- 移除 PermGen 能夠促進 HotSpot JVM 與 JRockit VM 的融合,由於 JRockit 沒有永久代。 根據上面的各類緣由,PermGen 最終被移除,方法區移至 Metaspace,字符串常量移至 Java Heap。
程序計數器
程序計數器(Program Counter Register)是一塊較小的內存空間,它能夠看做是當前線程所執行的字節碼的行號指示器。jvm
因爲 Java 虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器內核都只會執行一條線程中的指令。函數
所以,爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存。
若是線程正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是 Native 方法,這個計數器值則爲空(Undefined)。此內存區域是惟一一個在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 狀況的區域。
Java虛擬機棧
與程序計數器同樣,Java 虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。
虛擬機棧描述的是 Java 方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀(Stack Frame,是方法運行時的基礎數據結構)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
在活動線程中,只有位千棧頂的幀纔是有效的,稱爲當前棧幀。正在執行的方法稱爲當前方法,棧幀是方法運行的基本結構。在執行引擎運行時,全部指令都只能針對當前棧幀進行操做。
1. 局部變量表
局部變量表是存放方法參數和局部變量的區域。 局部變量沒有準備階段, 必須顯式初始化。若是是非靜態方法,則在 index[0] 位置上存儲的是方法所屬對象的實例引用,一個引用變量佔 4 個字節,隨後存儲的是參數和局部變量。字節碼指令中的 STORE 指令就是將操做棧中計算完成的局部變呈寫回局部變量表的存儲空間內。
虛擬機棧規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出 StackOverflowError 異常;若是虛擬機棧能夠動態擴展(當前大部分的 Java 虛擬機均可動態擴展),若是擴展時沒法申請到足夠的內存,就會拋出 OutOfMemoryError 異常。
2. 操做棧
操做棧是個初始狀態爲空的桶式結構棧。在方法執行過程當中, 會有各類指令往 棧中寫入和提取信息。JVM 的執行引擎是基於棧的執行引擎, 其中的棧指的就是操 做棧。字節碼指令集的定義都是基於棧類型的,棧的深度在方法元信息的 stack 屬性中。
i++ 和 ++i 的區別:
- i++:從局部變量表取出 i 並壓入操做棧,而後對局部變量表中的 i 自增 1,將操做棧棧頂值取出使用,最後,使用棧頂值更新局部變量表,如此線程從操做棧讀到的是自增以前的值。
- ++i:先對局部變量表的 i 自增 1,而後取出並壓入操做棧,再將操做棧棧頂值取出使用,最後,使用棧頂值更新局部變量表,線程從操做棧讀到的是自增以後的值。
以前之因此說 i++ 不是原子操做,即便使用 volatile 修飾也不是線程安全,就是由於,可能 i 被從局部變量表(內存)取出,壓入操做棧(寄存器),操做棧中自增,使用棧頂值更新局部變量表(寄存器更新寫入內存),其中分爲 3 步,volatile 保證可見性,保證每次從局部變量表讀取的都是最新的值,但可能這 3 步可能被另外一個線程的 3 步打斷,產生數據互相覆蓋問題,從而致使 i 的值比預期的小。
3. 動態連接
每一個棧幀中包含一個在常量池中對當前方法的引用, 目的是支持方法調用過程的動態鏈接。
4.方法返回地址
方法執行時有兩種退出狀況:
- 正常退出,即正常執行到任何方法的返回字節碼指令,如 RETURN、IRETURN、ARETURN 等;
- 異常退出。
不管何種退出狀況,都將返回至方法當前被調用的位置。方法退出的過程至關於彈出當前棧幀,退出可能有三種方式:
- 返回值壓入上層調用棧幀。
- 異常信息拋給可以處理的棧幀。
- PC計數器指向方法調用後的下一條指令。
本地方法棧
本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用是很是類似的,它們之間的區別不過是虛擬機棧爲虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。Sun HotSpot 虛擬機直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧同樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
線程開始調用本地方法時,會進入 個再也不受 JVM 約束的世界。本地方法能夠經過 JNI(Java Native Interface)來訪問虛擬機運行時的數據區,甚至能夠調用寄存器,具備和 JVM 相同的能力和權限。 當大量本地方法出現時,勢必會削弱 JVM 對系統的控制力,由於它的出錯信息都比較黑盒。對內存不足的狀況,本地方法棧仍是會拋出 nativeheapOutOfMemory。
JNI 類本地方法最著名的應該是 System.currentTimeMillis()
,JNI使 Java 深度使用操做系統的特性功能,複用非 Java 代碼。 可是在項目過程當中, 若是大量使用其餘語言來實現 JNI , 就會喪失跨平臺特性。
Java堆
對於大多數應用來講,Java 堆(Java Heap)是 Java 虛擬機所管理的內存中最大的一塊。Java 堆是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存。
堆是垃圾收集器管理的主要區域,所以不少時候也被稱作「GC堆」(Garbage Collected Heap)。從內存回收的角度來看,因爲如今收集器基本都採用分代收集算法,因此 Java 堆中還能夠細分爲:新生代和老年代;再細緻一點的有 Eden 空間、From Survivor 空間、To Survivor 空間等。從內存分配的角度來看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。
Java 堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可,當前主流的虛擬機都是按照可擴展來實現的(經過 -Xmx 和 -Xms 控制)。若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出 OutOfMemoryError 異常。
方法區
方法區(Method Area)與 Java 堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。永久代是HotSpot的概念,方法區是Java虛擬機規範中的定義,是一種規範,而永久代是一種實現,一個是標準一個是實現。雖然Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,可是它卻有一個別名叫作 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
Java 虛擬機規範對方法區的限制很是寬鬆,除了和 Java 堆同樣不須要連續的內存和能夠選擇固定大小或者可擴展外,還能夠選擇不實現垃圾收集。垃圾收集行爲在這個區域是比較少出現的,其內存回收目標主要是針對常量池的回收和對類型的卸載。當方法區沒法知足內存分配需求時,將拋出 OutOfMemoryError 異常。
JDK8 以前,Hotspot 中方法區的實現是永久代(Perm),JDK8 開始使用元空間(Metaspace),之前永久代中的字符串常量池移至堆內存,運行時常量池、類信息等其餘內容移至元空間,元空間直接在本地內存分配。
爲何要使用元空間取代永久代的實現?
- 字符串存在永久代中,容易出現性能問題和內存溢出。
- 類及方法的信息等比較難肯定其大小,所以對於永久代的大小指定比較困難,過小容易出現永久代溢出,太大則容易致使老年代溢出。
- 永久代會爲 GC 帶來沒必要要的複雜度,而且回收效率偏低。
- 將 HotSpot 與 JRockit 合二爲一。
運行時常量池
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
通常來講,除了保存 Class 文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。
運行時常量池相對於 Class 文件常量池的另一個重要特徵是具有動態性,Java 語言並不要求常量必定只有編譯期才能產生,也就是並不是預置入 Class 文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的即是 String 類的 intern() 方法。
既然運行時常量池是方法區的一部分,天然受到方法區內存的限制,當常量池沒法再申請到內存時會拋出 OutOfMemoryError 異常。
直接內存
直接內存(Direct Memory)並非虛擬機運行時數據區的一部分,也不是 Java 虛擬機規範中定義的內存區域。
在 JDK 1.4 中新加入了 NIO,引入了一種基於通道(Channel)與緩衝區(Buffer)的 I/O 方式,它可使用 Native 函數庫直接分配堆外內存,而後經過一個存儲在 Java 堆中的 DirectByteBuffer 對象做爲這塊內存的引用進行操做。這樣能在一些場景中顯著提升性能,由於避免了在 Java 堆和 Native 堆中來回複製數據。
顯然,本機直接內存的分配不會受到 Java 堆大小的限制,可是,既然是內存,確定仍是會受到本機總內存(包括 RAM 以及 SWAP 區或者分頁文件)大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置 -Xmx 等參數信息,但常常忽略直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操做系統級的限制),從而致使動態擴展時出現 OutOfMemoryError 異常。
最後,推薦與感謝: 深刻理解Java虛擬機(第2版) 碼出高效:Java開發手冊 Java內存模型原理,你真的理解嗎?) 深刻理解 Java 內存模型