JVM理解2
1、垃圾回收(GC)
GC(Garbage Collection,即垃圾回收)的基本原理:將內存中不再被使用的對象進行回收。顧名思義就是釋放垃圾占用的空間,防止內存泄露。有效的使用可以對使用的內存,對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收。
GC 又分為年輕代 GC 和老年代 GC,年輕代 GC 只針對年輕代內存空間進行垃圾回收,耗時比較短;老年代 GC 針對整個堆與方法區的空間進行垃圾回收,耗時較年輕代 GC 長。
垃圾回收主要是發生在堆內存里面,在1.8以后FULLGC也會發生在meta space中。
1.1、Minor GC、Major GC、Full GC
- Minor GC:對年輕代(包括 Eden 和 Survivor 區域)進行GC被稱為 Minor GC。因為新生代的特點,MinorGC非常頻繁,且回收速度比較快,每次回收的量也很大。
- Major GC:對老年代的對象的收集稱為 Major GC。Major GC的速度一般會比Minor GC慢10倍以上。
- Full GC:Full GC是針對整個新生代、老生代、元空間(metaspace,java8以上版本取代perm gen)的全局范圍的GC,Full GC是對整個堆來說的。Full GC不等于Major GC,也不等于Minor GC+Major GC。出現Full GC的時候經常伴隨至少一次的 Minor GC。
GC 中用于回收的方法稱為收集器,由于GC需要消耗一些資源和時間,Java 在對對象的生命周期特征進行分析后,按照新生代、老年代的方式來對對象進行收集,以盡可能的縮短GC對應用造成的暫停。
1.2、STOP THE WORLD(STW)
Stop一the一World,簡稱STW,指的是GC事件發生過程中,會產生應用程序的停頓。停頓產生時整個應用程序線程都會被暫停,沒有任何響應,有點像卡死的感覺,這個停頓稱為STW。舉例:可達性分析算法中枚舉根節點(GC Roots)會導致所有Java執行線程停頓。
停頓的原因:
-
分析工作必須在一個能確保一致性的快照中進行
-
一致性指整個分析期間整個執行系統看起來像被凍結在某個時間點上
-
如果出現分析過程中對象引用關系還在不斷變化,則分析結果的準確性無法保證
特點描述:
- 被STW中斷的應用程序線程會在完成GC之后恢復,頻繁中斷會讓用戶感覺像是網速不快造成電影卡帶一樣, 所以我們需要減少STW的發生。
- STW事件和采用哪款GC無關,所有的GC都有這個事件。
- 哪怕是G1也不能完全避免Stop一the一world情況發生,只能說垃圾回收器越來越優秀,回收效率越來越高,盡可能地縮短了暫停時間。
- STW是JVM在后臺自動發起和自動完成的。在用戶不可見的情況下,把用戶正常的工作線程全部停掉。
- 開發中不要采用System.gc();會導致Stop一the一world的發生。
1.3、Full GC原因和解決方法
下面介紹幾種可能會導致 JVM 進行 Full GC的情況及解決辦法:
- 顯式調用 System.gc()
此方法的調用是建議JVM進行Full GC,雖然只是建議而非一定,但很多情況下它會觸發 Full GC,從而增加Full GC的頻率,也即增加了間歇性停頓的次數。建議能不使用此方法就別使用,讓虛擬機自己去管理它的內存,可通過通過-XX:+ DisableExplicitGC來禁止RMI調用System.gc()。
- 老年代空間不足
老年代空間只有在新生代對象轉入及創建為大對象、大數組時才會出現不足的現象。在老年代空間不足時,jvm 會發生 fullgc,如果在執行Full GC后空間仍然不足,就會觸發 OOM,報錯如【java.lang.OutOfMemoryError: Java heap space】。
為避免以上兩種狀況引起的Full GC,調優時應盡量做到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要創建過大的對象及數組。
- 方法區空間不足
JVM規范中運行時數據區域中的方法區,又被稱為永生代或者永生區(并不是所有的jvm都有永生代的概念),方法區中存放的為一些class的信息、常量、靜態變量等數據,當系統中要加載的類、反射的類和調用的方法較多時,方法區可能會被占滿,在未配置為采用CMS GC的情況下也會執行Full GC。如果經過Full GC仍然回收不了,那么JVM會拋出如下錯誤信息【java.lang.OutOfMemoryError: PermGen space】
為避免方法區占滿造成Full GC現象,可采用的方法為增大方法區空間或轉為使用CMS GC。
- 堆中分配很大的對象
所謂大對象,是指需要大量連續內存空間的java對象,例如很長的數組,此種對象會直接進入老年代,而老年代雖然有很大的剩余空間,但是無法找到足夠大的連續空間來分配給當前對象,此種情況就會觸發JVM進行Full GC。
為了解決這個問題,CMS垃圾收集器提供了一個可配置的參數,即-XX:+UseCMSCompactAtFullCollection開關參數,用于在“享受”完Full GC服務之后額外免費贈送一個碎片整理的過程,內存整理的過程無法并發的,空間碎片問題沒有了,但停頓時間不得不變長了,JVM設計者們還提供了另外一個參數 -XX:CMSFullGCsBeforeCompaction,這個參數用于設置在執行多少次不壓縮的Full GC后,跟著來一次帶壓縮的。
- 統計得到的Minor GC晉升到舊生代的平均大小大于老年代的剩余空間
這是一個較為復雜的觸發情況,Hotspot為了避免由于新生代對象晉升到舊生代導致舊生代空間不足的現象,在進行Minor GC時,做了一個判斷,如果之前統計所得到的Minor GC晉升到舊生代的平均大小大于舊生代的剩余空間,那么就直接觸發Full GC。
例如程序第一次觸發Minor GC后,有6MB的對象晉升到舊生代,那么當下一次Minor GC發生時,首先檢查舊生代的剩余空間是否大于6MB,如果小于6MB,則執行Full GC。
當新生代采用PS GC時,方式稍有不同,PS GC是在Minor GC后也會檢查,例如上面的例子中第一次Minor GC后,PS GC會檢查此時舊生代的剩余空間是否大于6MB,如小于,則觸發對舊生代的回收。
2、垃圾定位
在進行垃圾回收之前,需要首先進行垃圾定位,即判斷哪些對象可以進行回收。當對象沒有被任何引用指向時就可被垃圾回收。
2.1、引用計數法(目前JVM未使用)
引用計數法也就是記錄當前對象的引用次數,當引用次數為0時則進行回收。給對象添加一個引用計數器,每當有一個地方引用它,計數器值就加一;相反的,當引用失效的時候,計數器值就減一;任何時刻計數器為0的對象就是不可能再被使用的。也就是說,當計時器的數值為0的時候,這個對象就可以被回收了。
引用計數是垃圾收集器中的早期策略,但是引用計數法存在一個巨大的問題,就是循環依賴,例如:

