歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> JVM運行時內存結構

JVM運行時內存結構

日期:2017/2/27 15:56:22   编辑:Linux教程

1.JVM內存模型
JVM運行時內存=共享內存區+線程內存區

1).共享內存區
共享內存區=持久帶+堆
持久帶=方法區+其他
堆=Old Space+Young Space
Young Space=Eden+S0+S1

JVM用持久帶(Permanent Space)實現方法區,主要存放所有已加載的類信息,方法信息,常量池等等。可通過-XX:PermSize和-XX:MaxPermSize來指定持久帶初始化值和最大值。

堆,主要用來存放類的對象實例信息。堆分為Old Space(又名,Tenured Generation)和Young Space。

Old Space主要存放應用程序中生命周期長的存活對象;Eden(伊甸園)主要存放新生的對象;S0和S1是兩個大小相同的內存區域,主要存放每次垃圾回收後Eden存活的對象,作為對象從Eden過渡到Old Space的緩沖地帶(S是指英文單詞Survivor Space)。

2).線程內存區
線程內存區=單個線程內存+單個線程內存+.......
單個線程內存=PC Regster+JVM棧+本地方法棧
JVM棧=棧幀+棧幀+.....
棧幀=局域變量區+操作數區+幀數據區

在Java中,一個線程會對應一個JVM棧(JVM Stack),JVM棧裡記錄了線程的運行狀態。

JVM棧以棧幀為單位組成,一個棧幀代表一個方法調用。棧幀由三部分組成:局部變量區、操作數棧、幀數據區。

(1)局部變量區
局部變量區,可以理解為一個以數組形式進行管理的內存區,從0開始計數,每個局部變量的空間是32位的,即4字節。
基 本類型byte、char、short、boolean、int、float及對象引用等占一個局部變量空間,類型為short、byte和char的值 在存入數組前要被轉換成int值;long、double占兩個局部變量空間,在訪問long和double類型的局部變量時,只需要取第一個變量空間的 索引即可,。
例如:

public static int runClassMethod(int i,long l,float f,double d,Object o,byte b) {   
    return 0;   
}   
            
public int runInstanceMethod(char c,double d,short s,boolean b) {   
    return 0;   
}

對應的局域變量區是:

runInstanceMethod的局部變量區第一項是個reference(引用),它指定的就是對象本身的引用,也就是我們常用的this,但是在runClassMethod方法中,沒這個引用,那是因為runClassMethod是個靜態方法。

(2)操作數棧
操作數棧和局部變量區一樣,也被組織成一個以字長為單位的數組。但和前者不同的是,它不是通過索引來訪問的,而是通過入棧和出棧來訪問的。操作數棧是臨時數據的存儲區域。

例如:

int a= 100;
int b =5;
int c = a+b;

對應的操作數棧變化為:

從圖中可以得出:操作數棧其實就是個臨時數據存儲區域,它是通過入棧和出棧來進行操作的。

PS:JVM實現裡,有一種基於棧的指令集(Hotspot,oracle JVM),還有一種基於寄存器的指令集(DalvikVM,安卓 JVM),兩者有什麼區別的呢?

基於棧的指令集有接入簡單、硬件無關性、代碼緊湊、棧上分配無需考慮物理的空間分配等優勢,但是由於相同的操作需要更多的出入棧操作,因此消耗的內存更大。 而基於寄存器的指令集最大的好處就是指令少,速度快,但是操作相對繁瑣。

示例:

package com.demo3;
 
public class Test {
 
    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = (a + b) * 5;
    }
}

基於棧的Hotspot的執行過程如下:

基於寄存器的DalvikVM執行過程如下所示:

上述兩種方式最終通過JVM執行引擎,CPU接收到的匯編指令是:

(3)幀數據區
幀數據區存放了指向常量池的指針地址,當某些指令需要獲得常量池的數據時,通過幀數據區中的指針地址來訪問常量池的數據。此外,幀數據區還存放方法正常返回和異常終止需要的一些數據。

2.垃圾回收機制
1)、為什麼要垃圾回收
JVM自動檢測和釋放不再使用的內存,提高內存利用率。
Java 運行時JVM會執行 GC,這樣程序員不再需要顯式釋放對象。
2)、垃圾回收(GC)的分類

Minor GC(次要回收)
Full GC(主要回收)

3)、垃圾回收(GC)的產生過程
(1)新生成的對象在Eden區完成內存分配;
(2) 當Eden區滿了,再創建對象,會因為申請不到空間,觸發minorGC,進行young(eden+1survivor)區的垃圾回收。(為什麼是 eden+1survivor:兩個survivor中始終有一個survivor是空的,空的那個被標記成To Survivor);
(3)minorGC 時,Eden不能被回收的對象被放入到空的survivor(也就是放到To Survivor,同時Eden肯定會被清空),另一個survivor(From Survivor)裡不能被GC回收的對象也會被放入這個survivor(To Survivor),始終保證一個survivor是空的。(MinorGC完成之後,To Survivor 和 From Survivor的標記互換);
(4)當做第3步的時候,如果發現存放對象的那個survivor滿了,則這些對象被copy到old區,或者survivor區沒有滿,但是有些對象已經足夠Old(通過XX:MaxTenuringThreshold參數來設置),也被放入Old區;
(5)當Old區被放滿的之後,進行完整的垃圾回收,即 Full GC;
(6)Full GC時,整理的是Old Generation裡的對象,把存活的對象放入到Permanent Generation裡。

