java8-線程池問題-jvmgc置空回收機制的排查

先說結論,主觀上,咱們都以爲jvm貌似是在確認對象不達的時候進行回收的java

問題就在這個不可達上面 , 官方的解釋是 可達對象 (reachable object) 是能夠從任何活動線程的任何潛在的持續訪問中的任何對象;java 編譯器或代碼生成器可能會對再也不訪問的對象提早置爲 null,使得對象能夠被提早回收

也就是說對象的回收時機是能夠在函數執行完成以前就觸發的算法

public class JavaGCTestMain {
    @Override
    protected void finalize() {
        System.out.println(this + " was finalized!");
    }

    public static void main(String[] args) {
        JavaGCTestMain a = new JavaGCTestMain();
        System.out.println("Created " + a);
        for (int i = 0; i < 1_000_000_000; i++) {
            //制定必定的時間間隔觸發gc
            if (i % 1_000_00 == 0) {
                System.gc();
            }
        }
        System.out.println("done.");
    }
}

運行以後你會發現輸出的數據是這樣的緩存

Created org.mekweg.TestMain@4554617c
org.mekweg.TestMain@4554617c was finalized!

程序函數在沒有被執行完以前就被gc回收了多線程

這樣引入一個複雜的場景好比多線程環境gc是否仍是有問題的

ps: 這個狀況實際上是最近遇到的一個線程池莫名其妙被kill的問題引伸出來的app

先貼一段代碼jvm

public class ThreadBugTest {
    public static void main(String[] args) {
        ThreadBugTest threadBugTest = new ThreadBugTest();
        for (int i = 0; i < 8; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        threadBugTest.run();
                    }
                }
            }).start();
        }
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    System.gc();
                }
            }
        }).start();
    }

    public void run(){
        Executor future = ExecutorWrapper.create();
        future.execute();
    }
    //實際運行的類
    static class Executor{
        private AtomicBoolean ctl = new AtomicBoolean(false);
        public void execute() {
            // 啓動一個新線程,模擬 ThreadPoolExecutor.execute
            new Thread(new Runnable() {
                @Override
                public void run() {}
            }).start();
            // 模擬 ThreadPoolExecutor,啓動新建線程後,循環檢查線程池狀態,驗證是否會在 finalize 中 shutdown
            // 若是線程池被提早 shutdown,則拋出異常
            for (int i = 0; i < 1_000_000; i++) {
                if(ctl.get()){
                    System.out.println(FinalizedTest.finalizedTest);
                    throw new RuntimeException("reject!!!["+ctl.get()+"]");
                }
            }
        }

        public void shutdown() {
            ctl.compareAndSet(false,true);
        }

        //若是被回收,觸發shutdown
        @Override
        protected void finalize() throws Throwable {
            this.shutdown();
        }
    }

    static class ExecutorWrapper {

        public static Executor create(){
            return new FinalizableDelegatedTExecutorService(new Executor());
        }

        static class FinalizableDelegatedTExecutorService extends Executor {
            private Executor e;
            FinalizableDelegatedTExecutorService(Executor executor) {
                this.e = executor;
            }
            @Override
            public void execute() {
                e.execute();
            }

            @Override
            public void shutdown() {
                e.shutdown();
            }
            //調用引入類
            @Override
            protected void finalize() throws Throwable {
                this.shutdown();
            }
        }
    }
}

執行一下咱們上面的函數你會發現一個問題 Executor 對象這個建立線程的玩意居然被回收了.....ide

org.mekweg.test.FinalizedTest@6b1c8f31
Exception in thread "Thread-0" java.lang.RuntimeException: reject!!![true]
    at org.mekweg.test.TThreadPoolExecutor.execute(TThreadPoolExecutor.java:24)
    at org.mekweg.test.Executors$DelegatedTExecutorService.execute(Executors.java:34)
    at org.mekweg.test.FinalizedTest$1.run(FinalizedTest.java:15)
    at java.lang.Thread.run(Thread.java:748)

這個問題映射到java jdk的問題上的話就是在jdk1.8 使用的時候會有線程池莫名其妙關閉的問題 , 貼一下代碼函數

public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                    0L, TimeUnit.MILLISECONDS,
                    new LinkedBlockingQueue<Runnable>()));
}

static class FinalizableDelegatedExecutorService
    extends DelegatedExecutorService {
    FinalizableDelegatedExecutorService(ExecutorService executor) {
        super(executor);
    }
    protected void finalize() {
        super.shutdown();
    }
}

看上面的代碼有一個 finalize方法,他調用了shutdown(); --- 因此咱們用一開始的代碼來模擬這個jdk的bugthis

這裏對這裏面的對象進行一次可達性分析線程

先說一下java gc 的可達性分析的流程吧... ...

在Java中採起了 可達性分析法。該方法的基本思想是經過一系列的「GC Roots」對象做爲起點進行搜索,若是在「GC Roots」和一個對象之間沒有可達路徑,則稱該對象是不可達的,不過要注意的是被斷定爲不可達的對象不必定就會成爲可回收對象。被斷定爲不可達的對象要成爲可回收對象必須至少經歷兩次標記過程,若是在這兩次標記過程當中仍然沒有逃脫成爲可回收對象的可能性,則基本上就真的成爲可回收對象了。

此算法的核心思想:經過一系列稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索走過的路徑稱爲「引用鏈」,當一個對象到 GC Roots 沒有任何的引用鏈相連時(從 GC Roots 到這個對象不可達)時,證實此對象不可用。

