JVM學習——(二)內存模型

一、運行時數據區各區域的聯繫

1.1 棧幀

上一篇說過虛擬機棧一個線程執行的區域,是線程私有的。

可以這樣理解,每個線程對應一個虛擬機棧,這個線程中的每個方法對應一個棧幀。

 

那麼棧幀中是什麼內容呢?

每個棧幀中包含局部變量表、操作數棧執行運行時常量池的引用、方法返回地址和附加信息。

局部變量表:方法中定義的局部變量以及方法的參數存放在這張表中。

局部變量表中的變量不可直接使用,如果需要使用,必須通過相關指令將其價值至操作數棧中,作爲操作數使用。

操作數棧:以壓棧和出棧的方式存儲操作數。

方法返回地址:當一個方法開始執行後,只有兩種方式可以退出,一是遇到方法返回的字節碼指令;二是遇見異常,並且異常沒有在方法體內被處理。

 

1.2 棧指向堆

如果在棧幀中有一個變量,類型爲引用類型。比如Object  obj=new Object(),這種情況就是典型的棧中元素指向堆中的對象。

1.3 方法區指向堆

方法區中會存放靜態變量、常量等數據。如下面的例子,就是典型的方法區中元素指向堆中的對象。

private static Object obj=new Object();

1.4 堆指向方法區

方法區中會包含類的信息,堆中會有對象,那怎麼知道對象是那個類創建的呢?

對象的類的信息存在方法區中,這種情況就是堆指向方法區。

 

1.5 Java對象內存佈局

一個Java對象在內存中包括3個部分:對象頭、實例數據和對齊填充。

二、內存模型

2.1 圖解

一塊是非堆區,一堆是堆區。

堆區分爲兩大塊,一個是Old區,一個是Young區。

Young區分爲兩大塊,一塊是Survivor區,一塊是Eden區。

Survivor區又劃分爲s0和s1兩塊。s0和s1一樣大,也可以叫From區和To區。

Eden:s0:s1=8:1:1。

 

2.2 對象創建所在區域

根據之前對Heap的描述已經知道了對象和數組的創建會在堆中分配內存空間,堆中這麼多區域,那麼一個對象的創建到底在哪個區域呢?

一般新創建的對象都會被分配到Eden區,一些特殊的大的對象會直接分配到Old區。

舉個例子,比如對象A、B、C創建在Eden區,但是Eden區的內存空間是有限的,比如有100M。假如已經使用了100M或者達到一個設定的臨界值,這時候就需要對Eden區的內存空間進行清理,即垃圾收集(Garbage Collect),對於這樣的GC稱之爲Minor GC,Minor GC指的就是Young區的GC。

經過GC之後,有些對象就會被清理掉,有些對象可能還存活,對於存活的對象需要將其複製到Survior區,然後再清空Eden區中這些對象。

 

2.3 Survivor區詳解

由圖解可以看出,Survivor區劃分爲兩塊,S0和S1,也可以稱爲From和To。

接着Minor GC繼續說,在同一時間點上,S0和S1只能有一個區域有數據,另外一個是空的。
比如一開始只有Eden區和From中有對象,To中是空的。
此時進行一次GC操作,From區中對象的年齡就會+1,Eden區中所有存活的對象都會被複制到To區,
From區中還能存活的對象會有兩個去處。
若對象年齡達到設定的年齡閾值,此時對象會被移動到Old區。
若Eden區和From區中沒有達到閾值的對象,則會被複制到To區。
此時Eden區和From區已經被清空了(被GC掉的對象已經沒了,沒有被GC掉還存活的對象都有了去處)。
這時From和To交換角色,之前的From變成了To,之前的To則變成From。
也就是說,無論如何都要保證下一次名爲To的Survivor區域是空的。
Minor GC一直重複這樣的過程,直到To區被填滿,然後會將所有對象複製到老年代中。

概括的說,就是對Eden區和From區進行清理,年齡達限的對象直接進入老年代Old區,未達限的對象都進入To區。

