Java Reference核心原理分析

{"type":"doc","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"帶着問題,看源碼針對性會更強一點、印象會更深刻、並且效果也會更好。所以我先賣個關子,提兩個問題(沒準下次跳槽時就被問到)。"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以用ByteBuffer的allocateDirect方法,申請一塊堆外內存創建一個DirectByteBuffer對象,然後利用它去操作堆外內存。這些申請完的堆外內存,我們可以回收嗎?可以的話是通過什麼樣的機制回收的?"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"大家應該都知道WeakHashMap可以用來實現內存相對敏感的本地緩存,爲什麼WeakHashMap合適這種業務場景,其內部實現會做什麼特殊處理呢?"}]}]}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"GC可到達性與JDK中Reference類型"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面提到的兩個問題,其答案都在JDK的Reference裏面。JDK早期版本中並沒有Reference相關的類,這導致對象被GC回收後如果想做一些額外的清理工作(比如socket、堆外內存等)是無法實現的,同樣如果想要根據堆內存的實際使用情況決定要不要去清理一些內存敏感的對象也是法實現的。爲此JDK1.2中引入的Reference相關的類,即今天要介紹的Reference、SoftReference、WeakReference、PhantomReference,還有與之相關的Cleaner、ReferenceQueue、ReferenceHandler等。與Reference相關核心類基本都在java.lang.ref包下面。其類關係如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/4a/4ad1ea5f8488ba94c74f0c0deedbfaa4.png","alt":"reference.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"其中,SoftReference代表軟引用對象,垃圾回收器會根據內存需求酌情回收軟引用指向的對象。普通的GC並不會回收軟引用,只有在即將OOM的時候(也就是最後一次Full GC)如果被引用的對象只有SoftReference指向的引用,纔會回收。WeakReference代表弱引用對象,當發生GC時,如果被引用的對象只有WeakReference指向的引用,就會被回收。PhantomReference代表虛引用對象(也有叫幻象引用的,個人認爲還是虛引用更加貼切),其是一種特殊的引用類型,不能通過虛引用獲取到其關聯的對象,但當GC時如果其引用的對象被回收,這個事件程序可以感知,這樣我們可以做相應的處理。最後就是最常見強引用對象,也就是通常我們new出來的對象。在繼續介紹Reference相關類的源碼前,先來簡單的看一下GC如何決定一個對象是否可被回收。其基本思路是從GC Root開始向下搜索,如果對象與GC Root之間存在引用鏈,則對象是可達的,GC會根據是否可到達與可到達性決定對象是否可以被回收。而對象的可達性與引用類型密切相關,對象的可到達性可分爲5種。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"強可到達,如果從GC Root搜索後,發現對象與GC Root之間存在強引用鏈則爲強可到達。強引用鏈即有強引用對象,引用了該對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"軟可到達,如果從GC Root搜索後,發現對象與GC Root之間不存在強引用鏈,但存在軟引用鏈,則爲軟可到達。軟引用鏈即有軟引用對象,引用了該對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"弱可到達,如果從GC Root搜索後,發現對象與GC Root之間不存在強引用鏈與軟引用鏈,但有弱引用鏈,則爲弱可到達。弱引用鏈即有弱引用對象,引用了該對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"虛可到達,如果從GC Root搜索後,發現對象與GC Root之間只存在虛引用鏈則爲虛可到達。虛引用鏈即有虛引用對象,引用了該對象。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不可達,如果從GC Root搜索後,找不到對象與GC Root之間的引用鏈,則爲不可到達。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"看一個簡單的列子:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/8b/8b60c7e32ba011ba09aec69a613b0fe4.png","alt":"objectReach.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ObjectA爲強可到達,ObjectB也爲強可到達,雖然ObjectB對象被SoftReference ObjcetE 引用但由於其還被ObjectA引用所以爲強可到達;而ObjectC和ObjectD爲弱引用達到,雖然ObjectD對象被PhantomReference ObjcetG引用但由於其還被ObjectC引用,而ObjectC又爲弱引用達到,所以ObjectD爲弱引用達到;而ObjectH與ObjectI是不可到達。引用鏈的強弱有關係依次是 強引用 > 軟引用 > 弱引用 > 虛引用,如果有更強的引用關係存在,那麼引用鏈到達性,將由更強的引用有關係決定。"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"Reference核心處理流程"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"JVM在GC時如果當前對象只被Reference對象引用,JVM會根據Reference具體類型與堆內存的使用情況決定是否把對應的Reference對象加入到一個由Reference構成的pending鏈表上,如果能加入pending鏈表JVM同時會通知ReferenceHandler線程進行處理。ReferenceHandler線程是在Reference類被初始化時調用的,其是一個守護進程並且擁有最高的優先級。Reference類靜態初始化塊代碼如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"static {\n //省略部分代碼...\n Thread handler = new ReferenceHandler(tg, \"Reference Handler\");\n handler.setPriority(Thread.MAX_PRIORITY);\n handler.setDaemon(true);\n handler.start();\n //省略部分代碼...\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而ReferenceHandler線程內部的run方法會不斷地從Reference構成的pending鏈表上獲取Reference對象,如果能獲取則根據Reference的具體類型進行不同的處理,不能則調用wait方法等待GC回收對象處理pending鏈表的通知。ReferenceHandler線程run方法源碼:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public void run() {\n //死循環,線程啓動後會一直運行\n while (true) {\n tryHandlePending(true);\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"run內部調用的tryHandlePending源碼:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"static boolean tryHandlePending(boolean waitForNotify) {\n Reference r;\n Cleaner c;\n try {\n synchronized (lock) {\n if (pending != null) {\n r = pending;\n //instanceof 可能會拋出OOME,所以在將r從pending鏈上斷開前,做這個處理\n c = r instanceof Cleaner ? (Cleaner) r : null;\n //將將r從pending鏈上斷開\n pending = r.discovered;\n r.discovered = null;\n } else {\n //等待CG後的通知\n if (waitForNotify) {\n lock.wait();\n }\n //重試\n return waitForNotify;\n }\n }\n } catch (OutOfMemoryError x) {\n //當拋出OOME時,放棄CPU的運行時間,這樣有希望收回一些存活的引用並且GC能回收部分空間。同時能避免頻繁地自旋重試,導致連續的OOME異常\n Thread.yield();\n //重試\n return true;\n } catch (InterruptedException x) {\n //重試\n return true;\n }\n //如果是Cleaner類型的Reference調用其clean方法並退出\n if (c != null) {\n c.clean();\n return true;\n }\n ReferenceQueue super Object> q = r.queue;\n //如果Reference有註冊ReferenceQueue,則處理pending指向的Reference結點將其加入ReferenceQueue中\n if (q != ReferenceQueue.NULL) q.enqueue(r);\n return true;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面tryHandlePending方法中比較重要的點是c.clean()與q.enqueue®,這個是文章最開始提到的兩個問題答案的入口。Cleaner的clean方法用於完成清理工作,而ReferenceQueue是將被回收對象加入到對應的Reference列隊中,等待其他線程的後繼處理。更具體地關於Cleaner與ReferenceQueue後面會再詳細說明。Reference的核心處理流程可總結如下:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/58/58765255eb1b5e1a687d22d730c5eaed.png","alt":"reference處理流程.png","title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對Reference的核心處理流程有整體瞭解後,再來回過頭細看一下Reference類的源碼。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"/* Reference實例有四種內部的狀態\n * Active: 新創建Reference的實例其狀態爲Active。當GC檢測到Reference引用的referent可達到狀態發生改變時,\n * 爲改變Reference的狀態爲Pending或Inactive。這個取決於創建Reference實例時是否註冊過ReferenceQueue。\n * 註冊過其狀態會轉換爲Pending,同時GC會將其加入pending-Reference鏈表中,否則爲轉換爲Inactive狀態。\n * Pending: 代表Reference是pending-Reference鏈表的成員,等待ReferenceHandler線程調用Cleaner#clean\n * 或ReferenceQueue#enqueue操作。未註冊過ReferenceQueue的實例不會達到這個狀態\n * Enqueued: Reference實例成爲其被創建時註冊過的ReferenceQueue的成員,代表已入隊列。當其從ReferenceQueue\n * 中移除後,其狀態會變爲Inactive。\n * Inactive: 什麼也不會做,一旦處理該狀態,就不可再轉換。\n * 不同狀態時,Reference對應的queue與成員next變量值(next可理解爲ReferenceQueue中的下個結點的引用)如下:\n * Active: queue爲Reference實例被創建時註冊的ReferenceQueue,如果沒註冊爲Null。此時,next爲null,\n * Reference實例與queue真正產生關係。\n * Pending: queue爲Reference實例被創建時註冊的ReferenceQueue。next爲當前實例本身。\n * Enqueued: queue爲ReferenceQueue.ENQUEUED代表當前實例已入隊列。next爲queue中的下一實列結點,\n * 如果是queue尾部則爲當前實例本身\n * Inactive: queue爲ReferenceQueue.NULL,當前實例已從queue中移除與queue無關聯。next爲當前實例本身。\n */\npublic abstract class Reference {\n// Reference 引用的對象\nprivate T referent;\n/* Reference註冊的queue用於ReferenceHandler線程入隊列處理與用戶線程取Reference處理。\n * 其取值會根據Reference不同狀態發生改變,具體取值見上面的分析\n */\nvolatile ReferenceQueue super T> queue;\n// 可理解爲註冊的queue中的下一個結點的引用。其取值會根據Reference不同狀態發生改變,具體取值見上面的分析\nvolatile Reference next;\n/* 其由VM維護,取值會根據Reference不同狀態發生改變,\n * 狀態爲active時,代表由GC維護的discovered-Reference鏈表的下個節點,如果是尾部則爲當前實例本身\n * 狀態爲pending時,代表pending-Reference的下個節點的引用。否則爲null\n */\ntransient private Reference discovered;\n/* pending-Reference 鏈表頭指針,GC回收referent後會將Reference加pending-Reference鏈表。\n * 同時ReferenceHandler線程會獲取pending指針,不爲空時Cleaner.clean()或入列queue。\n * pending-Reference會採用discovered引用接鏈表的下個節點。\n */\nprivate static Reference pending = null;\n// 可理解爲註冊的queue中的下一個結點的引用。其取值會根據Reference不同狀態發生改變,具體取值見上面的分析\nvolatile Reference next;\n//用於CG同步Reference成員變量值的對象。\nstatic private class Lock { }\nprivate static Lock lock = new Lock();\n//省略部分代碼...\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面解釋了Reference中的主要成員的作用,其中比較重要是Reference內部維護的不同狀態,其狀態不同成員變量queue、pending、discovered、next的取值都會發生變化。Reference的主要方法如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//構造函數,指定引用的對象referent\nReference(T referent) {\n this(referent, null);\n}\n//構造函數,指定引用的對象referent與註冊的queue\nReference(T referent, ReferenceQueue super T> queue) {\n this.referent = referent;\n this.queue = (queue == null) ? ReferenceQueue.NULL : queue;\n}\n//獲取引用的對象referent\npublic T get() {\n return this.referent;\n}\n//將當前對象加入創建時註冊的queue中\npublic boolean enqueue() {\n return this.queue.enqueue(this);\n}\n"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"ReferenecQueue與Cleaner源碼分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"先來看下ReferenceQueue的主要成員變量的含義。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//代表Reference的queue爲null。Null爲ReferenceQueue子類\nstatic ReferenceQueue NULL = new Null<>();\n//代表Reference已加入當前ReferenceQueue中。\nstatic ReferenceQueue ENQUEUED = new Null<>();\n//用於同步的對象\nprivate Lock lock = new Lock();\n//當前ReferenceQueue中的頭節點\nprivate volatile Reference extends T> head = null;\n//ReferenceQueue的長度\nprivate long queueLength = 0;\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"ReferenceQueue中比較重要的方法爲enqueue、poll、remove方法。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//入列隊enqueue方法,只被Reference類調用,也就是上面分析中ReferenceHandler線程爲調用\nboolean enqueue(Reference extends T> r) {\n\t//獲取同步對象lock對應的監視器對象\n synchronized (lock) {\n //獲取r關聯的ReferenceQueue,如果創建r時未註冊ReferenceQueue則爲NULL,同樣如果r已從ReferenceQueue中移除其也爲null\n ReferenceQueue> queue = r.queue;\n //判斷queue是否爲NULL 或者 r已加入ReferenceQueue中,是的話則入隊列失敗\n if ((queue == NULL) || (queue == ENQUEUED)) {\n return false;\n }\n assert queue == this;\n //設置r的queue爲已入隊列\n r.queue = ENQUEUED;\n //如果ReferenceQueue頭節點爲null則r的next節點指向當前節點,否則指向頭節點\n r.next = (head == null) ? r : head;\n //更新ReferenceQueue頭節點\n head = r;\n //列隊長度加1\n queueLength++;\n //爲FinalReference類型引用增加FinalRefCount數量\n if (r instanceof FinalReference) {\n sun.misc.VM.addFinalRefCount(1);\n }\n //通知remove操作隊列有節點\n lock.notifyAll();\n return true;\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"poll方法源碼相對簡單,其就是從ReferenceQueue的頭節點獲取Reference。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public Reference extends T> poll() {\n //頭結點爲null直接返回,代表Reference還沒有加入ReferenceQueue中\n if (head == null)\n return null;\n //獲取同步對象lock對應的監視器對象\n synchronized (lock) {\n return reallyPoll();\n }\n}\n//從隊列中真正poll元素的方法\nprivate Reference extends T> reallyPoll() {\n Reference extends T> r = head;\n //double check 頭節點不爲null\n if (r != null) {\n \t//保存頭節點的下個節點引用\n Reference extends T> rn = r.next;\n //更新queue頭節點引用\n head = (rn == r) ? null : rn;\n //更新Reference的queue值,代表r已從隊列中移除\n\t\tr.queue = NULL;\n\t\t//更新Reference的next爲其本身\n r.next = r;\n queueLength--;\n //爲FinalReference節點FinalRefCount數量減1\n if (r instanceof FinalReference) {\n sun.misc.VM.addFinalRefCount(-1);\n }\n //返回獲取的節點\n return r;\n }\n return null;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"remove方法的源碼如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public Reference extends T> remove(long timeout) throws IllegalArgumentException, InterruptedException {\n if (timeout < 0) {\n throw new IllegalArgumentException(\"Negative timeout value\");\n }\n //獲取同步對象lock對應的監視器對象\n synchronized (lock) {\n \t//獲取隊列頭節點指向的Reference\n Reference extends T> r = reallyPoll();\n //獲取到返回\n if (r != null) return r;\n long start = (timeout == 0) ? 0 : System.nanoTime();\n //在timeout時間內嘗試重試獲取\n for (;;) {\n \t//等待隊列上有結點通知\n lock.wait(timeout);\n //獲取隊列中的頭節點指向的Reference\n r = reallyPoll();\n //獲取到返回\n if (r != null) return r;\n if (timeout != 0) {\n long end = System.nanoTime();\n timeout -= (end - start) / 1000_000;\n //已超時但還沒有獲取到隊列中的頭節點指向的Reference返回null\n if (timeout <= 0) return null;\n start = end;\n }\n }\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"簡單的分析完ReferenceQueue的源碼後,再來整體回顧一下Reference的核心處理流程。JVM在GC時如果當前對象只被Reference對象引用,JVM會根據Reference具體類型與堆內存的使用情況決定是否把對應的Reference對象加入到一個由Reference構成的pending鏈表上,如果能加入pending鏈表JVM同時會通知ReferenceHandler線程進行處理。ReferenceHandler線程收到通知後會調用Cleaner#clean或ReferenceQueue#enqueue方法進行處理。如果引用當前對象的Reference類型爲WeakReference且堆內存不足,那麼JVM就會把WeakReference加入到pending-Reference鏈表上,然後ReferenceHandler線程收到通知後會異步地做入隊列操作。而我們的應用程序中的線程便可以不斷地去拉取ReferenceQueue中的元素來感知JVM的堆內存是否出現了不足的情況,最終達到根據堆內存的情況來做一些處理的操作。實際上WeakHashMap低層便是過通上述過程實現的,只不過實現細節上有所偏差,這個後面再分析。再來看看ReferenceHandler線程收到通知後可能會調用的另外一個類Cleaner的實現。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同樣先看一下Cleaner的成員變量,再看主要的方法實現。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//繼承了PhantomReference類也就是虛引用,PhantomReference源碼很簡單只是重寫了get方法返回null\npublic class Cleaner extends PhantomReference {\n\t/* 虛隊列,命名很到位。之前說CG把ReferenceQueue加入pending-Reference鏈中後,ReferenceHandler線程在處理時\n * 是不會將對應的Reference加入列隊的,而是調用Cleaner.clean方法。但如果Reference不註冊ReferenceQueue,GC處理時\n * 又無法把他加入到pending-Reference鏈中,所以Cleaner裏面有了一個dummyQueue成員變量。\n */\n private static final ReferenceQueue dummyQueue = new ReferenceQueue();\n //Cleaner鏈表的頭結點\n private static Cleaner first = null;\n //當前Cleaner節點的後續節點\n private Cleaner next = null;\n //當前Cleaner節點的前續節點\n private Cleaner prev = null;\n //真正執行清理工作的Runnable對象,實際clean內部調用thunk.run()方法\n private final Runnable thunk;\n //省略部分代碼...\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從上面的成變量分析知道Cleaner實現了雙向鏈表的結構。先看構造函數與clean方法。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"//私有方法,不能直接new\nprivate Cleaner(Object var1, Runnable var2) {\n super(var1, dummyQueue);\n this.thunk = var2;\n}\n//創建Cleaner對象,同時加入Cleaner鏈中。\npublic static Cleaner create(Object var0, Runnable var1) {\n return var1 == null ? null : add(new Cleaner(var0, var1));\n}\n//頭插法將新創意的Cleaner對象加入雙向鏈表,synchronized保證同步\nprivate static synchronized Cleaner add(Cleaner var0) {\n if (first != null) {\n var0.next = first;\n first.prev = var0;\n }\n //更新頭節點引用\n first = var0;\n return var0;\n}\n\npublic void clean() {\n\t//從Cleaner鏈表中先移除當前節點\n if (remove(this)) {\n try {\n \t//調用thunk.run()方法執行對應清理邏輯\n this.thunk.run();\n } catch (final Throwable var2) {\n //省略部分代碼..\n }\n\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到Cleaner的實現還是比較簡單,Cleaner實現爲PhantomReference類型的引用。當JVM GC時如果發現當前處理的對象只被PhantomReference類型對象引用,同之前說的一樣其會將該Reference加pending-Reference鏈中上,只是ReferenceHandler線程在處理時如果PhantomReference類型實際類型又是Cleaner的話。其就是調用Cleaner.clean方法做清理邏輯處理。Cleaner實際是DirectByteBuffer分配的堆外內存收回的實現,具體見下面的分析。"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"DirectByteBuffer堆外內存回收與WeakHashMap敏感內存回收"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"繞開了一大圈終於回到了文章最開始提到的兩個問題,先來看一下分配給DirectByteBuffer堆外內存是如何回收的。在創建DirectByteBuffer時我們實際是調用ByteBuffer#allocateDirect方法,而其實現如下:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public static ByteBuffer allocateDirect(int capacity) {\n return new DirectByteBuffer(capacity);\n}\n\nDirectByteBuffer(int cap) {\n //省略部分代碼...\n try {\n \t//調用unsafe分配內存\n base = unsafe.allocateMemory(size);\n } catch (OutOfMemoryError x) {\n //省略部分代碼...\n }\n //省略部分代碼...\n //前面分析中的Cleaner對象創建,持有當前DirectByteBuffer的引用\n cleaner = Cleaner.create(this, new Deallocator(base, size, cap));\n att = null;\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"裏面和DirectByteBuffer堆外內存回收相關的代碼便是Cleaner.create(this, new Deallocator(base, size, cap))這部分。還記得之前說實際的清理邏輯是裏面和DirectByteBuffer堆外內存回收相關的代碼便是Cleaner裏面的Runnable#run方法嗎?直接看Deallocator.run方法源碼:"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"public void run() {\n if (address == 0) {\n // Paranoia\n return;\n }\n //通過unsafe.freeMemory釋放創建的堆外內存\n unsafe.freeMemory(address);\n address = 0;\n Bits.unreserveMemory(size, capacity);\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"終於找到了分配給DirectByteBuffer堆外內存是如何回收的的答案。再總結一下,創建DirectByteBuffer對象時會創建一個Cleaner對象,Cleaner對象持有了DirectByteBuffer對象的引用。當JVM在GC時,如果發現DirectByteBuffer被地方法沒被引用啦,JVM會將其對應的Cleaner加入到pending-reference鏈表中,同時通知ReferenceHandler線程處理,ReferenceHandler收到通知後,會調用Cleaner#clean方法,而對於DirectByteBuffer創建的Cleaner對象其clean方法內部會調用unsafe.freeMemory釋放堆外內存。最終達到了DirectByteBuffer對象被GC回收其對應的堆外內存也被回收的目的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"再來看一下文章開始提到的另外一個問題WeakHashMap如何實現敏感內存的回收。實際WeakHashMap實現上其Entry繼承了WeakReference。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"\n//Entry繼承了WeakReference, WeakReference引用的是Map的key\nprivate static class Entry extends WeakReference implements Map.Entry {\n V value;\n final int hash;\n Entry next;\n /**\n * 創建Entry對象,上面分析過的ReferenceQueue,這個queue實際是WeakHashMap的成員變量,\n * 創建WeakHashMap時其便被初始化 final ReferenceQueue queue = new ReferenceQueue<>()\n */\n Entry(Object key, V value,\n ReferenceQueue queue,\n int hash, Entry next) {\n super(key, queue);\n this.value = value;\n this.hash = hash;\n this.next = next;\n }\n //省略部分原碼...\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"往WeakHashMap添加元素時,實際都會調用Entry的構造方法,也就是會創建一個WeakReference對象,這個對象的引用的是WeakHashMap剛加入的Key,而所有的WeakReference對象關聯在同一個ReferenceQueue上。我們上面說過JVM在GC時,如果發現當前對象只有被WeakReference對象引用,那麼會把其對應的WeakReference對象加入到pending-reference鏈表上,並通知ReferenceHandler線程處理。而ReferenceHandler線程收到通知後,對於WeakReference對象會調用ReferenceQueue#enqueue方法把他加入隊列裏面。現在我們只要關注queue裏面的元素在WeakHashMap裏面是在哪裏被拿出去啦做了什麼樣的操作,就能找到文章開始問題的答案啦。最終能定位到WeakHashMap的expungeStaleEntries方法。"}]},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"private void expungeStaleEntries() {\n //不斷地從ReferenceQueue中取出,那些只有被WeakReference對象引用的對象的Reference\n for (Object x; (x = queue.poll()) != null; ) {\n synchronized (queue) {\n //轉爲 entry\n Entry e = (Entry) x;\n //計算其對應的桶的下標\n int i = indexFor(e.hash, table.length);\n //取出桶中元素\n Entry prev = table[i];\n Entry p = prev;\n //桶中對應位置有元素,遍歷桶鏈表所有元素\n while (p != null) {\n Entry next = p.next;\n //如果當前元素(也就是entry)與queue取出的一致,將entry從鏈表中去除\n if (p == e) {\n if (prev == e)\n table[i] = next;\n else\n prev.next = next;\n // Must not null out e.next;\n //清空entry對應的value\n e.value = null;\n size--;\n break;\n }\n prev = p;\n p = next;\n }\n }\n }\n}\n"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在只看一下WeakHashMap哪些地方會調用expungeStaleEntries方法就知道什麼時候WeakHashMap裏面的Key變得軟可達時我們就可以將其對應的Entry從WeakHashMap裏面移除。直接調用有三個地方分別是getTable方法、size方法、resize方法。 getTable方法又被很多地方調用如get、containsKey、put、remove、containsValue、replaceAll。最終看下來,只要對WeakHashMap進行操作就行調用expungeStaleEntries方法。所有隻要操作了WeakHashMap,沒WeakHashMap裏面被再用到的Key對應的Entry就會被清除。再來總結一下,爲什麼WeakHashMap適合作爲內存敏感緩存的實現。當JVM 在GC時,如果發現WeakHashMap裏面某些Key沒地方在被引用啦(WeakReference除外),JVM會將其對應的WeakReference對象加入到pending-reference鏈表上,並通知ReferenceHandler線程處理。而ReferenceHandler線程收到通知後將對應引用Key的WeakReference對象加入到 WeakHashMap內部的ReferenceQueue中,下次再對WeakHashMap做操作時,WeakHashMap內部會清除那些沒有被引用的Key對應的Entry。這樣就達到了每操作WeakHashMap時,自動的檢索並清量沒有被引用的Key對應的Entry的目地。"}]},{"type":"heading","attrs":{"align":null,"level":1},"content":[{"type":"text","text":"總結"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文通過兩個問題引出了JDK中Reference相關類的源碼分析,最終給出了問題的答案。但實際上一般開發規範中都會建議禁止重寫Object#finalize方法同樣與Reference類關係密切(具體而言是Finalizer類)。受篇幅的限制本文並未給出分析,有待各位自己看源碼啦。半年沒有寫文章啦,有點對不住關注的小夥伴。希望看完本文各位或多或少能有所收穫。如果覺得本文不錯就幫忙轉發記得標一下出處,謝謝。後面我還會繼續分享一些自己覺得比較重要的東西給大家。由於個人能力有限,文中不足與錯誤還望指正。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"看完三件事❤️"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果你覺得這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:"}]},{"type":"numberedlist","attrs":{"start":null,"normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"點贊,轉發,有你們的 『點贊和評論』,纔是我創造的動力。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"關注公衆號 『 "},{"type":"text","marks":[{"type":"strong"}],"text":"java爛豬皮"},{"type":"text","text":" 』,不定期分享原創知識。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"同時可以期待後續文章ing🚀"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":""}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/34/34172ad7f3cc8e0f28bd1fc6ca2d2b68.png","alt":null,"title":"","style":[{"key":"width","value":"50%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":""}]},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文作者:葉易 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"出處:https://club.perfma.com/article/125010"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章