在Java語言中,可做爲GC Roots的對象包含如下幾種:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象。(能夠理解爲:引用棧幀中的本地變量表的全部對象)
  2. 方法區中靜態屬性引用的對象(能夠理解爲:引用方法區該靜態屬性的全部對象)
  3. 方法區中常量引用的對象(能夠理解爲:引用方法區中常量的全部對象)
  4. 本地方法棧中(Native方法)引用的對象(能夠理解爲:引用Native方法的全部對象)

能夠理解爲:

  • (1)首先第一種是虛擬機棧(線程棧)中的引用的對象,咱們在程序中正常建立一個對象,對象會在堆上開闢一塊空間,同時會將這塊空間的地址做爲引用保存到虛擬機棧中,若是對象生命週期結束了,那麼引用就會從虛擬機棧中出棧,所以若是在虛擬機棧中有引用,就說明這個對象仍是有用的,這種狀況是最多見的。
  • (2)第二種是咱們在類中定義了全局的靜態的對象,也就是使用了static關鍵字,因爲虛擬機棧是線程私有的,因此這種對象的引用會保存在共有的方法區中,顯然將方法區中的靜態引用做爲GC Roots是必須的。
  • (3)第三種即是常量引用,就是使用了static final關鍵字,因爲這種引用初始化以後不會修改,因此方法區常量池裏的引用的對象也應該做爲GC Roots。最後一種是在使用JNI技術時,有時候單純的Java代碼並不能知足咱們的需求,咱們可能須要在Java中調用C或C++的代碼,所以會使用native方法,JVM內存中專門有一塊本地方法棧,用來保存這些對象的引用,因此本地方法棧中引用的對象也會被做爲GC Roots。

finalize()方法最終斷定對象是否存活:

即便在可達性分析算法中不可達的對象,也並不是是「非死不可」的,這時候它們暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少要經歷再次標記過程。
標記的前提是對象在進行可達性分析後發現沒有與GC Roots相鏈接的引用鏈。

1).第一次標記並進行一次篩選。
篩選的條件是此對象是否有必要執行finalize()方法。
當對象沒有覆蓋finalize方法,或者finzlize方法已經被虛擬機調用過,虛擬機將這兩種狀況都視爲「沒有必要執行」,對象被回收。

2).第二次標記
若是這個對象被斷定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲:F-Queue的隊列之中,並在稍後由一條虛擬機自動創建的、低優先級的Finalizer線程去執行。這裏所謂的「執行」是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束。這樣作的緣由是,若是一個對象finalize()方法中執行緩慢,或者發生死循環(更極端的狀況),將極可能會致使F-Queue隊列中的其餘對象永久處於等待狀態,甚至致使整個內存回收系統崩潰。
Finalize()方法是對象脫逃死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模標記,若是對象要在finalize()中成功拯救本身----只要從新與引用鏈上的任何的一個對象創建關聯便可,譬如把本身賦值給某個類變量或對象的成員變量,那在第二次標記時它將移除出「即將回收」的集合。若是對象這時候還沒逃脫,那基本上它就真的被回收了。

再來分析上面的問題

匹配gc算法的(1)號狀況 GC routs 將會以虛擬機棧引用的對象爲根... ... 看main函數中的建立方法

threadBugTest.run();

會發現這一行對全部的對象來講都是沒有引用的,也就是說在觸發下一次gc的時候將會被回收,執行其中的finilly函數....

反觀jdk的代碼,和咱們的相似,newSingleThreadExecutor方法直接return一個新的對象,若是這個對象和咱們例子中用法相似將會致使相似的問題

問題來了,其實咱們並不想結束這個Executor如何解決

  1. 直接強制引用就好了

修改代碼

public static void main(String[] args) {
    Executors.newSingleThreadExecutor()
    ThreadBugTest threadBugTest = new ThreadBugTest();
    for (int i = 0; i < 8; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                List<Executor> list = new ArrayList<>();
                while (true) {
                    list.add(threadBugTest.run());
                }
                //list.clean();
            }
        }).start();
    }
    new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                System.gc();
            }
        }
    }).start();
}

public Executor run(){
    Executor future = ExecutorWrapper.create();
    future.execute();
    return future;
}

將每次new出的對象進行緩存,就能保證不會被可達性分析命中,從而保證常駐

  1. java9+使用Reference.reachabilityFence(this);保證try finally中的對象強可達
public void execute(Runnable command) {
    try {
        e.execute(command);
    } finally { 
        reachabilityFence(this); 
    }
}

這個方法保證了只有在調用了reachabilityFence以後,java的gc才能回收這個class -- 估計是javajdk的一些特殊的編譯邏輯保證了這個特性........

好比這個代碼在java11中就不會出問題了

public static void main(String[] args) {
    JavaGCTestMain a = new JavaGCTestMain();
    System.out.println("Created " + a);
    for (int i = 0; i < 1_000_000_000; i++) {
        //制定必定的時間間隔觸發gc
        if (i % 1_000_00 == 0) {
            System.gc();
        }
    }
    Reference.reachabilityFence(a);
    System.out.println("done.");
}

不過這個問題貌似在java11 中解決了,相同的代碼在java11中沒有bug 一臉矇蔽 , 其實本質上仍是jvm的bug jvm沒有考慮到這種應用狀況

其實就是jvm的一個bug , 能夠實用強制引用規避...... 寫代碼的時候不要使用finilly
相關文章
相關標籤/搜索