針對上圖這種情況,對象ABC之間相互引用,他們的counter永遠不可能為0,造成他們永遠無法被回收,因此目前主流的 JVM 里都沒有選用引用計數算法來管理內存。
示例:
<- 背景 -> 對象objA 和 objB 都有字段 name,兩個對象相互進行引用 objA.name = objB; objB.name = objA; <- 問題 -> 當這兩個對象objA、objB再也沒有其他任何引用時,實際上他們應該要被垃圾收集器進行回收才對
但因為他們相互引用,所以導致計數器不為0,這導致引用計數算法無法通知垃圾收集器回收該兩個對象
2.2、可達性分析算法(JVM采用的算法)
這個算法的實質在于將一系列GC Roots作為初始的存活對象合集(live set),然后從該合集出發,探索所有能夠被該合集引用到的對象,并將其加入到該和集中,這個過程稱之為標記(mark)。 最終,未被探索到的對象便是死亡的,是可以回收的。
對象可達指的就是:雙方存在直接或間接的引用關系。 根可達或 GC Roots可達就是指:對象到GC Roots存在直接或間接的引用關系。
public class MyObject { private String objectName;//對象名 private MyObject refrence;//依賴對象 public MyObject(String objectName) { this.objectName = objectName; } public MyObject(String objectName, MyObject refrence) { this.objectName = objectName; this.refrence = refrence; } public static void main(String[] args) { MyObject a = new MyObject("a"); MyObject b = new MyObject("b"); MyObject c = new MyObject("c"); a.refrence = b; b.refrence = c; new MyObject("d", new MyObject("e")); } }
創建了5個對象,他們之間的引用關系如下圖,假設a是GC Roots的話,那么b、c就是可達的,d、e是不可達的。

目前主流的商用JVM都是通過可達性分析來判斷對象是否可以被回收的。
2.2.1、GC Roots
垃圾回收時,JVM首先要找到所有的GC Roots,這個過程稱作 「枚舉根節點」 ,這個過程是需要暫停用戶線程的,即觸發STW。然后再從GC Roots這些根節點向下搜尋,可達的對象就保留,不可達的對象就回收。
GC Roots就是對象,而且是JVM確定當前絕對不能被回收的對象(如方法區中類靜態屬性引用的對象)。只有找到這種對象,后面的搜尋過程才有意義,不能被回收的對象所依賴的其他對象肯定也不能回收嘛。當JVM觸發GC時,首先會讓所有的用戶線程到達安全點SafePoint時阻塞,也就是STW,然后枚舉根節點,即找到所有的GC Roots,然后就可以從這些GC Roots向下搜尋,可達的對象就保留,不可達的對象就回收。即使是號稱幾乎不停頓的CMS、G1等收集器,在枚舉根節點時,也是要暫停用戶線程的。GC Roots是一種特殊的對象,是Java程序在運行過程中所必須的對象,而且是根對象。
在 Java 語言里,可作為 GC Roots 對象的包括如下幾種:
- 虛擬機棧(棧楨中的本地變量表)中的引用的對象。
- 方法區中的類靜態屬性引用的對象。
- 方法區中的常量引用的對象。
- 本地方法棧中JNI的引用的對象。
3、JVM的五種引用
在 JDK1.2 版之后,Java 對引用的概念進行了擴充,將引用分為:強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)、終接器引用,這 5 種引用強度依次逐漸減弱。實際上,這些都是引用,即可以理解為變量。
除強引用外,其他 3 種引用均可以在 java.lang.ref 包中找到它們的身影,而終接器引用無需手動編碼,其內部配合引用隊列使用。如下圖,顯示了這 3 種引用類型對應的類,開發人員可以在應用程序中直接使用它們。

