天天看點

深入了解JVM(二)

GC詳解

GC的作用域

GC的作用域如下圖所示。

深入了解JVM(二)

關于垃圾回收,隻需要記住

分代回收算法

,即不同的區域使用不同的算法。

不同區域的GC頻率也不一樣:

  • 年輕代:GC頻繁區域。
  • 老年代:GC次數較少。
  • 永久代:不會産生GC。
一個對象的曆程

一個對象的曆程的如下圖所示。

深入了解JVM(二)

JVM在進行GC時,并非每次都是對三個區域進行掃描的,大部分的時候都是對

新生代

進行GC。

GC有兩種類型:

  • 普通GC(GC):隻針對新生代 。
  • 全局GC(Full GC):主要是針對老年代,偶爾伴随新生代。

GC的四大算法

引用計數法

引用計數法隻需要了解即可,JVM 一般不采用這種方式進行GC。它的原理如下圖所示。

深入了解JVM(二)

原理:每個對象都有一個引用計數器,每當對象被引用一次,計數器就+1,如果引用失效,計數器就-1,當計數器為0,則GC可以清理該對象。

缺點:

  • 計數器維護比較麻煩。
  • 循環引用無法處理。
複制算法

年輕代中GC使用的就是複制算法。

深入了解JVM(二)

原理:

  • 一般普通GC之後,Eden區幾乎都是空的了。
  • 每次存活的對象,都會被從from區和Eden區等複制到to區,from區和to區會發生一次交換,每當GC後幸存一次,就會導緻這個對象的年齡+1,如果這個年齡值大于15(預設GC次數,可以修改),就會進入養老區。記住一個點就好,誰空誰是to。複制算法的原理如下圖所示。
    深入了解JVM(二)

優點:

  • 沒有标記和清除的過程,效率高。
  • 不會産生記憶體碎片。

由于Eden區對象存活率極低!,據統計99% 對象都會在使用一次之後引用失效,是以在該區中推薦使用複制算法。

标記清除算法

老年代一般使用這個GC算法,但是會和後面的标記整理壓縮算法一起使用。其原理如下圖所示。

深入了解JVM(二)

原理:

  • 先掃描一次,對存活的對象進行标記。
  • 再次掃描,回收沒有被标記的對象。

優點:不需要額外的空間。

缺點:

  • 需要兩次掃描,耗時嚴重。
  • 會産生記憶體碎片,導緻記憶體空間不連續。
标記清除壓縮算法

标記清除壓縮算法,也叫标記整理算法,該算法是在标記清除算法的基礎上進行改進的算法,解決了标記清除算法會産生記憶體碎片的問題,但是相應的耗時可能也較為嚴重。其原理如下圖所示。

深入了解JVM(二)

原理:

  • 先掃描一次,對存活的對象進行标記。
  • 第二次掃描,回收沒有被标記的對象。
  • 壓縮,再次掃描,将活着的對象滑動到一側,這樣就能讓空出的記憶體空間是連續的。

當一個空間很少發生GC,可以考慮使用此算法。

GC算法小結

記憶體效率:複制算法>标記清除算法>标記整理算法

記憶體整齊度:複制算法=标記整理算法>标記清除算法

記憶體使用率:标記整理算法=标記清除算法>複制算法

從效率上來說,複制算法最好,但是空間浪費較多。為了兼顧所有的名額,标記整理算法會平滑一些,但是效率不盡如意。

實際上,所有的算法,無非就是以空間換時間或者以時間換空間。沒有最好的算法,隻有最合适的算法。是以上面說的分代收集算法,并不是指一種算法,而是在不同的區域使用不同的算法。

綜上所述:

  • 年輕代,相對于老年代,對象存活率較低,特别是在Eden區,對象存活率極低,99% 對象都會在使用一次之後引用失效,是以推薦使用複制算法。
  • 老年代,區域比較大,對象存活率較高,推薦使用标記清除壓縮算法。

JVM 垃圾回收的時候如何确定垃圾?GC Roots又是什麼?

什麼是垃圾?簡單的說,就是不再被引用的對象。,如:

Object object=null;
           

如果我們要進行垃圾回收,首先必須判斷這個對象是否可以回收。

在Java中,引用和對象都是有關聯的,如果要操作對象,就要通過引用來進行。

可達性分析算法

可達性分析算法,簡單來說就是通過從GC Root這個對象開始一層層往下周遊,能夠周遊到的對象就是可達的,不能被周遊到的對象就是不可達的,不可達對象就是要被回收的垃圾。其原理如下圖所示。

深入了解JVM(二)

一切都是從 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
           
深入了解JVM(二)
X參數

X參數隻要了解即可,如下X參數用于修改JVM的運作模式。

