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。複製算法的原理如下圖所示。
優點:
- 沒有標記和清除的過程,效率高。
- 不會產生內存碎片。
由於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
:設置最大堆的大小。-Xss
:線程棧大小設置,默認爲512k~1024k。-Xmn
: 設置年輕代的大小,一般不用改動。-XX:MetaspsaceSize
:設置元空間的大小,這個在本地內存中。-XX:+PrintGCDetails
:輸出詳細的垃圾回收信息。-XX:SurvivorRatio
:設置新生代中的 Eden/s0/s1空間的比例。例如:
uintx SurvivorRatio = 8
表示Eden:s0:s1 = 8:1:1
uintx SurvivorRatio = 4
表示Eden:s0:s1 = 4:1:1-XX:NewRatio
:設置年輕代與老年代的佔比。例如:
NewRatio = 2
表示新生代:老年代=1:2,默認新生代整個堆的1/3。
NewRatio = 4
表示新生代:老年代=1:4,默認新生代整個堆的1/5。-XX:MaxTenuringThreshold
:進入老年區的存活閾值。例如:
MaxTenuringThreshold = 15
表示GC15次後存活的對象進入老年區。
常見的幾種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的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);//java.lang.Object@6e0be858
}
}
軟引用
相對於強引用弱化了。如果系統內存充足,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);//java.lang.Object@6e0be858
System.out.println(o2.get());//得到引用的值 java.lang.Object@6e0be858
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);//java.lang.Object@6e0be858
System.out.println(o2.get());//得到引用的值 java.lang.Object@6e0be858
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);//java.lang.Object@6e0be858
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());//java.lang.ref.PhantomReference@61bbe9ba
}
}