先說結論,主觀上,咱們都以爲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回收了多線程
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的對象包含如下幾種:
能夠理解爲:
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一個新的對象,若是這個對象和咱們例子中用法相似將會致使相似的問題
修改代碼
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出的對象進行緩存,就能保證不會被可達性分析命中,從而保證常駐
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