-Xint          # 解釋執行
-Xcomp         # 第一次使用就編譯成本地的代碼
-Xmixed        # 混合模式(Java預設)
           
深入了解JVM(二)
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:後面是-開頭,表示沒有開啟,+開頭表示已經開啟了。

深入了解JVM(二)

關閉程式,在IDEA配置中添加JVM參數

-XX:+PrintGCDetails

,再次啟動程式,使用剛才的指令再次檢視一下PrintGCDetails參數是否開啟,輸出參數-XX:後面是+開頭,說明已經開啟了該參數。

深入了解JVM(二)
XX參數之key=value型

設定元空間大小為128M:

-XX:MetaspaceSize=128m

執行

jinfo -flag MetaspaceSize 端口号

可以檢視指定程式的元空間大小。

深入了解JVM(二)

設定進入老年區的存活年限(預設是15年):

-XX:MaxTenuringThreshold=15

該參數主要是控制新生代需要經曆多少次GC晉升到老年代中的最大門檻值。在JVM中用4個bit存儲(放在對象頭中),是以其最大值是15。

執行

jinfo -flag MaxTenuringThreshold

可以檢視進入老年區的存活年限。

深入了解JVM(二)

檢視某個端口的所有資訊的預設值:

jinfo -flags 端口号

-XX:+UseParallelGC

表示預設使用的是并行GC回收器。

深入了解JVM(二)

經典面試題:

-Xms

,

-Xmx

,是XX參數還是X參數?

1.

-Xms

表示設定初始堆的大小,等價于:

-XX:InitialHeapSize

2.

-Xmx

表示設定最大堆的大小,等價于:

-XX:MaxHeapSize

是以,

-Xms

,

-Xmx

是XX參數,這種寫法隻不過是文法糖,友善書寫。一般最常用的東西都是有文法糖的。

初始的預設值

檢視Java 環境初始預設值:

-XX:+PrintFlagsInitial

,隻要在這裡面顯示的值,都可以手動指派,但是不建議修改,了解即可。

深入了解JVM(二)

=

表示是預設值。

:=

表示值被修改過。

檢視被修改過的值:

java -XX:+PrintFlagsFinal -Xss128k GCDemo   # 檢視被修改過的值!啟動的時候判斷
           
深入了解JVM(二)

檢視使用者修改過的配置的XX選項:

java -XX:+PrintCommandLineFlags -version

深入了解JVM(二)

常用的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)。其原理如下圖所示。

深入了解JVM(二)

2、并行垃圾回收器,多線程工作,也會導緻STW。其原理如下圖所示。

深入了解JVM(二)

3、并發垃圾回收器,在回收垃圾的同時,可以正常執行線程,并行處理,但是如果是單核CPU,隻能交替執行。其原理如下圖所示。

深入了解JVM(二)

4、G1垃圾回收器,将堆記憶體分割成不同的區域,然後并發的對其進行垃圾回收。Java9以後為預設的垃圾回收器。其原理如下圖所示。

深入了解JVM(二)

檢視預設的垃圾回收器:

java -XX:+PrintCommandLineFlags -version

深入了解JVM(二)
Java的垃圾回收器有哪些?

Java曾經由7種垃圾回收器,現在有6種。主要垃圾回收器的位置分布和關系如下圖所示。

深入了解JVM(二)

上圖中,紅色箭頭表示新生區中使用了對應的垃圾回收器,在老年區隻能使用對應箭頭指向的垃圾回收器。藍色箭頭表示曾經的垃圾回收器有過的對應關系。

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種垃圾回收器都是組合使用的,新生區使用了某種垃圾回收器,養老區會使用與之對應的垃圾回收器,并不是自由搭配的。如下圖所示。

深入了解JVM(二)
如何選擇垃圾回收器

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)垃圾回收器 ,是面向伺服器端的應用的回收器。其原理如下圖所示。

深入了解JVM(二)

原理:将堆中的記憶體區域打散,預設分成2048塊。不同的區間可以并行處理垃圾,在GC過程中,幸存的對象會複制到另一個空閑分區中,由于都是以相等大小的分區為機關進行操作,是以G1天然就是一種壓縮方案(局部壓縮)。

使用G1垃圾回收器:

-XX:+UseG1GC

G1垃圾回收器最大的亮點是可以自定義垃圾回收的時間。設定最大的GC停頓時間(機關:毫秒):

XX:MaxGCPauseMillis=100

,JVM會盡可能的保證停頓小于這個時間。

G1垃圾回收器的優點
  • 沒有記憶體碎片。
  • 可以精準的控制垃圾回收時間。

強引用、軟引用,弱引用和虛引用

主要學習三個引用類:

SoftReference

WeakReference

PhantomReference

深入了解JVM(二)
強引用

假設出現了異常或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]
    }
}