一、記憶體模型及分區 JVM 分為堆區和棧區,還有方法區,初始化的對象放在堆裡面,引用放在棧裡面,class 類信息常量池(static 常量和 static 變數)等放在方法區。 1、棧(Stack-線程私有) 1.1 棧的結構是棧幀組成的,調用一個方法就壓入一幀,幀上面存儲局部變數表,操作數棧,方 ...
一、記憶體模型及分區
JVM 分為堆區和棧區,還有方法區,初始化的對象放在堆裡面,引用放在棧裡面,class 類信息常量池(static 常量和 static 變數)等放在方法區。
1.1、棧(Stack-線程私有)
1.1.1 棧的結構是棧幀組成的,調用一個方法就壓入一幀,幀上面存儲局部變數表,操作數棧,方法出口等信息,每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。局部變數表存放的是 8 大基礎類型加上一個引用類型,所以還是一個指向地址的指針。操作數棧的作用主要用來存儲運算結果以及運算的操作數,它不同於局部變數表通過索引來訪問,而是壓棧和出棧的方式。
棧幀(Stack Frame)是用來存儲數據和部分過程結果的數據結構,同時也被用來處理動態鏈接(Dynamic Linking)、 方法返回值和異常分派( Dispatch Exception)。棧幀隨著方法調用而創建,隨著方法結束而銷毀——無論方法是正常完成還是異常完成(拋出了在方法內未被捕獲的異常)都算作方法結束。
1.1.2 JVM為每個線程創建一個棧,用於存儲該線程執行方法的信息(實際參數、局部變數等),與線程的生命周期相同。
1.1.3 棧的特性:後進先出;由系統自動分配速度快且棧是一個連續的存儲空間。1.2、堆(Heep-線程共用)---運行時數據區
1.2.1 是被線程共用的一塊記憶體區域,創建的對象和數組都保存在 Java 堆記憶體中,也是垃圾收集器進行垃圾收集的最重要的記憶體區域。由於現代 VM 採用分代收集演算法, 因此 Java 堆從 GC 的角度還可以細分為: 新生代(Eden 區、From Survivor 區和 To Survivor 區)和老年代。
1.2.2 JVM只有一個堆,被所有線程所共用。
1.2.3 堆是一個不連續的存儲空間,分配靈活但速度慢。
1.3、方法區/永久代(線程共用):
1.3.1 主要是存儲類信息,常量池(static 常量和 static 變數),編譯後的代碼(位元組碼)等數據 (永遠不變或唯一的內容) 。
1.3.2 我們常說的永久代(Permanent Generation), 用於存儲被 JVM 載入的類信息、常量、靜態變數、即時編譯器編譯後的代碼等數據. HotSpot VM把GC分代收集擴展至方法區, 即使用Java堆的永久代來實現方法區, 這樣 HotSpot 的垃圾收集器就可以像管理 Java 堆一樣管理這部分記憶體,而不必為方法區開發專門的記憶體管理器(永久帶的記憶體回收的主要目標是針對常量池的回收和類型的卸載, 因此收益一般很小)
1.3.3 運行時常量池(Runtime Constant Pool)是方法區的一部分。Class 文件中除了有類的版本、欄位、方法、介面等描述等信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各種字常量和符號引用,這部分內容將在類載入後存放到方法區的運行時常量池中。 Java 虛擬機對 Class 文件的每一部分(自然也包括常量池)的格式都有嚴格的規定,每一個位元組用於存儲哪種數據都必須符合規範上的要求,這樣才會被虛擬機認可、裝載和執行。
1.3.4 JVM只有一個方法區,被所有線程共用。
1.3.5 方法區實際上也是堆,只是用來存儲類、常量相關的信息。
1.4、本地方法棧(線程私有)
本地方法區和 Java Stack 作用類似, 區別是虛擬機棧為執行 Java 方法服務, 而本地方法棧則為Native 方法服務, 如果一個 VM 實現使用 C-linkage 模型來支持 Native 調用, 那麼該棧將會是一個C 棧,但 HotSpot VM 直接就把本地方法棧和虛擬機棧合二為一。1.5、程式計數器(線程私有)
一塊較小的記憶體空間, 是當前線程所執行的位元組碼的行號指示器,每條線程都要有一個獨立的程式計數器,這類記憶體也稱為“線程私有”的記憶體。正在執行 java 方法的話,計數器記錄的是虛擬機位元組碼指令的地址(當前指令的地址)。如果還是 Native 方法,則為空。這個記憶體區域是唯一一個在虛擬機中沒有規定任何 OutOfMemoryError 情況的區域。
二、Minor GC、Major GC、Full GC
2.1 Minor GC
新生代 GC(Minor GC):從年輕代空間(包括 Eden 和 Survivor 區域)回收記憶體被稱為 Minor GC,因為 Java 對象大多都具備朝生夕滅的特性,所以 Minor GC 非常頻繁,一般回收速度也比較快。這一定義既清晰又易於理解。但是,當發生Minor GC事件的時候,有一些有趣的地方需要註意到:
2.1.1 當 JVM 無法為一個新的對象分配空間時會觸發 Minor GC,比如當 Eden 區滿了。所以分配率越高,越頻繁執行 Minor GC。
2.1.2 記憶體池被填滿的時候,其中的內容全部會被覆制,指針會從0開始跟蹤空閑記憶體。Eden 和 Survivor 區進行了標記和複製操作,取代了經典的標記、掃描、壓縮、清理操作。所以 Eden 和 Survivor 區不存在記憶體碎片。寫指針總是停留在所使用記憶體池的頂部。
2.1.3 執行 Minor GC 操作時,不會影響到永久代。從永久代到年輕代的引用被當成 GC roots,從年輕代到永久代的引用在標記階段被直接忽略掉。
2.1.4 所有的 Minor GC 都會觸發“全世界的暫停(stop-the-world)”,停止應用程式的線程。對於大部分應用程式,停頓導致的延遲都是可以忽略不計的。其中的真相就 是,大部分 Eden 區中的對象都能被認為是垃圾,永遠也不會被覆制到 Survivor 區或者老年代空間。如果正好相反,Eden 區大部分新生對象不符合 GC 條件,Minor GC 執行時暫停的時間將會長很多。
所以 Minor GC 的情況就相當清楚了——每次 Minor GC 會清理年輕代的記憶體。
2.1.5 觸發機制:當年輕代滿時就會觸發Minor GC,這裡的年輕代滿指的是Eden代滿,Survivor滿不會引發GC。
2.2 Major GC
Major GC清理Tenured區,用於回收老年代,出現Major GC通常會出現至少一次Minor GC。
Major GC的觸發條件:當年老代空間不夠用的時候,虛擬機會使用“標記—清除”或者“標記—整理”演算法清理出連續的記憶體空間,分配對象使用。
註意:也有認為是和full GC是等價的,收集整個GC堆。但因為HotSpot VM發展了這麼多年,外界對各種名詞的解讀已經完全混亂了,當有人說“major GC”的時候一定要問清楚他想要指的是上面的old GC(只清理老年代)還是 full GC。
2.3 Full GC
Full GC是針對整個新生代、老生代、元空間(metaspace,java8以上版本取代perm gen)的全局範圍的GC。Full GC不等於Major GC,也不等於Minor GC+Major GC,發生Full GC需要看使用了什麼垃圾收集器組合,才能解釋是什麼樣的垃圾回收。
Full GC觸發條件:
2.3.1 調用System.gc時,系統建議執行Full GC,但是不必然執行
2.3.2 老年代空間不足
2.3.3 方法去空間不足
2.3.4 通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體
2.3.5 由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用記憶體,則把該對象轉存到老年代,且老年代的可用記憶體小於該對象大小
三、JVM運行時記憶體
Java 堆從 GC 的角度還可以細分為: 新生代(Eden 區、From Survivor 區和 To Survivor 區)和老年代。3.1、新生代
是用來存放新生的對象。一般占據堆的 1/3 空間。由於頻繁創建對象,所以新生代會頻繁觸發MinorGC 進行垃圾回收。新生代又分為 Eden 區、ServivorFrom、ServivorTo 三個區。
3.1.1 Eden 區
Java 新對象的出生地(如果新創建的對象占用記憶體很大,則直接分配到老年代)。當 Eden 區記憶體不夠的時候就會觸發 MinorGC,對新生代區進行一次垃圾回收。
3.1.2 ServivorFrom 區
上一次 GC 的幸存者,作為這一次 GC 的被掃描者。
3.1.3 ServivorTo區
保留了一次 MinorGC 過程中的幸存者。
3.1.4 MinorGC 的過程(複製->清空->互換)
MinorGC 採用複製演算法。
3.1.4.1 eden、servivorFrom 複製到 ServivorTo,年齡+1
首先,把 Eden 和 ServivorFrom 區域中存活的對象複製到 ServivorTo 區域(如果有對象的年齡已經達到了老年的標準,則賦值到老年代區),同時把這些對象的年齡+1(如果 ServivorTo 不夠位置了就放到老年區);虛擬機會給每個對象定義一個對象年齡(Age)計數器,對象在Survivor區中每“熬過”一次GC,年齡就會+1。待到年齡到達一定歲數(預設是15歲),虛擬機就會將對象移動到年老代。
3.1.4.2 清空 eden、servivorFrom
然後,清空 Eden 和 ServivorFrom 中的對象;
3.1.4.3 ServivorTo 和 ServivorFrom 互換
最後,ServivorTo 和 ServivorFrom 互換,原 ServivorTo 成為下一次 GC 時的 ServivorFrom區。
3.2、老年代
主要存放應用程式中生命周期長的記憶體對象。
老年代的對象比較穩定,所以 MajorGC 不會頻繁執行。在進行 MajorGC 前一般都先進行了一次 MinorGC,使得有新生代的對象晉身入老年代,導致空間不夠用時才觸發。當無法找到足夠大的連續空間分配給新創建的較大對象時也會提前觸發一次 MajorGC 進行垃圾回收騰出空間。
MajorGC 採用標記清除演算法:首先掃描一次所有老年代,標記出存活的對象,然後回收沒有標記的對象。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產生記憶體碎片,為了減少記憶體損耗,我們一般需要進行合併或者標記出來方便下次直接分配。當老年代也滿了裝不下的時候,就會拋出 OOM(Out of Memory)異常。