Reference 子類中只有終結器引用是包內可見的,其他 3 種引用類型均為 public,可以在應用程序中直接使用
- 強引用(StrongReference):最傳統的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類似“Object obj = new Object()”這種引用關系。無論任何情況下,只要強引用關系還存在,垃圾收集器就永遠不會回收掉被引用的對象。
- 軟引用(SoftReference):在系統將要發生內存溢出之前,將會把這些對象列入回收范圍之中進行第二次回收。如果這次回收后還沒有足夠的內存,才會拋出內存流出異常。
- 弱引用(WeakReference):被弱引用關聯的對象只能生存到下一次垃圾收集之前。當垃圾收集器工作時,無論內存空間是否足夠,都會回收掉被弱引用關聯的對象。
- 虛引用(PhantomReference):一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲得一個對象的實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。
public static void main(String[] args) { //強引用 String zzl="zzz"; int t=1; //軟引用 SoftReference<Object> softReference=new SoftReference<>("aaa"); System.out.println(softReference.get()); //輸出aaa //弱引用 WeakReference<Object> weakReference=new WeakReference<>(new String("zzz")); System.out.println(weakReference.get()); //輸出zzz System.gc(); System.out.println(weakReference.get()); //輸出null //虛引用 ReferenceQueue<Object> queue=new ReferenceQueue<>(); PhantomReference<Object> phantomReference=new PhantomReference<>(new String("zzl"),queue); System.out.println(phantomReference.get()); //輸出null System.gc(); System.out.println(phantomReference.get()); //輸出null }

3.1、強引用(Strong Reference,不回收)
在 Java 程序中,最常見的引用類型是強引用(普通系統 99%以上都是強引用),也就是我們最常見的普通對象引用,也是默認的引用類型。當在 Java 語言中使用 new 操作符創建一個新的對象,并將其賦值給一個變量的時候,這個變量就成為指向該對象的一個強引用。
如果強引用的對象是可觸及的,垃圾收集器就永遠不會回收掉被引用的對象。對于一個普通的對象,如果沒有其他的引用關系,只要超過了引用的作用域或者顯式地將相應(強)引用賦值為 null,就是可以當做垃圾被收集了,當然具體回收時機還是要看垃圾收集策略。相對的,軟引用、弱引用和虛引用的對象是軟可觸及、弱可觸及和虛可觸及的,在一定條件下,都是可以被回收的。所以,強引用是造成 Java 內存泄漏的主要原因之一。
強引用具備以下特點:
- 強引用可以直接訪問目標對象。
- 強引用所指向的對象在任何時候都不會被系統回收,虛擬機寧愿拋出 OOM 異常,也不會回收強引用所指向對象。
- 強引用可能導致內存泄漏。
3.2、軟引用(Soft Reference,內存不足即回收)
軟引用是用來描述一些還有用,但非必需的對象。只被軟引用關聯著的對象,在系統將要發生內存溢出異常前,會把軟引用所指向的這些對象列進回收范圍之中進行第二次回收,如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。即僅有軟引用引用該對象時,在垃圾回收后,如果此時內存仍不足則會再次出發垃圾回收,回收軟引用對象。軟引用通常用來實現內存敏感的緩存。比如:高速緩存就有用到軟引用。如果還有空閑內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。
實例代碼:
package cn.itcast.jvm.t2; import java.io.IOException; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.List; /** * 演示軟引用 * -Xmx20m -XX:+PrintGCDetails -verbose:gc 通過-XX:+PrintGCDetails -verbose:gc參數配置可在控制臺輸出查看垃圾回收的過程 */ public class Demo2_3 { private static final int _4MB = 4 * 1024 * 1024; public static void main(String[] args) throws IOException { //強引用 /*List<byte[]> list = new ArrayList<>(); for (int i = 0; i < 5; i++) { list.add(new byte[_4MB]); } System.in.read();*/ //使用軟引用 soft(); } public static void soft() { // list --> SoftReference --> byte[] List<SoftReference<byte[]>> list = new ArrayList<>(); for (int i = 0; i < 5; i++) { SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]); System.out.println(ref.get()); list.add(ref); System.out.println("list的大小:" + list.size()); } System.out.println("循環結束,list的大小:" + list.size()); for (SoftReference<byte[]> ref : list) { System.out.println(ref.get()); } } }
當使用強引用時,會發生內存溢出:

當使用弱引用時,輸出如下:

3.2.1、軟引用隊列
在軟引用所指向的對象被回收后,默認情況下,該軟引用是仍然存在的,此時我們可以配合使用引用隊列來將軟引用自身釋放掉。
package cn.itcast.jvm.t2; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.List; /** * 演示軟引用, 配合引用隊列 * -Xmx20m -XX:+PrintGCDetails -verbose:gc */ public class Demo2_4 { private static final int _4MB = 4 * 1024 * 1024; public static void main(String[] args) { List<SoftReference<byte[]>> list = new ArrayList<>(); // 引用隊列 ReferenceQueue<byte[]> queue = new ReferenceQueue<>(); for (int i = 0; i < 5; i++) { // 關聯了引用隊列, 當軟引用所關聯的 byte[]被回收時,軟引用自己會加入到 queue 中去 SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB], queue); System.out.println(ref.get()); list.add(ref); System.out.println(list.size()); } // 從隊列中獲取無用的 軟引用對象,并移除 Reference<? extends byte[]> poll = queue.poll(); while( poll != null) { list.remove(poll); poll = queue.poll(); } System.out.println("==========================="); System.out.println("remove結束,list的大小:" + list.size()); for (SoftReference<byte[]> reference : list) { System.out.println(reference.get()); } } }
輸出如下:

可以看到,remove 后 list 集合的大小只有 1,即那些無用的軟引用都被釋放掉了。
3.3、弱引用(Weak Reference,發現即回收)
弱引用也是用來描述那些非必需對象,只被弱引用關聯的對象只能生存到下一次垃圾收集發生為止。在系統 GC 時,只要發現弱引用,不管系統堆空間使用是否充足,都會回收掉只被弱引用關聯的對象。僅有弱引用引用該對象時,在垃圾回收時,無論內存是否充足,都會回收弱引用對象。
但是,由于垃圾回收器的線程通常優先級很低,因此,并不一定能很快地發現持有弱引用的對象。在這種情況下,弱引用對象可以存在較長的時間。
弱引用和軟引用一樣,在構造弱引用時,也可以指定一個引用隊列,當弱引用對象被回收時,就會加入指定的引用隊列,通過這個隊列可以跟蹤對象的回收情況。
軟引用、弱引用都非常適合來保存那些可有可無的緩存數據。如果這么做,當系統內存不足時,這些緩存數據會被回收,不會導致內存溢出。而當內存資源充足時,這些緩存數據又可以存在相當長的時間,從而起到加速系統的作用。
代碼演示:
package cn.itcast.jvm.t2; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; /** * 演示弱引用 * -Xmx20m -XX:+PrintGCDetails -verbose:gc */ public class Demo2_5 { private static final int _4MB = 4 * 1024 * 1024; public static void main(String[] args) { // list --> WeakReference --> byte[] List<WeakReference<byte[]>> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]); list.add(ref); System.out.print("第" + (i+1) + "次循環:"); for (WeakReference<byte[]> w : list) { System.out.print(w.get()+" "); } System.out.println(); } System.out.println("循環結束:" + list.size()); } }
輸出如下:

3.4、虛引用(Phantom Reference,對象回收跟蹤)
也稱為“幽靈引用”或者“幻影引用”,是所有引用類型中最弱的一個。一個對象是否有虛引用的存在,完全不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它和沒有引用幾乎是一樣的,隨時都可能被垃圾回收器回收。
它不能單獨使用,也無法通過虛引用來獲取被引用的對象。當試圖通過虛引用的 get()方法取得對象時,總是 null。
為一個對象設置虛引用關聯的唯一目的在于跟蹤垃圾回收過程。比如:能在這個對象被收集器回收時收到一個系統通知。
虛引用必須和引用隊列一起使用。虛引用在創建時必須提供一個引用隊列作為參數。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象后,將這個虛引用加入引用隊列,以通知應用程序對象的回收情況。
由于虛引用可以跟蹤對象的回收時間,因此,也可以將一些資源釋放操作放置在虛引用中執行和記錄。
3.5、終結器引用
它用于實現對象的 finalize() 方法,也可以稱為終結器引用。無需手動編碼,其內部配合引用隊列使用。
在 GC 時,終結器引用入隊。由 Finalizer 線程通過終結器引用找到被引用對象調用它的 finalize()方法,第二次 GC 時才回收被引用的對象。
4、垃圾回收算法
JVM 垃圾回收算法:
- “標記–清除”算法:首先標記出所有需要被回收的對象,然后在標記完成后統一回收掉所有被標記的對象。
- “標記–整理”算法
- 復制算法:將內存劃分為等大的兩塊,每次只使用其中的一塊。
- 分代收集算法。
4.1、標記 - 清除算法
執行步驟:
- 先標記:遍歷內存區域,對需要回收的對象打上標記。
- 再清除:再次遍歷內存,對已經標記過的內存進行回收。
圖解:

優點:速度較快,只標記然后清除,不做內存整理。
缺點:
- 會造成內容碎片。清除后由于回收的內存空間地址不連續,所以容易產生大量內存碎片,當再需要一塊比較大的內存時,無法找到一塊滿足要求的,因而不得不再次出發GC,可能導致發生內存溢出。
- 掃描了整個空間兩次(第一次:標記存活對象;第二次:清除沒有標記的對象)
適用場景:
- 存活對象較多的情況下比較高效
- 適用于年老代(即舊生代)
4.2、標記 - 整理算法
執行步驟:
- 標記:對需要回收的進行標記
- 整理:讓存活的對象,向內存的一端移動,然后清理掉沒有用的內存

優點:沒有內存碎片。因為經過整理后,內存地址不會不連續。
缺點:速度慢。存活的內存需向同一端移動,內存地址發生改變,對象的指向地址也需要隨之發生改變,工作量比較大。
標記-整理算法是一種老年代的回收算法,它在標記-清除算法的基礎上做了一些優化。首先也需要從根節點開始對所有可達對象做一次標記,但之后,它并不簡單地清理未標記的對象,而是將所有的存活對象壓縮到內存的一端。之后,清理邊界外所有的空間。這種方法既避免了碎片的產生,又不像標記-復制算法一樣需要兩塊相同的內存空間,因此,其性價比比較高。
4.3、標記 - 復制算法
將內存劃分為等大的兩塊,每次只使用其中的一塊。當一塊用完了,觸發GC時,將該塊中存活的對象復制到另一塊區域,然后一次性清理掉這塊沒有用的內存。下次觸發GC時將那塊中存活的的又復制到這塊,然后抹掉那塊,循環往復。
優點:不會有內存碎片
缺點:需要占用雙倍內存空間
4.4、分代收集算法(JVM采用的垃圾回收算法)
當前大多商用虛擬機都采用這種分代收集算法,這個算法并沒有新的內容,只是根據對象的存活的時間的長短,將內存分為了新生代和老年代,這樣就可以針對不同的區域,采取對應的算法。
一般情況下將堆區劃分為新生代(Young Generation)和老年代(Tenured Generation)
- 新生代:存放生命周期較短的對象的區域
- 老年代:存放生命周期較長的對象的區域
在堆區之外還有一個就是永久代(Permanet Generation)。
在不同年代使用不同的算法,從而使用最合適的算法。新生代存活率低,可以使用復制算法。而老年代對象存活率高,沒有額外空間對它進行分配擔保,所以只能使用標記清除或者標記整理算法。
- 新生代復制算法

- 所有新生成的對象首先都是放在年輕代的,對象首先分配在伊甸園(eden)區域。
- 新生代內存按照 8:1:1 的比例分為一個eden(伊甸園)區和兩個 survivor(survivor0,survivor1) 區。一個Eden區,兩個 Survivor 區(一般而言)。
- 新生代伊甸園空間不足時,會觸發 minor gc。
- 在第一次發生 minor gc時,伊甸園區中的對象將會被復制到幸存區 to,然后to區和from區指向交換,即保證to區都是空的。
- 在GC開始的時候,對象只會存在于Eden區和名為“From”的Survivor區,Survivor 區“To”是空的。在進行GC時,Eden區中所有存活的對象都會被復制到“To”,在“From”區中仍存活的對象會根據他們的年齡值來決定去向,年齡達到一定值(年齡閾值可以通過-XX:MaxTenuringThreshold來設置)的對象會被直接移動到年老代中,沒有達到閾值的對象會被復制到“To”區域。
- GC會一直重復這樣的過程,直到“To”區被填滿,“To”區被填滿之后,會將所有對象移動到老年代中。
- 當老年代空間也不足,會先嘗試觸發 minor gc,如果之后空間仍不足,那么觸發 full gc,STW的時間更長。
也就是伊甸園區空間不足時,會發生垃圾回收(minor gc);當老年代空間也不足時,會先觸發 minor gc,如果還不足,則會觸發 full gc。
(當對象達到晉升閾值時,對象會被移動至老年代;或者當新生代的 to 區被填滿時,即新生代內存不足時,未達到晉升閾值的對象也會將被移動至老年代中;)
5、JVM參數
5.1、JVM參數
jvm的參數有很多,根據 jvm 參數開頭可以區分參數類型,共三類:“-”、“-X”、“-XX”,
1)標準參數(-):所有的JVM實現都必須實現這些參數的功能,而且向后兼容;例子:-verbose:class,-verbose:gc,-verbose:jni……
2)非標準參數(-X):默認jvm實現這些參數的功能,但是并不保證所有jvm實現都滿足,且不保證向后兼容;例子:Xms20m,-Xmx20m,-Xmn20m,-Xss128k……
3)非Stable參數(-XX):此類參數各個jvm實現會有所不同,將來可能會隨時取消,需要慎重使用;例子:-XX:+PrintGCDetails,-XX:-UseParallelGC,-XX:+PrintGCTimeStamps……
-XX 參數被稱為不穩定參數,之所以這么叫是因為此類參數的設置很容易引起JVM 性能上的差異,使JVM 存在極大的不穩定性。如果此類參數設置合理將大大提高JVM 的性能及穩定性。
1.布爾類型參數值
-XX:+<option> '+'表示啟用該選項
-XX:-<option> '-'表示關閉該選項
2.數字類型參數值:
-XX:<option>=<number> 給選項設置一個數字類型值,可跟隨單位,例如:'m'或'M'表示兆字節;'k'或'K'千字節;'g'或'G'千兆字節。32K與32768是相同大小的。
3.字符串類型參數值:
-XX:<option>=<string> 給選項設置一個字符串類型值,通常用于指定一個文件、路徑或一系列命令列表。 例如:-XX:HeapDumpPath=./dump.core
5.2、常用參數配置
堆設置:
- -Xms:初始堆空間大小。示例 -Xmx64m
- -Xmx:最大堆空間大小。示例 -Xmx64m
- -Xmn:新生代大小。示例 -Xmx32m
- -XX:NewRatio:設置新生代和老年代的比值。如:為3,表示年輕代與老年代比值為1:3
- -XX:SurvivorRatio:新生代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如值為3,表示Eden : Survivor=3 : 2,一個Survivor區占整個新生代的1/5
- -XX:MaxTenuringThreshold:晉升閾值,設置轉入老年代的存活次數。如果是0,則直接跳過新生代進入老年代
- -XX:PermSize、-XX:MaxPermSize:分別設置永久代最小大小與最大大小(Java8以前)
- -XX:MetaspaceSize、-XX:MaxMetaspaceSize:分別設置元空間最小大小與最大大小(Java8以后)
垃圾回收統計信息:
- -XX:+PrintGC:
- -verbose:gc:輸出虛擬機GC詳情
- -XX:+PrintGCDetails:打印GC詳情
-
-XX:+PrintGCDetails -verbose:gc:GC詳情
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
- -XX:+ScavengeBeforeFullGC:FullGC 前進行 MinorGC
- -Xss:設置每個線程可使用的內存大小,即棧的大小。示例 -Xss512k
- -XX:+HeapDumpOnOutOfMemoryError:當拋出oom時進行heapdump
- -XX:+HeapDumpPath:指定 heapdump 文件的路徑和目錄
最常見的幾個參數如下:
- -Xms20m :設置jvm初始化堆大小為20m,一般與-Xmx相同避免垃圾回收完成后jvm重新分。
- -Xmx20m:設置jvm最大可用內存大小為20m。
- -Xmn10m:設置新生代大小為20m。
- -Xss128k:設置每個線程的棧大小為128k。
根據字母拆分理解意思:

5.3、查看JVM參數
可使用下面命令查看當前所有 Java 進程的 jvm 參數。
jps -lv
- -l :輸出完全的包名,應用主類名,jar的完全路徑名
- -v:輸出jvm參數

6、GC分析
示例:
package cn.itcast.jvm.t2; import java.util.ArrayList; /** * 演示內存的分配策略 */ public class Demo2_1 { private static final int _512KB = 512 * 1024; private static final int _1MB = 1024 * 1024; private static final int _6MB = 6 * 1024 * 1024; private static final int _7MB = 7 * 1024 * 1024; private static final int _8MB = 8 * 1024 * 1024; // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC public static void main(String[] args) throws InterruptedException { } }
以 -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC 配置運行上面程序,打印出 GC和內存等信息,輸出如下:

上圖中可以看到,默認情況下,就算什么代碼都沒有,Java 程序啟動后就會使用一定的內存,此時全部在伊甸園區。可以看到伊甸園區使用了 22%,其他區域暫未被使用到。
演示伊甸園區垃圾回收,修改代碼如下:
package cn.itcast.jvm.t2; import java.util.ArrayList; /** * 演示內存的分配策略 */ public class Demo2_1 { private static final int _512KB = 512 * 1024; private static final int _1MB = 1024 * 1024; private static final int _6MB = 6 * 1024 * 1024; private static final int _7MB = 7 * 1024 * 1024; private static final int _8MB = 8 * 1024 * 1024; // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC public static void main(String[] args) throws InterruptedException { ArrayList<byte[]> list = new ArrayList<>(); list.add(new byte[_7MB]); list.add(new byte[_512KB]); list.add(new byte[_512KB]); } }
輸出如下:

如果想要存放一個大對象,而新生代區放不下,則會直接將該大對象放在老年代。
當主線程發生OOM時,程序會異常終止。但是當主線程內的其他線程OOM時,此時發生OOM的線程會被殺死,內存釋放,但其他線程不受影響,即主線程仍會繼續正常運行。
代碼示例:
package cn.itcast.jvm.t2; import java.util.ArrayList; /** * 演示內存的分配策略 */ public class Demo2_1 { private static final int _512KB = 512 * 1024; private static final int _1MB = 1024 * 1024; private static final int _6MB = 6 * 1024 * 1024; private static final int _7MB = 7 * 1024 * 1024; private static final int _8MB = 8 * 1024 * 1024; // -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc -XX:-ScavengeBeforeFullGC public static void main(String[] args) throws InterruptedException { new Thread(() -> { System.out.println("thread start"); ArrayList<byte[]> list = new ArrayList<>(); list.add(new byte[_8MB]); list.add(new byte[_8MB]); }).start(); System.out.println("sleep...."); Thread.sleep(10000L); System.out.println("sleep end...."); } }
輸出如下:

上面代碼,當線程中發生 OOM 時,主線程并沒有停止。當 sleep 休眠結束后,主線程才會終止。
7、垃圾回收器
如果說垃圾回收算法是內存回收的方法論,那么垃圾回收器就是內存回收的實踐者。
垃圾回收器按線程數分,可以分為串行垃圾回收器和并行垃圾回收器。
- 串行垃圾回收:指的是在同一個時間段內只允許有一個回收器線程用于執行垃圾回收操作。
- 并行垃圾回收:和串行垃圾回收相反,并行運算可以擁有多個垃圾回收線程同時執行垃圾回收。
不管是串行還是并行垃圾回收器,在進行垃圾回收時,用戶線程都是停止的,即使用“Stop The World”的機制,在回收的時候,需要暫停所有的線程。此時只是垃圾回收線程在工作。
7.1、串行垃圾回收器
串行垃圾回收:指的是在同一個時間段內只允許有一個回收器線程用于執行垃圾回收操作。適合堆內存較小,單核CPU的情況。
8、GC調優
JVM調優是一個不斷調整的過程,不能指望著一蹴而就。要不斷調整相關參數,觀察結果進行對比分析。還有就是,不同的垃圾收集器的JVM參數是不一樣的,所以具體的GC調優要根據不同的收集器做調整。
最快的 GC 就是不發生 GC。
當發生 full gc時,查看Full GC前后的內存占用可考慮以下問題:
- 數據量是不是太多?
- 數據表示是否太臃腫?
- 是否存在內存泄漏?
(實際上可能多數的 Java 應用不需要在服務器上進行 GC 優化;多數導致 GC 問題的 Java 應用,都不是因為我們參數設置錯誤,而是代碼問題;在應用上線之前,先考慮將機器的 JVM 參數設置到最優(最適合);減少創建對象的數量;減少使用全局變量和大對象;GC 優化是到最后不得已才采用的手段;在實際使用中,分析 GC 情況優化代碼比優化 GC 參數要多得多。)
8.1、新生代調優
新生代特點:
① new操作內存分配廉價(TLAB thread-local allocation buffer)
② 新生代垃圾回收采用復制算法,回收代價低
③ 大部分對象用過即可回收
④ Minor GC的時間遠低于Full GC
新生代內存并非是越大越好,總內存是一定的,新生代大了,老年代就會小,觸發Full GC 的概率越大,full gc 的速度比 minor gc慢多了。
新生代調優方法
- ① 內存大小調整。命令:
-Xmn size,Oracle建議新生代大小占整個堆空間25%-50%。
- ② 新生代調優參考
- 新生代內存建議能容納【并發量 * (請求 + 響應) 】的數據。
- 幸存區的大小應能保留【當前活躍對象 + 需要晉升對象】的數據。
- 晉升閾值配置得當,讓長時間存貨對象盡快晉升。
-XX:+PrintTenuringDistribution:通過配置該 jvm 參數來打印幸存區年齡空間數據,以此預估一個合理閾值。-XX:MaxTenuringThreshold=threshold:設置晉升閾值
8.2、老年代調優
以 CMS垃圾回收器 為例:
- CMS 的老年代內存越大越好。避免浮動垃圾過多,進而引起并發清除失敗,退化為SerialOld。
- 先嘗試不做調優,如果沒有 Full GC 那么已經內存可以滿足。如果Full GC頻繁,則先嘗試調優新生代。
- 觀察發生 Full GC 時老年代內存占用,將老年代內存預設調大 1/4 ~ 1/3
-XX:CMSInitiatingOccupancyFraction=percent (設置當老年代的空間占用占老年代總內存的多少百分比時,就發生Full GC)

浙公網安備 33010602011771號