俗話說,自己寫的代碼,6個月后也是別人的代碼……復習!復習!復習!涉及到的知識點總結如下:
- 一些JVM的跟蹤參數的設置
- Java堆的分配參數
-
-Xmx 和 –Xms 應該保持一個什么關系,可以讓系統的性能盡可能的好呢?是不是虛擬機內存越大越好?
- Java 7之前和Java 8的堆內存結構
- Java棧的分配參數
- GC算法思想介紹
–GC ROOT可達性算法–標記清除–標記壓縮–復制算法
- 可觸及性含義和在Java中的體現
- finalize方法理解
- Java的強引用,軟引用,弱引用,虛引用
- GC引起的Stop-The-World現象
- 串行收集器
- 並行收集器
- CMS
記得JVM學習1里總結了一個例子,就是使用 -XX:+printGC參數來使能JVM的GC日志打印,讓程序員可以追蹤GC的蹤跡。如例子:

1 public class OnStackTest { 2 /** 3 * alloc方法內分配了兩個字節的內存空間 4 */ 5 public static void alloc(){ 6 byte[] b = new byte[2]; 7 b[0] = 1; 8 } 9 10 public static void main(String[] args) { 11 long b = System.currentTimeMillis(); 12 13 // 分配 100000000 個 alloc 分配的內存空間 14 for(int i = 0; i < 100000000; i++){ 15 alloc(); 16 } 17 18 long e = System.currentTimeMillis(); 19 System.out.println(e - b); 20 } 21 }
配置參數-XX:+printGC,再次運行會打印GC日志,截取一句:
[GC (Allocation Failure) 4416K->716K(15872K), 0.0018384 secs]
代表發生了GC,花費了多長時間,效果是GC之前為4M多,GC之后為716K,回收了將近4M內存空間,而堆的大小大約是16M(默認的)。
如果還嫌這些信息不夠,JVM還提供了打印詳細GC日志的參數:-XX:+PrintGCDetails
[GC (Allocation Failure) [DefNew: 4480K->0K(4992K), 0.0001689 secs] 5209K->729K(15936K), 0.0001916 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
會詳細顯示堆的各個代的GC信息,還詳細的給出了耗時信息:user代表用戶態cpu耗時,sys代表系統的cpu耗時,real代表實際經歷時間。除此之外,-XX:+PrintGCDetails,還會在JVM退出前打印堆的詳細信息:

Heap def new generation total 4992K, used 4301K [0x03800000, 0x03d60000, 0x08d50000) eden space 4480K, 96% used [0x03800000, 0x03c33568, 0x03c60000) from space 512K, 0% used [0x03ce0000, 0x03ce0000, 0x03d60000) to space 512K, 0% used [0x03c60000, 0x03c60000, 0x03ce0000) tenured generation total 10944K, used 729K [0x08d50000, 0x09800000, 0x13800000) the space 10944K, 6% used [0x08d50000, 0x08e06700, 0x08e06800, 0x09800000) Metaspace used 103K, capacity 2248K, committed 2368K, reserved 4480K
def new generation total 4992K, used 3226K [0x03800000, 0x03d60000, 0x08d50000)
在對象出生的地方,也就是伊甸園,有4M空間,使用了72%
eden space 4480K, 72% used [0x03800000, 0x03b26830, 0x03c60000)
還有幸存代,from和to,他倆一定是相等的。
from space 512K, 0% used [0x03ce0000, 0x03ce0000, 0x03d60000)
to space 512K, 0% used [0x03c60000, 0x03c60000, 0x03ce0000)
最后還有一個老年代空間,總共有10M,使用了729K
tenured generation total 10944K, used 729K [0x08d50000, 0x09800000, 0x13800000)
最后是Java 8改進之后的元數據空間,其中還有些16進制數字,比如[0x08d50000, 0x09800000, 0x13800000),意思依次是低邊界,當前邊界,最高邊界,代表內存分配的初始位置,當前分配到的位置,和最終能分配到的位置。
重定向GC日志的方法

{Heap before GC invocations=0 (full 0): def new generation total 4928K, used 4416K [0x03c00000, 0x04150000, 0x09150000) eden space 4416K, 100% used [0x03c00000, 0x04050000, 0x04050000) from space 512K, 0% used [0x04050000, 0x04050000, 0x040d0000) to space 512K, 0% used [0x040d0000, 0x040d0000, 0x04150000) tenured generation total 10944K, used 0K [0x09150000, 0x09c00000, 0x13c00000) the space 10944K, 0% used [0x09150000, 0x09150000, 0x09150200, 0x09c00000) Metaspace used 1915K, capacity 2248K, committed 2368K, reserved 4480K Heap after GC invocations=1 (full 0): def new generation total 4928K, used 512K [0x03c00000, 0x04150000, 0x09150000) eden space 4416K, 0% used [0x03c00000, 0x03c00000, 0x04050000) from space 512K, 100% used [0x040d0000, 0x04150000, 0x04150000) to space 512K, 0% used [0x04050000, 0x04050000, 0x040d0000) tenured generation total 10944K, used 202K [0x09150000, 0x09c00000, 0x13c00000) the space 10944K, 1% used [0x09150000, 0x09182950, 0x09182a00, 0x09c00000) Metaspace used 1915K, capacity 2248K, committed 2368K, reserved 4480K }
監控Java類的加載情況: -XX:+TraceClassLoading
監控系統中每一個類的加載,每一行代表一個類,主要用於跟蹤調試程序。
監控類的使用情況:-XX:+PrintClassHistogram
在程序運行中,按下Ctrl+Break后,打印類的信息:截取發現程序使用了大量的hashmap:

num #instances #bytes class name ---------------------------------------------- 1: 2919 400528 [C 2: 173 77072 [B 3: 593 58016 java.lang.Class 4: 2552 40832 java.lang.String 5: 638 36280 [Ljava.lang.Object; 6: 827 26464 java.util.TreeMap$Entry
分別顯示序號(按照空間占用大小排序)、實例數量、總大小、類型
下面看看Java堆的分配參數,指定最大堆和最小堆 -Xmx –Xms
-Xms 10m,表示JVM Heap(堆內存)最小尺寸10MB,最開始只有 -Xms 的參數,表示 `初始` memory size(m表示memory,s表示size),屬於初始分配10m,-Xms表示的 `初始` 內存也有一個 `最小` 內存的概念(其實常用的做法中初始內存采用的也就是最小內存)。
-Xmx 10m,表示JVM Heap(堆內存)最大允許的尺寸10MB,按需分配。如果 -Xmx 不指定或者指定偏小,也許出現java.lang.OutOfMemory錯誤,此錯誤來自JVM不是Throwable的,無法用try...catch捕捉。
看下對JVM設置:-Xmx20m -Xms5m

public class OnStackTest { /** * alloc方法內分配了兩個字節的內存空間 */ public static void alloc(){ byte[] b = new byte[10]; b[0] = 1; } public static void main(String[] args) { long b = System.currentTimeMillis(); // 分配 100000000 個 alloc 分配的內存空間 for(int i = 0; i < 100000000; i++){ alloc(); } System.out.print("Xmx ="); System.out.println(Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); System.out.print("free mem ="); System.out.println(Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); System.out.print("total mem ="); System.out.println(Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); long e = System.currentTimeMillis(); System.out.println(e - b); } }
Xmx =19.375M
free mem =4.21685791015625M
total mem =5.875M
1032
記住:Java會盡量的維持在最小堆運行,即使設置的最大值很大,只有當GC之后也無法滿足最小堆,才會去擴容。
-Xmx 和 –Xms 應該保持一個什么關系,可以讓系統的性能盡可能的好呢?是不是虛擬機內存越大越好?
占坑,后續的GC機制來補充回答這個問題。首先並不是虛擬機內存越大就越好,大概原因是因為:內存越大,JVM 進行 Full GC 所需的時間越久,由於 Full GC 時 stop whole world 特性,如果是用於響應HTTP 請求的服務器,這個時候就表現為停止響應,對於需要低延遲的應用來說,這是不可接受的。對於需要高吞吐量的應用來說,可以不在乎這種停頓,比如一些后台的應用之類的,那么內存可以適當調大一些。需要根據具體情況權衡。
設置新生代大小,-Xmn參數,設置的是絕對值,30m就是30m,10m就是10m。還有一個參數 -XX:NewRatio,看名字就知道是按照比例來設置,意思是設置新生代(eden+2*s)和老年代(不包含永久區)的比值,比如-XX:NewRatio4 表示 新生代:老年代=1:4。

- 根據實際事情調整新生代和幸存代的大小
- 官方推薦新生代占堆的3/8
- 幸存代占新生代的1/10
- 在OOM時,記得Dump出堆,確保可以排查現場問題

- 性能,每次引用和去引用都要加減
- 循環引用問題

- 靜態變量引用的對象
- 常量引用的對象
- 本地方法棧(JNI)引用的對象
- Java棧中引用的對象


- 可觸及的–從GC ROOT這個根節點對象,沿着引用的鏈條,可以觸及到這個對象,該對象就叫可觸及的,也就是之前說的可達性算法的思想。
- 可復活的–一旦所有引用被釋放,就是可復活狀態,因為在finalize()中可能復活該對象(finalize方法只會調用一次)。
- 不可觸及的–在finalize()后,可能會進入不可觸及狀態,不可觸及的對象不可能復活,就可以回收了。
GC准備釋放內存的時候,會先調用finalize()。而調用了這個方法不代表對象一定會被回收。因為GC和finalize() 都是靠不住的,只要JVM還沒有快到耗盡內存的地步,它是不會浪費時間進行垃圾回收的。
finalize()在什么時候被調用?
有三種情況
- 所有對象被Garbage Collection時自動調用,比如運行System.gc()的時候。
- 程序退出時為每個對象調用一次finalize方法。
- 顯式的調用finalize方法。
finalize 是Object的 protected 方法,子類可以覆蓋該方法以實現資源清理工作,GC在回收對象之前調用該方法。finalize與C++中的析構函數不是對應的。C++中的析構函數調用的時機是確定的(對象離開作用域或delete掉),但Java中的finalize的調用具有不確定性。
不建議用finalize方法完成“非內存資源”的清理工作,因為Java語言規范並不保證finalize方法會被及時地執行、而且根本不會保證它們會被執行,而且 finalize 方法可能會帶來性能問題。因為JVM通常在單獨的低優先級線程中完成finalize的執行,finalize方法中,可將待回收對象賦值給GC Roots可達的對象引用,從而達到對象再生的目的。finalize方法至多由GC執行一次(用戶當然可以手動調用對象的finalize方法,但並不影響GC對finalize的行為)
但建議用於:
- 清理本地對象(通過JNI創建的對象);
- 作為確保某些非內存資源(如Socket、文件等)釋放的一個補充:在finalize方法中顯式調用其他資源釋放方法。
- 強引用:類似我們常見的,比如 A a = new A();a就叫強引用。任何被強引用指向的對象都不能GC,這些對象都是在程序中需要的。
- 軟引用:使用java.lang.ref.SoftReference類來表示,軟引用可以很好的用來實現緩存,當JVM需要內存時,垃圾回收器就會回收這些只有被軟引用指向的對象。如下:

Counter prime = new Counter(); SoftReference soft = new SoftReference(prime) ; //soft reference prime = null;
強引用置空之后,代碼的第二行為對象Counter創建了一個軟引用,該引用同樣不能阻止垃圾回收器回收對象,但是可以延遲回收,軟引用更適用於緩存機制,而弱引用更適用於存貯元數據。
- 弱引用:使用java.lang.ref.WeakReference 類來表示,弱引用非常適合存儲元數據,例如:存儲ClassLoader引用。如果沒有類被加載,那么也沒有指向ClassLoader的引用。一旦上一次的強引用被去除,只有弱引用的ClassLoader就會被回收。也就是說如果一個對象只有弱引用指向它,GC會立即回收該對象,這是一種急切回收方式。如:

Counter counter = new Counter(); // strong reference WeakReference<Counter> weakCounter = newWeakReference<Counter>(counter); //weak reference counter = null;
只要給強引用對象counter賦null,該對象就可以被垃圾回收器回收。因為該對象不再含有其他強引用,即使指向該對象的弱引用weakCounter也無法阻止垃圾回收器對該對象的回收。相反的,如果該對象含有軟引用,Counter對象不會立即被回收,除非JVM需要內存。
另一個使用弱引用的例子是WeakHashMap,它是除HashMap和TreeMap之外,Map接口的另一種實現。WeakHashMap有一個特點:map中的鍵值(keys)都被封裝成弱引用,也就是說一旦強引用被刪除,WeakHashMap內部的弱引用就無法阻止該對象被垃圾回收器回收。
- 虛引用:沒什么實際用處,就是一個標志,當GC的時候好知道。擁有虛引用的對象可以在任何時候GC。

ReferenceQueue refQueue = new ReferenceQueue(); DigitalCounter digit = new DigitalCounter(); PhantomReference<DigitalCounter> phantom = new PhantomReference<DigitalCounter>(digit, refQueue);
引用實例被添加在引用隊列中,可以在任何時候通過查詢引用隊列回收對象。
現在我對一個對象的生命周期進行描述:

package wys.demo1; public class Demo1 { public static Demo1 obj; @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("CanReliveObj finalize called"); obj = this;// 把obj復活了!!! } @Override public String toString(){ return "I am CanReliveObj"; } public static void main(String[] args) throws InterruptedException{ obj = new Demo1();// 強引用 obj = null; //不會被立即回收,是可復活的對象 System.gc();// 主動建議JVM做一次GC,GC之前會調用finalize方法,而我在里面把obj復活了!!! Thread.sleep(1000); if(obj == null){ System.out.println("obj 是 null"); }else{ System.out.println("obj 可用"); } System.out.println("第二次gc"); obj = null; //不可復活 System.gc(); Thread.sleep(1000); if(obj == null){ System.out.println("obj 是 null"); }else{ System.out.println("obj 可用"); } } }
結果:
CanReliveObj finalize called
obj 可用
第二次gc
obj 是 null
說明JVM不管程序員手動調用finalize,JVM它就是執行一次finalize方法。執行finalize方法完畢后,GC會再次進行二輪回收,去判斷該對象是否可達,若不可達,才進行回收。
建議:避免使用finalize方法!
太復雜了,還是讓系統照管比較好。可以定義其它的方法來釋放非內存資源。建議使用try-catch-finally來替代它執行清理操作。
如果手動調用了finalize,很容易出錯。且它執行的優先級低,何時被調用,不確定——也就是何時發生GC不確定,因為只有當內存告急時,GC才工作,即使GC工作,finalize方法也不一定得到執行,這是由於程序中的其他線程的優先級遠遠高於執行finalize()的線程優先級。 因此當finalize還沒有被執行時,系統的其他資源,比如文件句柄、數據庫連接池等已經消耗殆盡,造成系統崩潰。且垃圾回收和finalize方法的執行本身就是對系統資源的消耗,有可能造成程序的暫時停止,因此在程序中盡量避免使用finalize方法。
- Dump線程
- JVM的死鎖檢查
- 堆的Dump。


先不說了,先看看JVM的垃圾回收器吧,先看一種最古老的收集器——串行收集器
最古老,最穩定,效率高,但是串行的最大問題就是停頓時間很長!因為串行收集器只使用一個線程去回收,可能會產生較長的停頓現象。我們可以使用參數-XX:+UseSerialGC,設置新生代、老年代使用串行回收,此時新生代使用復制算法,老年代使用標記-壓縮算法(標記-壓縮算法首先需要從根節點開始,對所有可達對象做一次標記。但之后,它並不簡單的清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。之后,清理邊界外所有的空間。有效解決內存碎片問題)。
因為串行收集器只使用一個線程去回收,可能會產生較長的停頓現象。
還有一種收集器叫並行收集器(兩種並行收集器)
- 一種是ParNew並行收集器。使用JVM參數設置XX:+UseParNewGC,設置之后,那么新生代就是並行回收,而老年代依然是串行回收,也就是並行回收器不會影響老年代,它是Serial收集器在新生代的並行版本,新生代並行依然使用復制算法,但是是多線程,需要多核支持,我們可以使用JVM參數: XX:ParallelGCThreads 去限制線程的數量。如圖:

注意:新生代的多線程回收不一定快!看在多核還是單核,和具體環境。、
- 還有一種是Parallel收集器,它類似ParNew,但是更加關注JVM的吞吐量!同樣是在新生代復制算法,老年代使用標記壓縮算法,可以使用JVM參數XX:+UseParallelGC設置使用Parallel並行收集器+ 老年代串行,或者使用XX:+UseParallelOldGC,使用Parallel並行收集器+ 並行老年代。也就是說,Parallel收集器可以同時讓新生代和老年代都並行收集。如圖:
最后看一個很重要的收集器-CMS(並發標記清除收集器Concurrent Mark Sweep)收集器
顧名思義,它在老年代使用的是標記清除算法,而不是標記壓縮算法,也就是說CMS是老年代收集器(新生代使用ParNew),所謂並發標記清除就是CMS與用戶線程一起執行。標記-清除算法與標記-壓縮相比,並發階段會降低吞吐量,使用參數-XX:+UseConcMarkSweepGC打開。
CMS運行過程比較復雜,着重實現了標記的過程,可分為:
- 初始標記,標記GC ROOT 根可以直接關聯到的對象(會產生全局停頓),但是初始標記速度快。
- 並發標記(和用戶線程一起),主要的標記過程,標記了系統的全部的對象(不論垃圾不垃圾)。
- 重新標記,由於並發標記時,用戶線程依然運行(可能產生新的對象),因此在正式清理前,再做一次修正,會產生全局停頓。
- 並發清除(和用戶線程一起),基於標記結果,直接清理對象。這也是為什么使用標記清除算法的原因,因為清理對象的時候用戶線程還能執行!標記壓縮算法的壓縮過程涉及到內存塊移動,這樣會有沖突。
- 並發重置,為下一次GC做准備工作。
- 在用戶線程運行過程中,分一半CPU去做GC,系統性能在GC階段,反應速度就下降一半。
- 清理不徹底。因為在清理階段,用戶線程還在運行,會產生新的垃圾,無法清理。
- 因為和用戶線程基本上是一起運行的,故不能在空間快滿時再清理。
可以使用-XX:CMSInitiatingOccupancyFraction設置觸發CMS GC的閾值,設置空間內存占用到多少時,去觸發GC,如果不幸內存預留空間不夠,就會引起concurrent mode failure。
可以使用-XX:+ UseCMSCompactAtFullCollection, Full GC后,進行一次整理,而整理過程是獨占的,會引起停頓時間變長。
從三個方面考慮:
- 軟件如何設計架構
- 代碼如何寫
- 堆空間如何分配
歡迎關注
dashuai的博客是終身學習踐行者,大廠程序員,且專注於工作經驗、學習筆記的分享和日常吐槽,包括但不限於互聯網行業,附帶分享一些PDF電子書,資料,幫忙內推,歡迎拍磚!