深入探索JVM自動資源管理
本文要點
了解C++ RAII模式和Java收尾機制(Finalization)間的差異。
深入Hotspot的源代碼,釐清Finalizer的註冊機制。
對比finalize()方法與Java 7的try-with-resources(TWR)語句。
查看TWR在位元組碼中的實現方式。
理解TWP優於finalize()的原因。
本文內容經授權摘錄自《Java優化》(「Optimizing Java」)一書。該書即將由O』Reilly出版社出版,作者是Ben Evans和James Gough,可從O Reilly和Amazon獲得預覽版。
InfoQ最近報道了有建議要棄用Object的finalize()方法。finalize()方法自Java 1.0開始就存在於Java中,雖然該方法一直被認為是一個糟糕的設計,也是Java平台遺留的一個大「毒瘤」。但是要在Java的Object類上棄用該方法,這無疑是一個非同尋常的操作。
背景知識
finalize()機制意在力圖提供一種自動資源管理,類似於C++及類似語言的RAII(資源獲取即初始化,Resource Acquisition Is Initialisation)模式。在RAIL模式中,提供了析構函數(在Java中就是finalize())實現自動清理資源,並在對象銷毀時釋放資源。
該模式的基本用例非常簡單。當一個對象被創建時,就接管了對一些資源的所有權。對象資源的所有權會持續存在於對象的整個生命周期。之後,當對象消亡時,資源的所有權會自動放棄。
下面讓我們看一個簡單的C++例子。該例子顯示了如何使用RAII模式包裹一個C風格的文件I/O操作。該技術的核心在於使用對象析構方法(在開始處添加「~」標識析構方法名,其後與類名相同)進行清理:
class file_error {}; class file { public: file(const char* filename) : _h_file(std::fopen(filename, "w+")) { if (_h_file == NULL) { throw file_error(); } } //析構函數。 ~file() { std::fclose(_h_file); } void write(const char* str) { if (std::fputs(str, _h_file) == EOF) { throw file_error(); } } void write(const char* buffer, std::size_t numc) { if (numc != 0 && std::fwrite(buffer, numc, 1, _h_file) == 0) { throw file_error() ; } } private: std::FILE* _h_file; };
該方法的標準合理性基於這一觀察:編程人員在打開一個文件句柄後,很容易在不再需要時忘記調用close()函數,因此有必要將資源的所有權綁定到對象的生命周期。這樣,對象資源的自動清除就變成了平台的職責,而非編程人員的職責。
這一理念給出了一種很好的設計,尤其是當一個類型存在的唯一理由是充當文件或網路Socket等資源的「持有者」時。
在Java中,上述設計的實現依賴於JVM的垃圾回收器,因為子系統可明確地指出對象不再存活。如果在類型上給出了一個finalize()方法,那麼該類型的所有對象會受到特殊的對待。垃圾回收器將會對覆寫了finalize()方法的對象做特殊處理。
注釋:JVM對可終結對象的註冊機制是:一旦```Object.```(即對特定類型的最終超類構造函數)成功返回,就在這些對象上運行一個特定的Handler。
我們需要知道Hotspot的一個實現細節,那就是除了標準的Java指令之外,虛擬機還具有一些實現特定指令的特殊位元組碼。這些特殊位元組碼用於重寫標準虛擬機,以處理某些特定的場景。
此處提供了位元組碼定義的完整列表,其中包括了標準Java以及特定於Hotspot的實現。
對我們而言,我們關心的是一個特定用例,即return_register_finalizer指令。具有該指令是十分有必要的,因為JVMTI可能會為Object.而重寫位元組碼。要準確地遵循標準並在正確的時間註冊Finalizer方法,需要識別在不重寫的情況下Object.完成的時間點,並且使用特殊位元組碼對該時間點進行標記。
一個對象一旦被註冊為需要終結,它並非立刻在垃圾回收周期中被回收(Reclaimed),而是要歷經如下的生命周期延續:
先前已註冊的可終結對象會被識別,並置於一個特殊的終結隊列中。
隨垃圾回收進程重啟應用線程後,將會有獨立的終結線程從上述隊列中獲取對象,並清空隊列。
每個對象將從隊列中移出,並啟動另一個終結線程,由該終結線程對該實例運行finalize()方法。
一旦finalize()方法終止,對象就已準備好,在下一回收周期中被實際回收。
總而言之,所有要被終結的對象,必須首先經由一個垃圾回收的標記,被標識為不可訪問,然後才能被終結,之後,需重新運行垃圾回收,對數據進行回收。這也意味著,可終結對象至少額外地多存活了一個垃圾回收循環。對於變成年老代(Tenured)的對象,這可能需要大量的時間。
該機制還存在一些超乎我們可接受程度的複雜性,即全部清空(drain)隊列線程必須啟動另一個實際運行finalize()方法的終結線程。必須採用這種做法,以防止出現可能的finalize()被阻塞情況。
如果finalize()運行於全部清空隊列線程上,那麼如果finalize()方法的編寫存在問題,那麼就會阻止整個機制正常工作。為避免發生這樣的問題,我們將不得不為每個需終結的對象實例創建一個全新的線程。
此外,終結線程還必須忽略任何已拋出的異常。乍一看這很奇怪,但是終結線程並不具備處理異常的有效方法,並且創建可終結對象的原始上下文早已不存在了。對於任一給出的用戶代碼,沒有任何可行的方法能感知到異常,或是從異常中恢復。
為澄清這一點,我們回顧一下,Java異常提供了一種展開(unwind)堆棧的方式,用於在從非致命錯誤中恢復的當前執行線程中發現方法。考慮到這一點,我們就能理解終結需要忽略異常這一限制,即finalize()調用並非發生在創建或執行對象的線程上,而是發生在另一個完全不同的線程上。
Finalizer類還提供了一些洞察,有助於理解一些額外的許可權是如何通過被賦予該許可權的運行時而賦予一個類。例如,該類包含了如下代碼:
/* 由VM調用 */ static void register(Object finalizee) { new Finalizer(finalizee); }
當然,上面的代碼在正常的應用代碼中是毫無意義的,因為它創建了一個未使用的對象。除非構造函數具有副作用(通常在Java中,副作用被認為是不好的設計),否則它不會做任何事情。在這種情況下,一種做法是「勾」(hook)一個新的可終結對象。
缺點
如果從技術角度全面地看,Java終結機制的實現存在著嚴重的缺陷,這是由於該機制與平台內存管理模式間的不匹配所導致的。
對於C++而言,內存是手工處理的,對象處於編程人員的顯式控制下,具有顯式的生命周期管理。這意味著,終結可在刪除對象時發生,資源的獲取和釋放直接地依賴於對象的生命周期。
Java的內存管理子系統是一種垃圾回收器,只有當無法再分配可用內存時才需要運行。因而,內存管理的運行時間間隔不確定(如果有可能的話)。由此,finalize()方法只有在對象被回收時才運行,時間也是不確定的。
如果將finalize()機制用於資源(例如文件句柄)的自動釋放,那麼對於何時(如果有的話)這些資源將實際可用,該機制缺乏保障。這使得finalize()機制從根本上不適合於它所聲明的用途,即自動資源管理。
Try-with-resources語句(TWR)
為了安全地處理佔有資源的對象,在Java 7中引入了try-with-resources語句。該語句提供了一種新的語法特性,專門設計用於資源的自動處理。這一語言層結構允許被管理的資源指定在關鍵字try後的圓括弧對中。
這必須是一個對象構造語句,在正常的Java代碼中是不允許的。Java編譯器還會檢查被創建的對象類型是否實現了AutoCloseable介面。該介面是Java 7中引入的Closeable介面的超介面,專用於此用途。
這樣,資源就存在於try語句塊的範圍中,並且在try語句塊範圍的最後。TWR實現了對close()方法的自動調用,而不是讓開發人員記住去調用該函數。從行為上看,對close()方法的調用類似於在finally語句塊中的處理。因此,即使在業務邏輯中拋出了異常,close()也會運行。
注釋:事實上,相比於人工編寫的代碼,清理的自動部分所生成的代碼更好。這是因為javac知道如何按順序關閉相互依賴的資源,例如JDBC連接及其相關類型。這意味著,使用try-with-resources語句是該機制的最佳使用方法,而不是採用手工關閉這樣的原有方式。
最關鍵的問題在於,現在局部變數的生存期限於一個單一的範圍中,因此自動清理變成依賴於一個範圍,而不再依賴於對象的生存期。例如:
即使一個簡單的try-with-resources語句,也會被編譯為一系列規模相當大的位元組碼。我們可以使用javap的-p選項進行查看生成的位元組碼,並導出為如下的反編譯形式:
public void readFirstLine(java.io.File) throws java.io.IOException; Code: 0: new #2 // class java/io/BufferedReader 3: dup 4: new #3 // class java/io/FileReader 7: dup 8: aload_1 9: invokespecial #4 // Method java/io/FileReader."":(Ljava/io/File;)V 12: invokespecial #5 // Method java/io/BufferedReader."":(Ljava/io/Reader;)V 15: astore_2 16: aconst_null 17: astore_3 18: aload_2 19: invokevirtual #6 // Method java/io/BufferedReader.readLine:()Ljava/lang/String; 22: astore 4 24: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; 27: aload 4 29: invokevirtual #8 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 32: aload_2 33: ifnull 108 36: aload_3 37: ifnull 58 40: aload_2 41: invokevirtual #9 // Method java/io/BufferedReader.close:()V 44: goto 108 47: astore 4 49: aload_3 50: aload 4 52: invokevirtual #11 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V 55: goto 108 58: aload_2 59: invokevirtual #9 // Method java/io/BufferedReader.close:()V 62: goto 108 65: astore 4 67: aload 4 69: astore_3 70: aload 4 72: athrow 73: astore 5 75: aload_2 76: ifnull 105 79: aload_3 80: ifnull 101 83: aload_2 84: invokevirtual #9 // Method java/io/BufferedReader.close:()V 87: goto 105 90: astore 6 92: aload_3 93: aload 6 95: invokevirtual #11 // Method java/lang/Throwable.addSuppressed:(Ljava/lang/Throwable;)V 98: goto 105 101: aload_2 102: invokevirtual #9 // Method java/io/BufferedReader.close:()V 105: aload 5 107: athrow 108: return Exception table: from to target type 40 44 47 Class java/lang/Throwable 18 32 65 Class java/lang/Throwable 18 32 73 any 83 87 90 Class java/lang/Throwable 65 75 73 any
儘管終結和try-with-resources語句在設計意圖上是一致的,但兩者是完全不同的。終結嚴重依賴於解釋器中的彙編代碼去註冊要終結的對象,並使用垃圾回收器,通過隊列以及獨立的專用終結線程進行清理。尤其是,在終結中幾乎不追蹤(Trace)位元組碼的運行機制,追蹤能力是由特定的虛擬機內部機制提供的。
與之相對比,try-with-resources語句完全是一種編譯時機制,可以看成是一種語法糖(Syntactic sugar)。它僅生成常規的位元組碼,不具有任何其他特殊的運行時行為。try-with-resources語句自動生成大量的位元組碼,這是它唯一可見的效果。這一行為可能會影響到JIT編譯器對使用該語句的方法有效地進行內聯或編譯。但是,這並不構成應避免使用該語句的原因。
總結一下,終結幾乎在所有情況下都不適用於做資源管理。終結依賴於垃圾回收,而垃圾回收本身就是一種非確定性過程。因此,任何依賴於終結的機制都無法確定資源的釋放時間,缺乏時間上的保證。
無論終結是否會在JDK中被棄用並最終被移除,我們給出的建議依然不變,即永遠不要編寫對finalize()方法重寫的類,並對自身代碼中存在的相似類進行重構。
要實現C++的RAII模式及類似的模式,我們推薦的最佳實踐是try-with-resources語句。它的確限制了將模式用於語句塊範圍的代碼,但這是由於Java平台缺少進入對象生存期的底層可見性。Java開發人員需在處理資源對象時,練習使用這些規則,並從儘可能高的高度審視它們,因為這些規則本身就是好的設計實踐。
作者簡介
Ben Evans是初創公司jClarity的聯合創始人,該公司致力於開發可以為開發和運維團隊提供幫助的性能工具和服務。他是LJC(倫敦的Java用戶組)的組織者之一,也是JCP執行委員會的成員之一,幫助定義Java生態系統中的一些標準。他還是「Java Champion」榮譽得主。他曾與人合著過《The Well-Grounded Java Developer》(中文版是《Java程序員修鍊之道》)和《Java in a Nutshell》(第6版)。他曾就Java平台、性能、並發和相關主題發表過多次演講。Ben提供演講、教學、撰寫和諮詢服務,細節可聯繫商談。
查看英文原文:Under The Hood with the JVM s Automatic Resource Management
※盤點國外多個APPStore應用屏幕截圖示例
※webpack2終極優化
※IntelliJ IDEA 2017.1 JDK 8 性能調優
※深入理解CSS外邊距摺疊
TAG:推酷 |
※慧程天下用產品資源整合+ERP管理平台,為自駕遊行業賦能
※資源預覆蓋自動路由檢索系統設計與研發
※跟蹤不斷突變的HIV基因組,合理利用公共衛生資源
※HTC將VR和智能手機進行結合 賦予團隊更多的資源
※資源 | 中文NLP資源庫
※Unity資源包管理器-全新項目管理方式
※自然資源資產管理和自然生態監管機構有望設立
※Uber CEO:會在電動自行車和電動滑板上投入更多資源
※自然資源部新使命:處理好自然資源監管與環境治理的關係
※密歇根州立大學NestDNN:動態分配多任務資源移動端深度學習框架
※MICE行業服務的核心理念會向「內容+資源」轉變
※PHP 統一資源處理 API——流的概述與使用詳解
※目的地資源投資開發熱度不減 酒店B2B領域進入併購整合期
※UE4_MMD資源卡通渲染
※珍貴資源隨身安全存放,東芝CANVIO PREMIUM升級版移動硬碟體驗
※分散式資源管理與作業調度
※中國移動整合IT核心資源 成立中移信息技術有限公司
※金融資源聚合平台FRAP
※VR/AR是未來科技的前沿和中心,將為共享資源V店打造更好的自己
※深入挖掘中國特色社會主義政治經濟學的思想史資源