1 內存布局總體結構
根據 JVM 規范,JVM 內存共分為虛擬機棧(Virtual Machine Stacks)、堆(Heap)、方法區(Method Area)、程序計數器(Program Counter Registers)、本地方法棧(Native Method Stacks)五個部分。
-
Java 8 之前在堆(Heap)中除了年輕代(YongGen)、老年代(OldGen)之外還存在一個永久代(PremGen)
-
永久代存放:類的元數據、靜態變量和常量
-
方法區(Method Area)存在於永久代之中
-
運行時常量池(Runtime Constant Pool)存在於方法區(Method Area)中
-
-
Java 8 及之后的版本,徹底移除了持久代(PermGen),而使用 元空間(Metaspace) 來進行替代
-
永久代中的 類元信息(class metadata) 轉移到了 本地內存(Native Memory) 而不是虛擬機
-
永久代中的 字符串常量池(interned Strings) 和 類靜態變量(class static variables) 轉移到了堆( Heap)中
-
永久代參數(PermSize 與 MaxPermSize)失效,替換為元空間參數(MetaspaceSize 與 MaxMetaspaceSize)
-
-
Java 8 為什么要將永久代替換成Metaspace?
-
字符串存在永久代中,容易出現性能問題和內存溢出。
-
類及方法的信息等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出。
-
永久代會為 GC 帶來不必要的復雜度,並且回收效率偏低。
-
Oracle 可能會將 HotSpot 與 JRockit 合二為一,JRockit 沒有所謂的永久代。
-
-
廢除永久代的好處
-
由於類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間
-
將運行時常量池從 PermGen 分離出來,與類的元數據分開,提升類元數據的獨立性。
-
將元數據從 PermGen 剝離出來到 Metaspace,可以提升對元數據的管理同時提升GC效率。
-
2 程序計數器(Program Counter Register)
用於執行引擎在線程切換。在虛擬機的概念模型里,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令、分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。
由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,為了線程切換后能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。
-
如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址
-
如果線程正在執行的是一個Native方法,這個計數器值則為 Undefined
-
程序計數器是線程私有的,它的生命周期與線程相同,每個線程都有一個
3 Java 虛擬機棧(Java Virtual MachineStacks)
Java虛擬機棧(Java Virtual Machine Stacks)是線程私有的,生命周期和線程相同。
Java虛擬機棧和線程同時創建,用於存儲棧幀(Stack Frame)。每個方法在執行時都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。
棧里的每條數據,就是棧幀。在每個 Java 方法被調用的時候,都會創建一個棧幀,並入棧。一旦完成相應的調用,則出棧。所有的棧幀都出棧后,線程也就結束了。每個棧幀,都包含四個區域:
-
局部變量表(Local Variable Table):用於存放方法參數和方法內定義的局部變量。 包括8種基本數據類型、對象引用和returnAddress類型(指向一條字節碼指令的地址)。其中64位長度的long和double類型的數據會占用2個局部變量空間(Slot),其余的數據類型只占用1個。
-
操作數棧(Operand Stack):是一個后入先出棧(LIFO)。隨着方法執行和字節碼指令的執行,會從局部變量表或對象實例的字段中復制常量或變量寫入到操作數棧,再隨着計算的進行將棧中元素出棧到局部變量表或者返回給方法調用者,也就是出棧/入棧操作。
-
動態連接(Dynamic Linking):將符號引用轉換成直接引用。
-
返回地址(Return Address):方法正常退出時,調用者的PC計數器的值作為返回地址,即調用該方法的指令的下一條指令的地址。而通過異常退出的,返回地址是要通過異常表來確定,棧幀中一般不會保存這部分信息。
在Java虛擬機規范中,對這個區域規定了兩種異常狀況:
-
如果線程請求的棧深度大於虛擬機所允許的深度,將拋出 StackOverflowError 異常;
-
如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可動態擴展,只不過Java虛擬機規范中也允許固定長度的虛擬機棧),如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。
4 本地方法棧(Native Method Stack)
基本功能與虛擬機棧非常相似,服務的對象是 native 方法。本地方法棧也是線程私有的,它的生命周期與線程相同,每個線程都有一個。
在 HotSpot 虛擬機中直接就把本地方法棧和虛擬機棧合二為一。
會和 Java 虛擬機棧一樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
5 堆(Heap)與 元空間(Metaspace)
堆是什么:
-
在虛擬機啟動的時候創建。
-
堆中的數據是線程所共享的,目的就是存放對象實例。
-
堆是 垃圾收集器管理 的主要區域。
-
堆是虛擬機所管理的內存中最大的一塊,由於現在收集器基本都采用分代收集算法,所以Java堆還可以細分為:新生代和老年代(JDK 1.7以及之前還存在永久代);新生代又可以分為:Eden 空間、From Survivor空間、To Survivor空間,默認占比 8:1:1。
-
堆是計算機物理存儲上不連續的、邏輯上是連續的,也是大小可調節的(可以通過
-Xms
和-Xmx
控制)。 -
方法結束后,堆中對象不會馬上移出僅僅在垃圾回收的時候時候才移除。
-
如果在堆中沒有內存完成實例的分配,並且堆也無法再擴展時,將會拋出OutOfMemoryError異常
另外:從內存分配的角度來看,線程共享的Java堆中可能划分出多個線程私有的分配緩沖區(Thread Local AllocationBuffer,TLAB)。
-
Yong Gen:1個Eden Space和2個Suvivor Space(from 和to)。主要存放新創建的對象,內存大小相對會比較小,垃圾回收會比較頻繁。
-
Old Gen(Tenured Gen): 主要存放JVM認為生命周期比較長的對象(經過幾次的Young Gen的垃圾回收后仍然存在),內存大小相對會比較大,垃圾回收也相對沒有那么頻繁。
-
默認 -XX:NewRatio=2 , 標識新生代占1 , 老年代占2 ,新生代占整個堆的1/3
-
默認 -XX:SurvivorRatio=6,標識Eden 空間:From Survivor空間:To Survivor空間 = 8:1:1
對象分配內存的工作流程圖
GC相關概念
-
form survivor 又稱 s0
-
to survivor 又稱 s1
-
部分收集:Partial GC
-
新生代收集:Minor GC / Young GC
- 年輕代空間不足觸發, 這里年輕代指的是Eden滿。Survivor滿不會引發GC
-
老年代收集:Major GC / Old GC
- 老年代空間不足時,會嘗試觸發MinorGC. 如果空間還是不足,則觸發Major GC,如果Major GC , 內存仍然不足,則報錯OOM
-
-
混合收集:Mixed GC
- G1垃圾回收器會混合回收, region 區域回收
-
整堆收集:Full GC
-
用System.gc() , 系統會執行Full GC ,不是立即執行
-
老年代空間不足時觸發
-
方法區空間不足時觸發
-
關於元空間(Metaspace)的單獨說明
-
在 JDK1.7 之前,HotSpot 虛擬機把方法區當成永久代來進行垃圾回收
-
從 JDK1.8 開始,HotSpot 虛擬機移除永久代,並把方法區移至元空間
-
永久代與元空間的區別
-
永久代在物理上是堆的一部分,和新生代、老年代的地址是連續的,而元空間屬於本地內存
-
在原來的永久代划分中,永久代用來存放類的元數據信息、靜態變量以及常量池等。現在類的元信息存儲在元空間中,靜態變量和常量池等並入堆中。
-
6 方法區(Method Area)與 運行時常量池(Runtime Constant Pool)
在 HotSpot 虛擬機上 GC 分代收集擴展至方法區,使用了永久代來實現方法區(JDK1.7及以前,從 JDK 1.8 開始,移除永久代,並把方法區移至元空間)。
元空間、永久代是方法區具體的落地實現。方法區看作是一塊獨立於Java堆的內存空間,它主要是用來存儲所加載的類信息的。方法區是一個規范,只不過取代永久代的是元空間(Metaspace)。
-
與堆類似,方法區是被各個線程共享的內存區域
-
存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據
類加載器將Class文件加載到內存之后,將類的信息存儲到方法區中
-
類信息:類全名、直接父類的全名、修飾符、實現的接口列表
-
類的屬性信息:名稱、類型、修飾符
-
類的方法信息:返回類型、參數數量和類型、修飾符、字節碼bytecodes、操作數棧、局部變量表及大小(abstract和native方法除外)、異常表
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后進入方法區的運行時常量池中存放。
-
靜態常量池:存放編譯期間生成的各種字面量與符號引用。在字節碼文件中即
.class
文件。 -
運行時常量池:常量池表在運行時的表現形式。在方法區。
-
編譯后的字節碼文件中包含了類型信息、域信息、方法信息等。通過ClassLoader將字節碼文件的常量池中的信息加載到內存中,存儲在了方法區的運行時常量池中。
7 直接內存(Direct Memory)
直接內存(Direct Memory) 並不是虛擬機運行時數據區的一部分。
-
直接內存申請空間耗費更高的性能,當頻繁申請到一定量時尤為明顯
-
直接內存IO讀寫的性能要優於普通的堆內存,在多次讀寫操作的情況下差異明顯
在JDK 1.4中新加入了NIO(New Input/Output) 類, 引入了一種基於通道(Channel) 與緩沖區 (Buffer)的 I/O 方法,它可以使用Native函數庫直接分配堆外內存, 然后通過一個存儲在Java堆里面的 DirectByteBuffer 對象作為這塊內存的引用進行操作。避免了 在Java堆和Native堆中來回復制數據。
直接內存的大小並不受到 JVM 堆大小的限制,甚至不受到 JVM 進程內存大小的限制。它只受限於本機總內存(RAM 及 SWAP 區或者分頁文件)大小以及處理器尋址空間的限制。
DirectBuffer並沒有真正向OS申請分配內存,其最終還是通過調用 Unsafe 的 allocateMemory() 來進行內存分配。不過 JVM 對 Direct Memory 可申請的大小也有限制,可用 -XX:MaxDirectMemorySize=1M
設置,這部分內存不受JVM垃圾回收管理。
內容為之前學習筆記整理,如果有問題請指正!