JVM-初見

ML李嘉圖發表於2021-08-19

推薦:

JVM-超全-圖解

JVM-思維導圖

JVM的體系結構

簡化圖:

類載入器

類載入器作用:載入.class檔案

類載入流程(三個階段):

1.載入階段

將編譯好的class檔案載入到記憶體中(方法區),然後會生成一個代表這個類的Class物件。

2.連結階段

會為靜態變數分配記憶體並設定預設值。

3.初始化階段

執行類構造器()進行初始化賦值。

java自帶的類載入器:

  • 啟動類載入器(Bootstrap ClassLoader):又名根類載入器或引導類載入器,負責載入%JAVA_HOME%\bin目錄下的所有jar包,或者是-Xbootclasspath引數指定的路徑,例:rt.jar
  • 擴充類載入器(Extension ClassLoader):負責載入%JAVA_HOME%\bin\ext目錄下的所有jar包,或者是java.ext.dirs引數指定的路徑
  • 系統類載入器(Application ClassLoader):又名應用類載入器,負責載入使用者類路徑上所指定的類庫,如果應用程式中沒有自定義載入器,那麼此載入器就為預設載入器

雙親委派機制

類載入器收到載入請求
1.不會自己先去載入,把請求委託給父類載入器,如果父類載入器還存在其父類載入器,則進一步向上委託,最終將到達頂層的啟動類載入器
2.如果父類可以完成載入任務,就成功返回
3.如果完不成,子載入器才會嘗試自己去載入

優點:避免重複載入 + 避免核心類篡改

Native

程式中使用:private native void start0();

1.凡是帶了native關鍵字的,說明java的作用範圍達不到了,回去呼叫底層c語言的庫!

2.會進入本地方法棧,然後去呼叫本地方法介面將native方法引入執行

本地方法棧(Native Method Stack)

記憶體區域中專門開闢了一塊標記區域: Native Method Stack,負責登記native方法,

在執行引擎( Execution Engine )執行的時候通過本地方法介面(JNI)載入本地方法庫中的方法

本地方法介面(JNI)

本地介面的作用是融合不同的程式語言為Java所用,它的初衷是融合C/C++程式,

Java在誕生的時候是C/C++橫行的時候,想要立足,必須有呼叫C、C++的程式,

然後在記憶體區域中專門開闢了一塊標記區域: Native Method Stack,負責登記native方法,

在執行引擎( Execution Engine )執行的時候通過本地方法介面(JNI)載入本地方法庫中的方法

PC程式計數器

程式計數器: Program Counter Register

每個執行緒都有一個程式計數器,是執行緒私有的,就是一個指標,

指向方法區中的方法位元組碼(用來儲存指向像一條指令的地址, 也即將要執行的指令程式碼),

在執行引擎讀取下一條指令, 是一個非常小的記憶體空間,幾乎可以忽略不計

為什麼需要程式計數器?記錄要執行的程式碼位置,防止執行緒切換重新執行

位元組碼執行引擎修改程式計數器的值

方法區(Method Area)

方法區是被所有執行緒共享,所有欄位和方法位元組碼,以及一些特殊方法,如建構函式,介面程式碼也在此定義,

簡單說,所有定義的方法的資訊都儲存在該區域,此區域屬於共享區間。

靜態變數(static)、常量(final)、類資訊(構造方法、介面定義)(Class)、執行時的常量池存在方法區中,但是例項變數存在堆記憶體中,和方法區無關

棧:先出後進,每個執行緒都有自己的棧,棧記憶體主管程式的執行,生命週期和執行緒同步,執行緒結束,棧記憶體也就是釋放。

對於棧來說,不存在垃圾回收問題,一旦執行緒結束,棧就結束。

棧記憶體中執行:8大基本型別 + 物件引用 + 例項的方法。

棧執行原理:棧楨

棧滿了:StackOverflowError

佇列:先進先出(FIFO:First Input First Output)

