【JVM系列8】JVM經典面試問題(內存溢出和內存泄露)解答及調優實戰分析 前言 常見問題及調優實戰

前言

JVM系列介紹到這裏,其實理論知識和基本工具的使用基本上都介紹過了,當然,JVM的理論知識也不僅僅只是這些,如果想要更深入的裏面還是會有很多細節值得深入瞭解,但是就目前來說,掌握了前面幾篇文章介紹的內容,我們已經可以對JVM進行基本的調優工作了,所以本篇文章會以一些常見問題並結合實際例子來進行分析。

常見問題及調優實戰

1、內存泄漏與內存溢出的區別

內存泄漏(Memory Leak):指的是對象無法得到及時的回收,導致其持續佔用內存空間,造成了內存空間的浪費。 內存泄露一般是強引用纔會出現問題,其他像軟引用,弱引用和虛引用影響不大
內存溢出(Out Of Memory):內存泄漏到一定的程度就會導致內存溢出,但是內存溢出也有可能是大對象導致的。

這兩個區別結合下面的問題2可以更好的理解。

2、如何防止內存泄露

我們先來看下面一個簡單的例子:

package com.zwx.jvm;

public class JVMTuningDemo {
    public static void main(String[] args) {
        {
            byte[] bytes = new byte[1024 * 1024 * 64];
        }
        System.gc();
    }
}

調用之後打開gc日誌,如果不知道怎麼獲取gc日誌的,可以點擊這裏

可以看到GC之後,對象並沒有回收掉,從代碼上來說,因爲有{},所以理論上已經離開作用域了,bytes會被回收(如果不加{}是肯定不會被回收的,因爲沒有離開作用域),但是這裏爲什麼還是沒有被回收?
回答這個問題之前我們先對上面的代碼改進一下

package com.zwx.jvm;

public class JVMTuningDemo {
    public static void main(String[] args) {
        {
            byte[] bytes = new byte[1024 * 1024 * 64];
            bytes = null;
        }
        System.gc();
    }
}

這時候再來看,會發現已經被回收了

這是因爲之前雖然已經離開作用域了,但是卻並沒有收回引用,也就是說棧幀中的局部變量表數組中所對應的slot(局部變量表中數組的每一個位置都被稱之爲slot)還是有值的,並沒有被切斷引用,而將其置爲Null就等於切斷了引用,所以可以被回收。

如果看過我的併發編程系列文章中對AQS同步隊列以及阻塞隊列的源碼分析,那麼也應該可以看到,這些源碼中也是大量使用了這種方式來幫助虛擬機進行gc:

在有些場景這種設置爲null的方式確實是一種解決方式,但是其實最優雅的方式還是以恰當的變量作用域來控制回收變量。
我們再對上面的例子進行改寫:

package com.zwx.jvm;

public class JVMTuningDemo {
    public static void main(String[] args) {
        {
            byte[] bytes = new byte[1024 * 1024 * 64];
        }
        int i = 0;
        System.gc();
    }
}

運行之後打開gc日誌:

我們會發現,bytes對象確實也被回收了,這又是爲什麼呢?

這是因爲棧幀中的局部變量表內的每一個slot都是可以複用的,當bytes變量離開了其作用域之後,Java虛擬機知道這個slot已經無效了,但是雖然無效,引用卻還在,所以如果沒有新的變量過來佔用bytes變量所在的slot,是無法將bytes回收的,而一旦有新的變量過來佔用slot,自然而然bytes對象的引用就被切斷了,從而被gc掉。

3、GCRoot不可達的對象一定會被回收嗎

答案是不一定的。

即使在可達性分析法中被判定不可達的對象,也並非是“非死不可”的,這時候它們暫時處於“緩刑階段”,對象依然有“逃生”的機會。
一個對象在第一次被標記爲不可達對象時,並不會立刻被回收,而是會進行判斷是否有必要執行finalize()方法,那麼什麼時候會執行finalize()方法呢?有兩種情況:

  • 1、Java虛擬機已經調用過當前對象的finalize()方法

  • 2、finalize()方法被我們重寫了
    如果不滿足這兩種情況,那麼對象就相當於是“死刑立即執行”,沒有機會逃生,但是一旦滿足執行finalize()方法的條件,而我們又在finalize()方法中將對象重新和引用鏈中的對象進行了關聯,這時候對象就可以順利“逃生”。
    我們來看下面一個例子:

    package com.zwx.jvm;

    import java.util.ArrayList; import java.util.List;

    public class ObjEscapeByFinalize { public static ObjEscapeByFinalize objEscapeByFinalize = null;

    public static void main(String[] args) throws InterruptedException {
        objEscapeByFinalize = new ObjEscapeByFinalize();
        //首次自救
        objEscapeByFinalize = null;
        System.gc();
        Thread.sleep(1000);//finalize()方法優先級比較低,稍微停頓一會等一等
        print();
    
        //再次自救
        objEscapeByFinalize = null;
        System.gc();
        Thread.sleep(1000);
        print();
    }
    
    static void print(){
        if (null == objEscapeByFinalize){
            System.out.println("obj has been gc");
        }else{
            System.out.println("obj escape success");
        }
    }
    
    @Override
    protected void finalize() throws Throwable {
        System.out.println("come in method:finalize");
        super.finalize();
        objEscapeByFinalize = this;
    }
    

    }

運行結果爲:

come in method:finalize
obj escape success
obj has been gc

從結果可以看到,第一次自救成功,而第二次已經沒有了自救機會,因爲當前對象已經執行過一次finalize()方法了,而如果我們把finalize()方法中的:

