GC詳解
GC的作用域
GC的作用域如下圖所示。

關于垃圾回收,隻需要記住
分代回收算法
,即不同的區域使用不同的算法。
不同區域的GC頻率也不一樣:
- 年輕代:GC頻繁區域。
- 老年代:GC次數較少。
- 永久代:不會産生GC。
一個對象的曆程
一個對象的曆程的如下圖所示。
JVM在進行GC時,并非每次都是對三個區域進行掃描的,大部分的時候都是對
新生代
進行GC。
GC有兩種類型:
- 普通GC(GC):隻針對新生代 。
- 全局GC(Full GC):主要是針對老年代,偶爾伴随新生代。
GC的四大算法
引用計數法
引用計數法隻需要了解即可,JVM 一般不采用這種方式進行GC。它的原理如下圖所示。
原理:每個對象都有一個引用計數器,每當對象被引用一次,計數器就+1,如果引用失效,計數器就-1,當計數器為0,則GC可以清理該對象。
缺點:
- 計數器維護比較麻煩。
- 循環引用無法處理。
複制算法
年輕代中GC使用的就是複制算法。
原理:
- 一般普通GC之後,Eden區幾乎都是空的了。
- 每次存活的對象,都會被從from區和Eden區等複制到to區,from區和to區會發生一次交換,每當GC後幸存一次,就會導緻這個對象的年齡+1,如果這個年齡值大于15(預設GC次數,可以修改),就會進入養老區。記住一個點就好,誰空誰是to。複制算法的原理如下圖所示。
深入了解JVM(二)
優點:
- 沒有标記和清除的過程,效率高。
- 不會産生記憶體碎片。
由于Eden區對象存活率極低!,據統計99% 對象都會在使用一次之後引用失效,是以在該區中推薦使用複制算法。
标記清除算法
老年代一般使用這個GC算法,但是會和後面的标記整理壓縮算法一起使用。其原理如下圖所示。
原理:
- 先掃描一次,對存活的對象進行标記。
- 再次掃描,回收沒有被标記的對象。
優點:不需要額外的空間。
缺點:
- 需要兩次掃描,耗時嚴重。
- 會産生記憶體碎片,導緻記憶體空間不連續。
标記清除壓縮算法
标記清除壓縮算法,也叫标記整理算法,該算法是在标記清除算法的基礎上進行改進的算法,解決了标記清除算法會産生記憶體碎片的問題,但是相應的耗時可能也較為嚴重。其原理如下圖所示。
原理:
- 先掃描一次,對存活的對象進行标記。
- 第二次掃描,回收沒有被标記的對象。
- 壓縮,再次掃描,将活着的對象滑動到一側,這樣就能讓空出的記憶體空間是連續的。
當一個空間很少發生GC,可以考慮使用此算法。
GC算法小結
記憶體效率:複制算法>标記清除算法>标記整理算法
記憶體整齊度:複制算法=标記整理算法>标記清除算法
記憶體使用率:标記整理算法=标記清除算法>複制算法
從效率上來說,複制算法最好,但是空間浪費較多。為了兼顧所有的名額,标記整理算法會平滑一些,但是效率不盡如意。
實際上,所有的算法,無非就是以空間換時間或者以時間換空間。沒有最好的算法,隻有最合适的算法。是以上面說的分代收集算法,并不是指一種算法,而是在不同的區域使用不同的算法。
綜上所述:
- 年輕代,相對于老年代,對象存活率較低,特别是在Eden區,對象存活率極低,99% 對象都會在使用一次之後引用失效,是以推薦使用複制算法。
- 老年代,區域比較大,對象存活率較高,推薦使用标記清除壓縮算法。
JVM 垃圾回收的時候如何确定垃圾?GC Roots又是什麼?
什麼是垃圾?簡單的說,就是不再被引用的對象。,如:
Object object=null;
如果我們要進行垃圾回收,首先必須判斷這個對象是否可以回收。
在Java中,引用和對象都是有關聯的,如果要操作對象,就要通過引用來進行。
可達性分析算法
可達性分析算法,簡單來說就是通過從GC Root這個對象開始一層層往下周遊,能夠周遊到的對象就是可達的,不能被周遊到的對象就是不可達的,不可達對象就是要被回收的垃圾。其原理如下圖所示。
一切都是從 GC Root 這個對象開始周遊的,隻要在這裡面的就不是垃圾,反之就是垃圾。
什麼是GC Root?
- 虛拟機棧中引用的對象。
- 類中靜态屬性引用的對象。
- 方法區中的常量。
- 本地方法棧中Native方法引用的對象。
如下代碼所示:
public class GCRoots{
private byte[] array = new byte[100*1024*1024]; // GC root,開辟内空間!
private static GCRoots2 t2; // GC root;
private static final GCRoots3 t3 = new GCRoots3(); // GC root;
public static void m1(){
GCRoots g1 = new GCRoots(); //GCroot
System.gc();
}
public static void main(String[] args){
m1();
}
}
總結:
- 對于數組,如果隻是在類成員中進行定義而沒有聲明數組大小,不是GC Root;如果已經聲明了數組大小,則是GC Root,因為此時它已經開辟了記憶體空間。
- 對于靜态成員對象屬性,隻要定義了,不管初始化值是null還是new出了對象,都是GC Root。
JVM常用參數
JVM隻有三種參數類型:
标配參數
、
X參數
,
XX參數
。
标配參數
标配參數是指在JVM各個版本之間都非常穩定,很少有變化的參數。如:
java -version
java -help
java -showversion
X參數
X參數隻要了解即可,如下X參數用于修改JVM的運作模式。
-Xint # 解釋執行
-Xcomp # 第一次使用就編譯成本地的代碼
-Xmixed # 混合模式(Java預設)
XX參數之布爾型(重點)
-XX: +或者-某個屬性值
, + 代表開啟某個功能,- 表示關閉了某個功能。
如以下代碼讓程式睡眠21億秒:
package com.wunian.gc;
//jps -l 檢視堆棧資訊,獲得目前java程式端口号
//jinfo -flag PrintGCDetails 5360 檢視運作中的java程式,某項虛拟機參數是否開啟(輸出+号表示開啟,-表示關閉)
//jinfo -flag MetaspaceSize 6312 檢視元空間大小
//jinfo -flag MaxTenuringThreshold 6312 檢視控制新生代中對象需要經曆多少次GC晉升到老年代,預設為15
//jinfo -flags 6312 檢視指定端口的所有資訊
//java -XX:+PrintFlagsInitial 檢視java環境初始預設值
public class GCDemo {
public static void main(String[] args) throws InterruptedException {
System.out.println("Hello World");
Thread.sleep(Integer.MAX_VALUE);
}
}
程式運作後,打開DOS視窗,執行
jps -l
指令檢視堆棧資訊。得到目前程式運作的端口号,再執行
jinfo -flag PrintGCDetails 端口号
指令來檢視剛剛運作的Java程式的PrintGCDetails參數是否開啟,如果輸出參數-XX:後面是-開頭,表示沒有開啟,+開頭表示已經開啟了。
關閉程式,在IDEA配置中添加JVM參數
-XX:+PrintGCDetails
,再次啟動程式,使用剛才的指令再次檢視一下PrintGCDetails參數是否開啟,輸出參數-XX:後面是+開頭,說明已經開啟了該參數。
XX參數之key=value型
設定元空間大小為128M:
-XX:MetaspaceSize=128m
執行
jinfo -flag MetaspaceSize 端口号
可以檢視指定程式的元空間大小。
設定進入老年區的存活年限(預設是15年):
-XX:MaxTenuringThreshold=15
該參數主要是控制新生代需要經曆多少次GC晉升到老年代中的最大門檻值。在JVM中用4個bit存儲(放在對象頭中),是以其最大值是15。
執行
jinfo -flag MaxTenuringThreshold
可以檢視進入老年區的存活年限。
檢視某個端口的所有資訊的預設值:
jinfo -flags 端口号
-XX:+UseParallelGC
表示預設使用的是并行GC回收器。
經典面試題:
-Xms
,
-Xmx
,是XX參數還是X參數?
1.
-Xms
表示設定初始堆的大小,等價于:
-XX:InitialHeapSize
。
2.
-Xmx
表示設定最大堆的大小,等價于:
-XX:MaxHeapSize
。
是以,
-Xms
,
-Xmx
是XX參數,這種寫法隻不過是文法糖,友善書寫。一般最常用的東西都是有文法糖的。
初始的預設值
檢視Java 環境初始預設值:
-XX:+PrintFlagsInitial
,隻要在這裡面顯示的值,都可以手動指派,但是不建議修改,了解即可。
=
表示是預設值。
:=
表示值被修改過。
檢視被修改過的值:
java -XX:+PrintFlagsFinal -Xss128k GCDemo # 檢視被修改過的值!啟動的時候判斷
檢視使用者修改過的配置的XX選項:
java -XX:+PrintCommandLineFlags -version
常用的JVM調優參數
-
:設定初始堆的大小。-Xms
-
:設定最大堆的大小。-Xmx
-
:線程棧大小設定,預設為512k~1024k。-Xss
-
: 設定年輕代的大小,一般不用改動。-Xmn
-
:設定元空間的大小,這個在本地記憶體中。-XX:MetaspsaceSize
-
:輸出詳細的垃圾回收資訊。-XX:+PrintGCDetails
-
:設定新生代中的 Eden/s0/s1空間的比例。例如:-XX:SurvivorRatio
表示Eden:s0:s1 = 8:1:1uintx SurvivorRatio = 8
表示Eden:s0:s1 = 4:1:1uintx SurvivorRatio = 4
-
:設定年輕代與老年代的占比。例如:-XX:NewRatio
表示新生代:老年代=1:2,預設新生代整個堆的1/3。NewRatio = 2
表示新生代:老年代=1:4,預設新生代整個堆的1/5。NewRatio = 4
-
:進入老年區的存活門檻值。例如:-XX:MaxTenuringThreshold
表示GC15次後存活的對象進入老年區。MaxTenuringThreshold = 15
常見的幾種OOM
java.lang.StackOverflowError
棧溢出,最常見的OOM之一,方法調用自身,示例代碼如下:
package com.wunian.gc;
/**
* 棧溢出 java.lang.StackOverflowError
* 方法調用自身
*/
public class OOMDemo {
public static void main(String[] args) {
a();
}
public static void a(){
a();
}
}
java.lang.OutOfMemoryError: Java heap space
堆溢出,最常見的OOM之一,字元串無限拼接,示例代碼如下:
package com.wunian.gc;
import java.util.Random;
/**
* 堆溢出 java.lang.OutOfMemoryError: Java heap space
* -Xms10m -Xmx10m
*/
public class OOMDemo2 {
public static void main(String[] args) {
String str="coding";
while(true){
str+=str+new Random(1111111111)+new Random(1111111111);
}
}
}
java.lang.OutOfMemoryError: GC overhead limit exceeded
GC回收時間過長(次數過多)也會導緻 OOM,可能CPU占用率一直是100%,頻繁GC但是沒有什麼效果。示例代碼如下:
package com.wunian.gc;
import java.util.ArrayList;
import java.util.List;
/**
* GC回收時間(次數)過長也會導緻 OOM; java.lang.OutOfMemoryError: GC overhead limit exceeded
* -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
*/
public class OOMDemo3 {
public static void main(String[] args) {
int i=0;
List<String> list =new ArrayList<>();
try {
while(true){
list.add(String.valueOf(++i).intern());
/**
* String.intern()是一個Native方法,底層調用C++的 StringTable::intern方法實作。
* 當通過語句str.intern()調用intern()方法後,JVM 就會在目前類的常量池中查找是否存在與str等值的String,
* 若存在則直接傳回常量池中相應Strnig的引用;若不存在,則會在常量池中建立一個等值的String,
* 然後傳回這個String在常量池中的引用。
*/
}
} catch (Exception e) {
System.out.println("i=>"+i);
e.printStackTrace();
throw e;
}
}
}
java.lang.OutOfMemoryError: Direct buffer memory
基礎緩沖區錯誤,使用NIO方法配置設定的本地記憶體超出了JVM參數設定的最大堆外記憶體。設定最大Java堆外記憶體大小:
-XX:MaxDirectMemorySize=5m
,示例代碼如下:
import sun.misc.VM;
import java.nio.ByteBuffer;
import java.util.concurrent.TimeUnit;
/**
* 基礎緩沖區的錯誤! java.lang.OutOfMemoryError: Direct buffer memory
* -XX:MaxDirectMemorySize可以設定java堆外記憶體的峰值
* -Xms10m -Xmx10m -XX:MaxDirectMemorySize=5m -XX:+PrintGCDetails
*/
public class OOMDemo4 {
public static void main(String[] args) throws InterruptedException {
System.out.println("配置的MaxDirectMemorySize"+ VM.maxDirectMemory()/(double)1024/1024+"MB");
TimeUnit.SECONDS.sleep(2);
//故意破壞
//ByteBuffer.allocate();配置設定 JVM的堆記憶體,屬于GC管轄
//ByteBuffer.allocateDirect();//配置設定本地OS記憶體,不屬于GC管轄
配置設定了6M記憶體,但是jvm參數設定了最大堆外記憶體是5M
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(6 * 1024 * 1024);
}
}
java.lang.OutOfMemoryError: unable to create new native thread
高并發環境下,此錯誤更多的時候和平台有關,出現此錯誤的可能原因有:
- 應用建立的線程太多。
- 伺服器不允許你建立這麼多線程。
示例代碼如下:
package com.wunian.gc;
/**
* 伺服器線程不夠了,超過了限制,也會爆出OOM異常
* java.lang.OutOfMemoryError: unable to create new native thread
*/
public class OOMDemo5 {
public static void main(String[] args) {
for (int i = 1; ; i++) {
System.out.println("i=>"+i);
new Thread(()->{
try {
Thread.sleep(Integer.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
},""+i).start();
}
}
}
java.lang.OutOfMemoryError: Metaspace
Java8之後使用元空間代替永久代,使用的是本地記憶體。元空間主要用于存儲:
- 虛拟機加載類資訊
- 常量池
- 靜态變量
- 編譯後的代碼
要模拟元空間溢出,隻需要不斷的生成類即可,這裡需要用到Spring中的Enhancer類,示例代碼如下:
package com.wunian.gc;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
/**
* 元空間溢出 java.lang.OutOfMemoryError: Metaspace
* -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
*/
public class OOMDemo6 {
static class OOMTest{}
public static void main(String[] args) {
int i=0;//模拟計數器
try {
//不斷的加載對象!底層使用Spring的cglib動态代理
while (true) {
i++;
Enhancer enhancer=new Enhancer();
enhancer.setSuperclass(OOMTest.class);
enhancer.setUseCache(false);//不使用緩存
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return method.invoke(o,args);
}
});
enhancer.create();
}
} catch (Exception e) {
System.out.println("i=>"+i);
e.printStackTrace();
}
}
}
深入了解垃圾回收器
GC算法如引用計數算法、複制算法、标記清除算法、标記整理算法都是方法論,垃圾回收器就是這些算法對應的落地的實作。
四種垃圾回收器
1、串行垃圾回收器,單線程工作,執行GC時會停止所有的線程直到GC結束(STW:Stop the World)。其原理如下圖所示。
2、并行垃圾回收器,多線程工作,也會導緻STW。其原理如下圖所示。
3、并發垃圾回收器,在回收垃圾的同時,可以正常執行線程,并行處理,但是如果是單核CPU,隻能交替執行。其原理如下圖所示。
4、G1垃圾回收器,将堆記憶體分割成不同的區域,然後并發的對其進行垃圾回收。Java9以後為預設的垃圾回收器。其原理如下圖所示。
檢視預設的垃圾回收器:
java -XX:+PrintCommandLineFlags -version
Java的垃圾回收器有哪些?
Java曾經由7種垃圾回收器,現在有6種。主要垃圾回收器的位置分布和關系如下圖所示。
上圖中,紅色箭頭表示新生區中使用了對應的垃圾回收器,在老年區隻能使用對應箭頭指向的垃圾回收器。藍色箭頭表示曾經的垃圾回收器有過的對應關系。
6種垃圾回收器名稱分别是:
- DefNew : 預設的新一代 【Serial 串行】
- Tenured : 老年代 【Serial Old】
- ParNew : 并行新一代 【并行ParNew】
- PSYoungGen : 并行清除年輕代 【Parallel Scavcegn】
- ParOldGen: 并行老年區
深入了解JVM(二)
JVM的Server/Client模式
現在的JVM預設都是Server模式,Client幾乎不會使用。以前32位的Windows作業系統,預設都是Client的 JVM 模式,64位的預設都是 Server模式。
垃圾回收器之間的組合關系
上述6種垃圾回收器都是組合使用的,新生區使用了某種垃圾回收器,養老區會使用與之對應的垃圾回收器,并不是自由搭配的。如下圖所示。
如何選擇垃圾回收器
1、單核CPU,單機程式,記憶體小。選擇
-XX:UseSerialGC
。
2、多核CPU,吞吐量大,背景計算。選擇
XX:+UseParallelGC
。
3、多核CPU,不希望有時間停頓,能夠快速響應。選擇
-XX:+UseParNewGC
或者
XX:+UseParallelGC
。
##G1垃圾回收器
以往垃圾回收器的特點
1、年輕代和老年代是各自獨立的記憶體區域。
2、年輕代使用Eden+s0+s1複制算法。
3、老年代垃圾收集必須掃描整個老年代的區域。
4、垃圾回收器原則:盡可能少而快的執行GC。
G1垃圾回收器的原理
G1(Garbage-First)垃圾回收器 ,是面向伺服器端的應用的回收器。其原理如下圖所示。
原理:将堆中的記憶體區域打散,預設分成2048塊。不同的區間可以并行處理垃圾,在GC過程中,幸存的對象會複制到另一個空閑分區中,由于都是以相等大小的分區為機關進行操作,是以G1天然就是一種壓縮方案(局部壓縮)。
使用G1垃圾回收器:
-XX:+UseG1GC
G1垃圾回收器最大的亮點是可以自定義垃圾回收的時間。設定最大的GC停頓時間(機關:毫秒):
XX:MaxGCPauseMillis=100
,JVM會盡可能的保證停頓小于這個時間。
G1垃圾回收器的優點
- 沒有記憶體碎片。
- 可以精準的控制垃圾回收時間。
強引用、軟引用,弱引用和虛引用
主要學習三個引用類:
SoftReference
、
WeakReference
和
PhantomReference
強引用
假設出現了異常或OOM,隻要是強引用的對象,都不會被回收。強引用就是導緻記憶體洩露的原因之一。
package com.wunian.ref;
/**
* 強引用
* -XX:+PrintGCDetails -Xms5m -Xmx5m
*/
public class StrongRefDemo {
public static void main(String[] args) {
Object o1=new Object();//這樣定義的預設就是強引用
Object o2=o1;
o1=null;
System.gc();
System.out.println(o1);//null
System.out.println(o2);//[email protected]
}
}
軟引用
相對于強引用弱化了。如果系統記憶體充足,GC不會回收該對象,但是記憶體不足的情況下就會回收該對象。
package com.wunian.ref;
import java.lang.ref.SoftReference;
/**
* 軟引用
* -XX:+PrintGCDetails -Xms5m -Xmx5m
*/
public class SoftRefDemo {
public static void main(String[] args) {
Object o1=new Object();//這樣定義的預設就是強引用
//Object o2=o1;
SoftReference<Object> o2=new SoftReference<>(o1);//軟引用
System.out.println(o1);//[email protected]
System.out.println(o2.get());//得到引用的值 [email protected]
o1=null;
try {
byte[] bytes=new byte[10*1024*1024];
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(o1);//null
System.out.println(o2.get());//null //由于堆記憶體不足被回收
}
//System.gc();
}
}
弱引用
不論記憶體是否充足,隻要是GC就會回收該對象。
package com.wunian.ref;
import java.lang.ref.WeakReference;
/**
* 弱引用
* -XX:+PrintGCDetails -Xms5m -Xmx5m
*/
public class WeakRefDemo {
public static void main(String[] args) {
Object o1=new Object();//這樣定義的預設就是強引用
WeakReference<Object> o2 = new WeakReference<>(o1);
System.out.println(o1);//[email protected]
System.out.println(o2.get());//得到引用的值 [email protected]
o1=null;
System.gc();
System.out.println(o1);//null
System.out.println(o2.get());//null
}
}
軟引用、弱引用的使用場景
假設現在有一個應用,需要讀取大量的本地圖檔。
1、如果每次讀取圖檔都要從硬碟中讀取,影響性能。
2、一次加載到記憶體中,可能造成記憶體溢出。
我們的思路:
1、使用一個HashMap儲存圖檔的路徑和内容。
2、記憶體足夠,不清理。
3、記憶體不足,清理加載到記憶體中的資料。
虛引用
虛就是虛無,虛引用就是沒有這個引用。虛引用需要結合隊列使用,其主要作用是跟蹤對象的垃圾回收狀态。
package com.wunian.ref;
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.util.concurrent.TimeUnit;
/**
* 虛引用
*/
public class PhantomRefDemo {
public static void main(String[] args) throws InterruptedException {
Object o1=new Object();
//虛引用需要結合隊列使用
ReferenceQueue<Object> referenceQueue=new ReferenceQueue<>();
PhantomReference<Object> objectPhantomReference=new PhantomReference<>(o1,referenceQueue);
System.out.println(o1);//[email protected]
System.out.println(objectPhantomReference.get());//null
System.out.println(referenceQueue.poll());//null
o1=null;
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println(o1);//null
System.out.println(objectPhantomReference.get());//null
//這好比是一個垃圾桶,通過隊列來檢測哪些對象被清理了,可以處理一些善後工作
System.out.println(referenceQueue.poll());//[email protected]
}
}