一個JVM只有一個堆記憶體,堆記憶體的大小是可以調節的,

類載入器讀取類檔案後,一般會把類,方法,常量,變數,我們所有引用型別的真實物件,放入堆中。

堆記憶體細分為三個區域:

  • 新生區(伊甸園區):Young/New
  • 養老區old
  • 永久區Perm

新生區:類的誕生,成長和死亡的地方

分為:

  • 伊甸園區:所有物件都在伊甸園區new出來
  • 倖存0區和倖存1區:輕GC之後存下來的

老年區(養老區):多次輕GC存活下來的物件放在老年區

真理:經過研究,99%的物件都是臨時物件

永久區

這個區域常駐記憶體的,用來存放IDK自身攜帶的Class物件,Interface後設資料,儲存的是Java執行時的一些環境或
類資訊,這個區域不存在垃圾回收。

關閉VM虛擬就會釋放這個區域的記憶體,一個啟動類,載入了大量的第三方jar包。

Tomcat部署了太多的應用,大量動態生成的反射類,不斷的被載入,直到記憶體滿,就會出現0OM;

  • jdk1.6之前:永久代,常量池是在方法區。
  • jdk1.7永久代,但是慢慢的退化了,去永久代, 常量池在堆中
  • jdk1.8之後:無永久代,常量池在元空間

注意:

元空間:邏輯上存在,物理上不存在 ,因為:儲存在本地磁碟內,不佔用虛擬機器記憶體

預設情況下,JVM使用的最大記憶體為電腦總記憶體的四分之一,JVM使用的初始化記憶體為電腦總記憶體的六十四分之一.

總結:

  • 棧:基本型別的變數,物件的引用變數,例項物件的方法
  • 堆:存放由new建立的物件和陣列
  • 方法區:Class物件,static變數,常量池(常量)

調優工具

public class Test2 {
    static String a="111111111111";

    public static void main(String[] args) {
        while (true){
            a=a+ new Random().nextInt(99999999)+new Random().nextInt(99999999);
        }
    }
}

下載地址:https://www.ej-technologies.com/download/jprofiler/version_92

安裝完成後,需要在IDEA中安裝外掛。

新增引數執行程式:

-Xms1m -Xmx1m -XX:+HeapDumpOnOutOfMemoryError:當出現OOM錯誤,會生成一個dump檔案(程式的記憶體映象)

在專案目錄下找到dump檔案,雙擊開啟 , 可以看到什麼佔用了大量的記憶體

常見JVM調優引數

配置引數 功能
-Xms 初始堆大小。如:-Xms256m
-Xmx 最大堆大小。如:-Xmx512m
-Xmn 新生代大小。通常為 Xmx 的 1/3 或 1/4。新生代 = Eden + 2 個 Survivor 空間。實際可用空間為 = Eden + 1 個 Survivor,即 90%
-XX:NewRatio 新生代與老年代的比例,如 –XX:NewRatio=2,則新生代佔整個堆空間的1/3,老年代佔2/3
-XX:SurvivorRatio 新生代中 Eden 與 Survivor 的比值。預設值為 8。即 Eden 佔新生代空間的 8/10,另外兩個 Survivor 各佔 1/10
-XX:+PrintGCDetails 列印 GC 資訊
XX:+HeapDumpOnOutOfMemoryError 讓虛擬機器在發生記憶體溢位時 Dump 出當前的記憶體堆轉儲快照,以便分析用

常見垃圾回收演算法

引用計數演算法

原理是此物件有一個引用,即增加一個計數,刪除一個引用則減少一個計數。

垃圾回收時,只用收集計數為 0 的物件。此演算法最致命的是無法處理迴圈引用的問題。

複製演算法

此演算法把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域。

垃圾回收時,遍歷當前使用區域,把正在使用中的物件複製到另外一個區域中。

此演算法每次只處理正在使用中的物件,因此複製成本比較小,同時複製過去以後還能進行相應的記憶體整理。

