具體學習 JVM 可以參考我以前寫的這篇博客 對Java虛擬機的學習總結,本篇博客在該博客的基礎上加以修改
1 Java 內存區域
Java 虛擬機在執行 Java 程序的過程中會把它所管理的內存劃分爲以下6個運行時數據區域。
- 程序計數器:一塊較小的內存空間,可以看作當前線程所執行的字節碼的行號指示器。如果線程正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是 Native 方法,這個計數器值則爲空。
- Java 虛擬機棧:與程序計數器一樣,Java 虛擬機棧也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是 Java 方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
- 本地方法棧:本地方法棧與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧爲虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。
- Java 堆:對大多數應用來說,Java 堆是 Java 虛擬機所管理的內存中最大的一塊。Java 堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。
- 方法區:與 Java 堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。方法區是 JVM 規範中定義的一個概念,具體放在哪裏,不同的實現可以放在不同的地方。
- 運行時常量池:運行時常量池是方法區的一部分。Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。
2 finalize() 方法工作原理
一旦垃圾回收器準備好釋放對象佔用的存儲空間,將首先調用其 finalize() 方法,並且在下一次垃圾回收動作發生時,纔會真正回收對象佔用的內存。
至於爲什麼在下一次垃圾回收動作發生時纔會回收內存,原因是如果一個對象覆蓋了 finalize() 方法,那麼在真正被宣告死亡的時候,至少需要經過兩次標記。第一次被標記的時候會被放在 一個 F-Queue 隊列中。finalize() 方法是對象逃脫死亡命運的最後一次機會,在第二次標記的時候,如果該對象成功與引用鏈(GC-Roots)上的任何一個對象關聯,那麼它仍然可以存活下來,否則將會被垃圾收集器回收。
3 Java 8 的內存分代改進
在 jdk1.8 中對內存模型中方法區的實現永久代進行了移除,取而代之的是元空間。原因是在方法區中實現垃圾回收的條件比較苛刻,因此存在着內存溢出的風險。在 jdk1.8 之後,當方法區內存使用較多時,元空間會使用物理內存,減少了風險。
4 OOM
4.1 什麼是 OOM?
OOM,即 Out Of Memory,官方說法是當 JVM 因爲沒有足夠的內存來爲對象分配空間並且垃圾回收器也已經沒有空間可回收時,就會拋出這個 error。總而言之,就是指沒有空閒內存,並且垃圾收集器也無法提供更多內存。
4.2 怎麼排查 OOM?
可以查看服務器運行日誌以及項目記錄的日誌,捕捉到內存溢出異常。
4.3 OOM 出現在什麼時候?哪些會導致 OOM?
- JAVA 堆內存溢出,此種情況最常見,一般由於內存泄露或者堆的大小設置不當引起,可以通過虛擬機參數 -Xms,-Xmx 等修改。
- JAVA 永久代溢出,即方法區溢出了,因爲永久代的大小是有限的,並且 JVM 對永久代垃圾回收(常量池回收、卸載不再需要的類型)非常不積極,所以當我們不斷添加新類型的時候,永久代出現 OutOfMemoryError 也非常多見 ,尤其是在運行時存在大量動態類型生成的場合( jdk8 已經沒有方法區了,改爲元數據區)
- JAVA 虛擬機棧溢出,不會拋 OOM error,一般是由於程序中存在死循環或者深度遞歸調用造成的,棧大小設置太小也會出現此種溢出。可以通過虛擬機參數 -Xss 來設置棧的大小。程序不斷的進行遞歸調用,而且沒有退出條件,就會導致不斷地進行壓棧。類似這種情況,JVM 實際會拋出 StackOverFlowError;當然,如果 JVM 試圖去擴展棧空間的的時候失敗,則會拋出 OutOfMemoryError。
- 直接內存不足,也會導致 OOM。
5 GC
5.1 什麼是 GC?
GC,即就是 Java 垃圾回收機制。GC 觸發的條件有兩種,一是程序調用 System.gc 時可以觸發,一是系統自身來決定 GC 觸發的時機。
5.2 什麼是 Minor GC?
從年輕代空間(包括 Eden 和 Survivor 區域)回收內存被稱爲 Minor GC。當 Eden 區滿時,觸發 Minor GC。
5.3 什麼是 Full GC?
Full GC 是清理整個堆空間,包括年輕代和老年代。
5.4 Full GC 的觸發條件
- 調用 System.gc 時,系統建議執行 Full GC,但是不一定執行
- 老年代空間不足
- 方法區空間不足
- 通過 Minor GC 後進入老年代的平均大小大於老年代的可用內存
- 由 Eden 區、From Space 區向 To Space 區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
5.5 垃圾回收的兩次標記
- 第一次標記:對於一個沒有其他引用的對象,篩選該對象是否有必要執行 finalize() 方法,如果沒有執行必要,則意味可直接回收。(篩選依據:是否覆寫或執行過 finalize 方法;因爲 finalize 方法只能被執行一次)。
- 第二次標記:如果被篩選判定位有必要執行,則會放入 FQueue 隊列,並自動創建一個低優先級的 finalize 線程來執行釋放操作。如果在一個對象釋放前被其他對象引用,則該對象會被移除 FQueue 隊列。