objEscapeByFinalize = this;

替換爲:

objEscapeByFinalize = new ObjEscapeByFinalize();

這時候就可以一直自救成功,因爲每次自救之後就產生了一個新的對象,新的對象並沒有執行過finalize()方法。

上面的demo還有一點需要注意的是,finalize()方法針對的是對象,假如上面的靜態對象換成一個其他對象,而finalize()方法又寫在當前對象,那麼是無效的,例如如下例子:

package com.zwx.jvm;

import java.util.ArrayList;
import java.util.List;

public class ObjEscapeByFinalize1 {
    public static List<Object> list = null;

    public static void main(String[] args) throws InterruptedException {
        list = new ArrayList<>();
        //首次自救
        list = null;
        System.gc();
        Thread.sleep(1000);
        print();

    }

    static void print(){
        if (null == list){
            System.out.println("obj has been gc");
        }else{
            System.out.println("obj escape success");
        }
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println("come in method:finalize");
        super.finalize();
        list = new ArrayList<>();
    }
}

這裏是無法實現自救的,因爲這裏要救的對象是List,而finalize()並不屬於List,是屬於ObjEscapeByFinalize1對象,所以這一點也是需要明確地。

不過雖然finalize()可以完成對象自救,但是由於這個方法的代價比較大而且運行時有不確定性,一般情況下還是不建議使用

4、Young GC會有STW嗎

不管是什麼類型的GC,都會有 stop-the-world,只是發生時間的長短,目前Java中所有的垃圾回收器均需要STW,唯一的區別只是時間的長短問題。

5、Major GC和Full GC的區別

之前我們提到了,Major GC通常會伴隨着Minor GC,也就等於觸發了Full GC,但是雖然如此,Major GC和Full GC並不是完全等價的,因爲Full GC 的同時會對方法區(jdk1.8的metaspace,jdk1.7的永久代)進行GC,所以嚴格來說:Full GC=Major GC+Minor GC+方法區GC

6、方法區會發生GC嗎

答案是肯定的。雖然方法區中的回收收益一般都不高,但是也是會被GC的,而方法區中被回收的最主要的就是對廢棄常量無用類的回收,判定一個廢棄常量比較簡單,但是判定一個類是無用類是比較困難的,那麼方法區中的怎麼判斷一個類是無用類呢?
判斷一個類是否無用,需要達到以下三個條件:

  • 1、該類所有的實例都已經被回收,也就是 Java 堆中不存在該類的任何實例。
  • 2、加載該類的類加載器ClassLoader已經被回收(從這個條件可以看出,一般只有大量使用了反射,動態代理或者字節碼框架等場景條件下才會滿足這個條件)。
  • 3、該類對應的 java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

這三個條件實際上是非常苛刻的,而即使達到以上三個條件,無用類也僅僅是可以被回收,但是是不是一定會被回收,還是取決於Java虛擬機。HotSpot虛擬機中提供了參數-Xnoclassgc來控制。

7、什麼是直接內存

直接內存(Direct Memory)不屬於運行時數據區,也被稱之爲堆外內存,通常訪問直接內存的速度會優於Java堆。直接沒存也有可能會發生OutOfMemoryError異常,Java 1.4中新加入的nio包下的ByteBuffer就操作了直接內存,直接內存可以通過參數-XX:MaxDirectMemorySize控制大小。

8、CMS收集器和G1收集器的區別

作爲同樣是並行的2款垃圾收集器,G1的目前是用來取代CMS收集器的,其主要有如下區別:

  • 1、CMS收集器是老年代的收集器,需要和其他新生代收集器配合使用,而G1同時適用於新生代和老年代,不需要和其他收集器配合使用
  • 2、CMS收集器以最小的停頓時間爲目標的併發收集器,G1收集器是一種可預測垃圾回收的停頓時間
  • 3、CMS收集器是使用“標記-清除”算法進行的垃圾回收,容易產生內存碎片,G1使用了 Region方式對堆內存進行了劃分,且基於標記整理算法實現,整體減少了垃圾碎片的產生

9、類加載機制經過哪些步驟

類加載機制主要經過了:加載(Loading)連接(Linking)初始化(Initialization)使用(Using)卸載(Unloading) 五個大階段,而其中連接(Linking)又分爲:驗證(Verification),準備(Preparation)解析(Resolution)三個階段。

10、系統CPU經常100%,如何定位

  • 1、首先要確認哪個進程佔用CPU高,可以使用top命令

2、找到之後可以繼續執行top -Hp PID命令查詢出佔用最大的線程

3、執行jstack命令生成線程快照信息:
jstack -l 進程PID >jstack.log
1

輸出之後,我們找到上面佔用CPU最高的一個線程pid=11566,將其轉換爲16進制,得到的結果是2d2e,然後進入生成的jstack.log文件找到這個線程可以查看線程信息。

4、上面就可以定位到了線程調用的方法了,接下來就可以去分析對應的代碼尋找問題了

總結

本文主要列舉了一些其他比較經典的,而前面在JVM系列其他文章中又沒有過多進行說明的問題,JVM學習之後需要不斷實戰積累調優經驗,雖然還有一些理論在JVM系列中沒有提及,但是我想如果可以認真把我 JVM系列至本篇爲止的8篇文章相關知識和理論都掌握的話,那至少可以說已經具備了調優的理論基礎了,剩下的就是不斷積累經驗,當然,推薦大家可以去通讀一下JVM規範,畢竟所有的Java虛擬機都是按照JVM規範來實現的,或者有必要的可以自己去編譯JDK來進行更深一步的研究。
請關注我,一起學習進步

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章