深入理解JVM(二)

GC詳解

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默認)

修改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:後面是-開頭,表示沒有開啓,+開頭表示已經開啓了。
查看堆棧信息
關閉程序,在IDEA配置中添加JVM參數-XX:+PrintGCDetails,再次啓動程序,使用剛纔的命令再次查看一下PrintGCDetails參數是否開啓,輸出參數-XX:後面是+開頭,說明已經開啓了該參數。
PrintGCDetails參數開啓

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 環境初始默認值
=表示是默認值。
:=表示值被修改過。
查看被修改過的值:

java -XX:+PrintFlagsFinal -Xss128k GCDemo   # 查看被修改過的值!啓動的時候判斷

查看被修改過的值
查看用戶修改過的配置的XX選項:java -XX:+PrintCommandLineFlags -version
查看用戶修改過的配置的XX選項

常用的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以後爲默認的垃圾回收器。其原理如下圖所示。
G1垃圾回收器原理
查看默認的垃圾回收器:java -XX:+PrintCommandLineFlags -version
查看默認垃圾回收器

Java的垃圾回收器有哪些?

Java曾經由7種垃圾回收器,現在有6種。主要垃圾回收器的位置分佈和關係如下圖所示。
主要垃圾回收器的位置分佈和關係
上圖中,紅色箭頭表示新生區中使用了對應的垃圾回收器,在老年區只能使用對應箭頭指向的垃圾回收器。藍色箭頭表示曾經的垃圾回收器有過的對應關係。
6種垃圾回收器名稱分別是:

  • DefNew : 默認的新一代 【Serial 串行】
  • Tenured : 老年代 【Serial Old】
  • ParNew : 並行新一代 【並行ParNew】
  • PSYoungGen : 並行清除年輕代 【Parallel Scavcegn】
  • ParOldGen: 並行老年區
    JDK8默認的垃圾回收器

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)垃圾回收器 ,是面向服務器端的應用的回收器。其原理如下圖所示。
G1垃圾回收器原理
原理:將堆中的內存區域打散,默認分成2048塊。不同的區間可以並行處理垃圾,在GC過程中,倖存的對象會複製到另一個空閒分區中,由於都是以相等大小的分區爲單位進行操作,因此G1天然就是一種壓縮方案(局部壓縮)。
使用G1垃圾回收器:-XX:+UseG1GC
G1垃圾回收器最大的亮點是可以自定義垃圾回收的時間。設置最大的GC停頓時間(單位:毫秒):XX:MaxGCPauseMillis=100 ,JVM會儘可能的保證停頓小於這個時間。

G1垃圾回收器的優點

  • 沒有內存碎片。
  • 可以精準的控制垃圾回收時間。

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

主要學習三個引用類:SoftReferenceWeakReferencePhantomReference
引用類關係圖

強引用

假設出現了異常或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
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章