一. JVM 內存區域
- JVM 內存區域主要分為線程私有區域【程序計數器、虛擬機棧、本地方法區】、線程共享區域【JAVA 堆、方法區】、直接內存。
- 線程私有數據區域生命周期與線程相同, 依賴用戶線程的啟動/結束 而 創建/銷毀(在 HotspotVM 內, 每個線程都與操作系統的本地線程直接映射, 因此這部分內存區域的存/否跟隨本地線程的生/死對應)。
- 線程共享區域隨虛擬機的啟動/關閉而創建/銷毀。
- 直接內存并不是 JVM 運行時數據區的一部分, 但也會被頻繁的使用: 在 JDK 1.4 引入的 NIO 提供了基于 Channel 與 Buffer 的 IO 方式, 它可以使用 Native 函數庫直接分配堆外內存, 然后使用DirectByteBuffer 對象作為這塊內存的引用進行操作(詳見: Java I/O 擴展), 這樣就避免了在 Java堆和 Native 堆中來回復制數據, 因此在一些場景中可以顯著提高性能。
1. 程序計數器(線程私有)
- 一塊較小的內存空間, 是當前線程所執行的字節碼的行號指示器,每條線程都要有一個獨立的程序計數器,這類內存也稱為“線程私有”的內存。
- 正在執行 java 方法的話,計數器記錄的是虛擬機字節碼指令的地址(當前指令的地址)。如果還是 Native 方法,則為空。
- 這個內存區域是唯一一個在虛擬機中沒有規定任何 OutOfMemoryError 情況的區域。
2. 虛擬機棧(線程私有)
- 是描述 java 方法執行的內存模型,每個方法在執行的同時都會創建一個棧幀(Stack Frame)用于存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
- 棧幀( Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接(Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。棧幀隨著方法調用而創建,隨著方法結束而銷毀——無論方法是正常完成還是異常完成(拋出了在方法內未被捕獲的異常)都算作方法結束。
3. 本地方法區(線程私有)
- 本地方法區和 Java Stack 作用類似, 區別是虛擬機棧為執行 Java 方法服務, 而本地方法棧則為Native 方法服務, 如果一個 VM 實現使用 C-linkage 模型來支持 Native 調用, 那么該棧將會是一個C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機棧合二為一。
4. 堆(Heap-線程共享)-運行時數據區
- 是被線程共享的一塊內存區域,創建的對象和數組都保存在 Java 堆內存中,也是垃圾收集器進行垃圾收集的最重要的內存區域。由于現代 VM 采用分代收集算法, 因此 Java 堆從 GC 的角度還可以細分為: 新生代(Eden 區、From Survivor 區和To Survivor 區)和老年代。
5. 方法區/永久代(線程共享)
- 即我們常說的永久代(Permanent Generation), 用于存儲被 JVM 加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據. HotSpot VM 把 GC 分代收集擴展至方法區, 即使用 Java堆的永久代來實現方法區, 這樣 HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分內存,而不必為方法區開發專門的內存管理器(永久帶的內存回收的主要目標是針對常量池的回收和類型的卸載, 因此收益一般很小)。
- 運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述等信息外,還有一項信息是常量池(Constant Pool Table),用于存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。 Java 虛擬機對 Class 文件的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個字節用于存儲哪種數據都必須符合規范上的要求,這樣才會被虛擬機認可、裝載和執行。
二. JVM 運行時內存
Java 堆從 GC 的角度還可以細分為: 新生代(Eden 區、From Survivor 區和To Survivor 區)和老年代。
1. 新生代
是用來存放新生的對象。一般占據堆的 1/3 空間。由于頻繁創建對象,所以新生代會頻繁觸發MinorGC 進行垃圾回收。新生代又
分為 Eden 區、ServivorFrom、ServivorTo 三個區。
1.1. Eden 區
Java 新對象的出生地(如果新創建的對象占用內存很大,則直接分配到老年代)。當 Eden 區內存不夠的時候就會觸發 MinorGC,
對新生代區進行一次垃圾回收。
1.2. ServivorFrom
上一次 GC 的幸存者,作為這一次 GC 的被掃描者。
1.3. ServivorTo
保留了一次 MinorGC 過程中的幸存者。
1.4. MinorGC 的過程(復制->清空->互換)
MinorGC 采用復制算法。
1:eden、servicorFrom復制到 ServicorTo,年齡+1
首先,把 Eden 和 ServivorFrom 區域中存活的對象復制到 ServicorTo 區域(如果有對象的年齡以及達到了老年的標準,則賦值
到老年代區),同時把這些對象的年齡+1(如果 ServicorTo 不夠位置了就放到老年區);
2:清空 eden、servicorFrom
然后,清空 Eden 和 ServicorFrom 中的對象;
3:ServicorTo和 ServicorFrom互換
最后,ServicorTo 和 ServicorFrom 互換,原 ServicorTo 成為下一次 GC 時的 ServicorFrom區。
2. 老年代
- 主要存放應用程序中生命周期長的內存對象。
- 老年代的對象比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行了一次 MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。MajorGC 采用標記清除算法:首先掃描一次所有老年代,標記出存活的對象,然后回收沒有標記的對象。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產生內存碎片,為了減少內存損耗,我們一般需要進行合并或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)異常。
3. 永久代
- 指內存的永久保存區域,主要存放 Class 和 Meta(元數據)的信息,Class 在被加載的時候被放入永久區域,它和和存放實例的區域不同,GC 不會在主程序運行期對永久區域進行清理。所以這也導致了永久代的區域會隨著加載的 Class 的增多而脹滿,最終拋出 OOM 異常。
3.1. JAVA8 與元數據
- 在 Java8 中,永久代已經被移除,被一個稱為“元數據區”(元空間)的區域所取代。元空間的本質和永久代類似,元空間與永久代之間最大的區別在于:元空間并不在虛擬機中,而是使用本地內存。因此,默認情況下,元空間的大小僅受本地內存限制。類的元數據放入 nativememory, 字符串池和類的靜態變量放入 java 堆中,這樣可以加載多少類的元數據就不再由MaxPermSize 控制, 而由系統的實際可用空間來控制。