From區被清空後和To區轉換角色,空了的From區將作爲下一次的To區,供下一次GC後存放存活的對象使用。

 

2.4 Old區詳解

從之前的分析可以看出,一般Old區都是年齡比較大的對象,或者超過了設定閾值的對象。

在Old區也會有GC的操作,Old區的GC我們成爲Major GC。

 

2.5 對象的一輩子

圖解對象的GC過程:

 

2.6 問題歸納

如何理解Minor/Major/Full GC?

Minor GC:新生代的GC

Major GC:老年代的GC

Full GC:新、老年代的GC

 

爲什麼需要Survivor?只有Eden不行嗎?
如果沒有Survivor,Eden區每進行一次Minor GC,並且沒有年齡限制的話,存活的對象都會被送入老年代。
這樣老年代很快就會被填滿,從而觸發Major GC(因爲Major GC一般伴隨着Minor GC,也可以看做觸發了Full GC)。
老年代內存空間遠大於新生代,進行一次FullGC消耗的時間比Minor GC長的多。
執行時間長有什麼壞處?頻繁的FullGC消耗的時間很長,會影響應用程序的執行和響應速度。
 
那麼如果增加或者減少老年代的空間可以解決嗎?
假如增加老年帶空間,更多存活對象才能填滿老年代。雖然降低了FullGC的頻率,但是隨着老年代空間變大,一旦發生FullGC,執行所需要的時間更長。
假如減少老年代的時間,雖然FullGC所需時間減少,但是老年代很快被存活對象填滿,FullGC頻率增加,也會出問題。
 
爲什麼需要兩個Survivor區?
最大的好處就是解決了碎片化。
假如只有一個Survivor區,
剛剛新建的對象在Eden中,一旦Eden滿了,觸發一次MinorGC,Eden中的存活對象就會被移動到Survivor區。
這樣繼續循環下去,下一次Eden滿了的時候,問題來了,
此時進行MinorGC,Eden和Survivor各有一些存活對象,如果此時把Eden區的存活對象硬放到Survivor區,這兩部分對象所戰友的內存是不聯繫的,導致了內存碎片化。
永遠有一個SurvivorSpace是空的,另一個非空的SurvivorSpace無碎片。
 
新生代中Eden:S1:S2爲什麼是8:1:1?
新生代中的可用內存:複製算法用來擔保的內存爲9:1
可用內存中Eden:S1爲8:1
即新生代中Eden:S1:S2=8:8:1
 

四、模擬內存溢出

 

4.1 堆內存溢出

設置堆內存大小參數比如-Xmx20M -Xms20M 以便儘快達到堆內存溢出大小
@RestController 
public class HeapController {

 List<Person> list=new ArrayList<Person>();

 @GetMapping("/heap")
 public String heap() throws Exception{
     while(true){
         list.add(new Person()); 
         Thread.sleep(1); 
       } 
    } 

}

結果:Exception in thread "http-nio-8080-exec-2" java.lang.OutOfMemoryError: GC overhead limit exceeded

4.2 虛擬機棧溢出

public class StackDemo {
    public static long count=0;
    public static void method(long i){
        System.out.println(count++);
        method(i); 
    }

    public static void main(String[] args) {
        method(1); 
    } 
}

結果:

 
Stack Space用來做方法的遞歸調用時壓入Stack Frame(棧幀)。所以當遞歸調用太深的時候,就有可能耗盡Stack
Space,爆出StackOverflow的錯誤。
 
-Xss128k:設置每個線程的堆棧大小。JDK 5以後每個線程堆棧大小爲1M,以前每個線程堆棧大小爲256K。根據應用的線
程所需內存大小進行調整。在相同物理內存下,減小這個值能生成更多的線程。但是操作系統對一個進程內的線程數還是有
限制的,不能無限生成,經驗值在3000~5000左右。
線程棧的大小是個雙刃劍,如果設置過小,可能會出現棧溢出,特別是在該線程內有遞歸、大的循環時出現溢出的可能性更
大,如果該值設置過大,就有影響到創建棧的數量,如果是多線程的應用,就會出現內存溢出的錯誤。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章