優點:不會出現碎片化問題

缺點:需要兩倍記憶體空間,浪費

標記-清除演算法

此演算法執行分兩階段。

第一階段從引用根節點開始標記可回收物件

第二階段遍歷整個堆,把未標記的物件清除。

優點:不會浪費記憶體空間

缺點:此演算法需要暫停整個應用,同時,會產生記憶體碎片

標記-壓縮演算法

此演算法結合了 " 標記-清除 ” 和 “ 複製 ” 兩個演算法的優點。

也是分兩階段,

第一階段從根節點開始標記所有可回收物件

第二階段遍歷整個堆,清除未標記物件並且把存活物件“壓縮”到堆的其中一塊,按順序排放。

此演算法避免了“標記-清除”的碎片問題,同時也避免了“複製”演算法的空間問題。

分代回收策略

1.絕大多數剛剛被建立的物件會存放在Eden區

2.當Eden區第一次滿的時候,會觸發MinorGC(輕GC)。首先將Eden區的垃圾物件回收清除,並將存活的物件複製到S0,此時S1是空的。

3.下一次Eden區滿時,再執行一次垃圾回收,此次會將Eden和S0區中所有垃圾物件清除,並將存活物件複製到S1,此時S0變為空。

4.如此反覆在S0和S1之間切換幾次(預設15次)之後,還存活的物件將他們轉移到老年代中。

5.當老年代滿了時會觸發FullGC(全GC)

MinorGC

  • 使用的演算法是複製演算法
  • 年輕代堆空間緊張時會被觸發
  • 相對於全收集而言,收集間隔較短

FullGC

  • 使用的演算法一般是標記壓縮演算法
  • 當老年代堆空間滿了,會觸發全收集操作
  • 可以使用 System.gc()方法來顯式的啟動全收集
  • 全收集非常耗時

垃圾收集器

垃圾回收器的常規匹配:

序列收集器(Serial)

Serial 收集器是 Hotspot 執行在 Client 模式下的預設新生代收集器, 它的特點是:單執行緒收集,但它卻簡單而高效

並行收集器(ParNew)

ParNew 收集器其實是前面 Serial 的多執行緒版本

Parallel Scavenge 收集器

與 ParNew 類似,Parallel Scavenge 也是使用複製演算法,也是並行多執行緒收集器。

但與其他收集器關注儘可能縮短垃圾收集時間不同,Parallel Scavenge 更關注系統吞吐量

系統吞吐量=執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間)

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本, 同樣是單執行緒收集器,使用 “ 標記-整理 ” 演算法

Parallel Old 收集器

Parallel Old 是 Parallel Scavenge 收集器的老年代版本, 使用多執行緒和 “ 標記-整理 ” 算
法,吞吐量優先

CMS 收集器(Concurrent Mark Sweep)

CMS是一種以獲取最短回收停頓時間為目標的收集器(CMS又稱多併發低暫停的收集器),

基於 ” 標記-清除 ” 演算法實現, 整個 GC 過程分為以下 4 個步驟:

初始標記(CMS initial mark)

併發標記(CMS concurrent mark: GC Roots Tracing 過程)

重新標記(CMS remark)

併發清除(CMS concurrent sweep: 已死物件將會就地釋放, 注意:此處沒有壓縮)

G1 收集器

G1將堆記憶體 “ 化整為零 ” ,將堆記憶體劃分成多個大小相等獨立區域(Region),

每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。

收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新建立的物件還是已經存活了一段時間、熬過多次收集的舊物件都能獲取很好的收集效果。

為什麼要垃圾回收時要設計STW(stop the world)?

如果不設計STW,可能在垃圾回收時使用者執行緒就執行完了,堆中的物件都失去了引用,全部變成了垃圾,索性就

設計了STW,快速做完垃圾回收,再恢復使用者執行緒執行。

相關文章