JDK官方監控工具visualvm中visual gc插件圖:

4)、什麼情況下觸發垃圾回收
一 般情況下,當新對象生成,並且在Eden申請空間失敗時,就會觸發Minor GC,對Eden區域進行GC,清除非存活對象,並且把尚且存活的對象移動到Survivor區。然後整理Survivor的兩個區。這種方式的GC是對 年輕代的Eden區進行,不會影響到年老代。因為大部分對象都是從Eden區開始的,同時Eden區不會分配的很大,所以Eden區的GC會頻繁進行。因 而,一般在這裡需要使用速度快、效率高的算法,使Eden去能盡快空閒出來。

Full GC因為需要對整個堆進行回收,包括Young、Old和Perm,所以比Minor GC要慢,因此應該盡可能減少Full GC的次數。在對JVM調優的過程中,很大一部分工作就是對於FullGC的調節。

有如下原因可能導致Full GC:

  • 年老代(Tenured)被寫滿

  • 持久代(Perm)被寫滿

  • System.gc()被顯示調用

  • 上一次GC之後Heap的各域分配策略動態變化

5)、垃圾回收典型算法
(1).Mark-Sweep(標記-清除)算法

(2).Copying(復制)算法

(3).Mark-Compact(標記-整理)算法

(4).Generational Collection(分代收集)算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據對象存活的生命周期將內存劃分為若干個不同的區域。一般情況下將堆區劃分為老年 代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根 據不同代的特點采取最適合的收集算法。

目前大部分垃圾收集器對於新生代都采取Copying算法。而由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。

6)、垃圾收集器
垃圾收集算法是內存回收的理論基礎,而垃圾收集器就是內存回收的具體實現。
(1).Serial/Serial Old
Serial/Serial Old收集器是最基本最古老的收集器,它是一個單線程收集器,並且在它進行垃圾收集時,必須暫停所有用戶線程。Serial收集器是針對新生代的收集器, 采用的是Copying算法,Serial Old收集器是針對老年代的收集器,采用的是Mark-Compact算法。它的優點是實現簡單高效,但是缺點是會給用戶帶來停頓。

(2).ParNew
ParNew收集器是Serial收集器的多線程版本,使用多個線程進行垃圾收集。

(3).Parallel Scavenge
Parallel Scavenge收集器是一個新生代的多線程收集器(並行收集器),它在回收期間不需要暫停其他用戶線程,其采用的是Copying算法,該收集器與前兩個收集器有所不同,它主要是為了達到一個可控的吞吐量。

(4).Parallel Old
Parallel Old是Parallel Scavenge收集器的老年代版本(並行收集器),使用多線程和Mark-Compact算法。

(5).CMS
CMS(Current Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,它是一種並發收集器,采用的是Mark-Sweep算法。

(6).G1
G1收集器是當今收集器技術發展最前沿的成果,它是一款面向服務端應用的收集器,它能充分利用多CPU、多核環境。因此它是一款並行與並發收集器,並且它能建立可預測的停頓時間模型。

3.JVM參數
1).堆

-Xmx:最大堆內存
-Xms:初始時堆內存
-XX:MaxNewSize:最大年輕區內存
-XX:NewSize:初始時年輕區內存
-XX:MaxPermSize:最大持久帶內存
-XX:PermSize:初始時持久帶內存

2).棧
-xss:設置每個線程的堆棧大小

3).垃圾回收

4.堆 VS 棧
JVM棧是運行時的單位,而JVM堆是存儲的單位。
JVM棧代表了處理邏輯,而JVM堆代表了數據。
JVM堆中存的是對象。JVM棧中存的是基本數據類型和JVM堆中對象的引用。
JVM堆是所有線程共享,JVM棧是線程獨有。
PS:Java中的參數傳遞是傳值呢?還是傳址?
我們都知道:C 語言中函數參數的傳遞有:值傳遞,地址傳遞,引用傳遞這三種形式。但是在Java裡,方法的參數傳遞方式只有一種:值傳遞。所謂值傳遞,就是將實際參數值的副本(復制品)傳入方法內,而參數本身不會受到任何影響。

要說明這個問題,先要明確兩點:
1.引用在Java中是一種數據類型,跟基本類型int等等同一地位。
2.程序運行永遠都是在JVM棧中進行的,因而參數傳遞時,只存在傳遞基本類型和對象引用的問題。不會直接傳對象本身。
在 運行JVM棧中,基本類型和引用的處理是一樣的,都是傳值。如果是傳引用的方法調用,可以理解為“傳引用值”的傳值調用,即“引用值”被做了一個復制品, 然後賦值給參數,引用的處理跟基本類型是完全一樣的。但是當進入被調用方法時,被傳遞的這個引用值,被程序解釋(或者查找)到JVM堆中的對象,這個時候 才對應到真正的對象。如果此時進行修改,修改的是引用對應的對象,而不是引用本身,即:修改的是JVM堆中的數據。所以這個修改是可以保持的了。
例如:

package com.demo3;
 
public class DataWrap {
    public int a;
    public int b;
}
 
package com.demo3;
 
public class ReferenceTransferTest {
    public static void swap(DataWrap dw) {
        int tmp = dw.a;
        dw.a = dw.b;
        dw.b = tmp;
    }
 
    public static void main(String[] args) {
        DataWrap dw = new DataWrap();
        dw.a = 6;
        dw.b = 9;
        swap(dw);
    }
}

對應的內存圖:

附:JVM詳細圖

Copyright © Linux教程網 All Rights Reserved