探究JVM——垃圾回收

来源:http://www.cnblogs.com/z941030/archive/2016/05/01/5449459.html
-Advertisement-
Play Games

垃圾回收主要考慮三件事情:哪些記憶體需要回收?什麼時候回收?如何回收? 一、哪些記憶體需要回收? 堆記憶體:對於JVM 來說,垃圾回收主要是針對堆記憶體中的對象實例。 方法區:垃圾收集行為在方法區是比較少出現的,一般來說,這個區域的回收“成績”比較難以令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分區域 ...


垃圾回收主要考慮三件事情:哪些記憶體需要回收?什麼時候回收?如何回收?

 

一、哪些記憶體需要回收?

 

堆記憶體:對於JVM 來說,垃圾回收主要是針對堆記憶體中的對象實例。

方法區:垃圾收集行為在方法區是比較少出現的,一般來說,這個區域的回收“成績”比較難以令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分區域的回收確實是必要的。

 

 

二、什麼時候回收?

  

  在堆裡面存放著Java世界中幾乎所有的對象實例,垃圾收集器在對堆進行回收前,第一件事情就是要確定這些對象之中哪些還“存活”著,哪些已經“死去”(即不可能再被任何途徑使用的對象)。那麼,怎麼判斷呢?

  

引用計數演算法(Reference Counting)

  很多教科書上給出的演算法是這樣的:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的對象就是不可能再被使用的。

  引用計數演算法的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的演算法,但是,至少主流的Java虛擬機裡面沒有選用引用計數演算法來管理記憶體,其中最主要的原因是它很難解決對象之間相互迴圈引用的問題。

舉個慄子:看下麵代碼(示例代碼來自《深入理解Java虛擬機》)

 1 /**
 2 *testGC()方法執行後,objA和objB會不會被GC呢?
 3 */
 4 public class ReferenceCountingGC{
 5     public Object instance=null 6     private static final int_1MB=1024*1024 7     /**
 8     *這個成員屬性的唯一意義就是占點記憶體,以便能在GC日誌中看清楚是否被回收過
 9     */
10     private byte[]bigSize=new byte[2*_1MB];
11     public static void testGC(){
12         ReferenceCountingGC objA=new ReferenceCountingGC();
13         ReferenceCountingGC objB=new ReferenceCountingGC();
14         objA.instance=objB;
15         objB.instance=objA;
16         objA=null17         objB=null18         //假設在這行發生GC,objA和objB是否能被回收?
19         System.gc();
20     }
21 }
22             

 

  上述代碼中的testGC()方法:對象objA和objB都有欄位instance,賦值令objA.instance=objB及objB.instance=objA,除此之外,這兩個對象再無任何引用,實際上這兩個對象已經不可能再被訪問,但是它們因為互相引用著對方,導致它們的引用計數都不為0,於是引用計數演算法無法通知GC收集器回收它們。

 

下麵介紹一種演算法:可達性分析演算法

 

可達性分析演算法(Reachability Analysis)

 

  在主流的商用程式語言(Java、C#等)的主流實現中,都是稱通過可達性分析來判定對象是否存活的。

  

  這個演算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。

  

  繼續舉慄子,如下圖所示,對象object 5、object 6、object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的對象。

          

 

在Java語言中,可作為GC Roots的對象包括下麵幾種:

 

虛擬機棧(棧幀中的本地變數表)中引用的對象。

方法區中類靜態屬性引用的對象。

方法區中常量引用的對象。

本地方法棧中JNI(即一般說的Native方法)引用的對象。

 

那麼問題就來了,即使在可達性分析演算法中不可達的對象,也並非是“非死不可”的。

  

要真正宣告一個對象死亡,至少要經歷兩次標記過程:

 

  ①如果對象在進行可達性分析後發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法:

  當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。

  如果這個對象被判定為有必要執行finalize()方法,那麼這個對象將會放置在一個叫做F-Queue的隊列之中,併在稍後由一個由虛擬機自動建立的、低優先順序的Finalizer線程去執行它。這裡所謂的“執行”是指虛擬機會觸發這個方法,但並不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死迴圈(更極端的情況),將很可能會導致F-Queue隊列中其他對象永久處於等待,甚至導致整個記憶體回收系統崩潰。

 

  ②finalize()方法是對象逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變數或者對象的成員變數,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。

 

finalize()方法應儘量避免使用,它的運行代價高昂,不確定性大,無法保證各個對象的調用順序。

 

 

關於方法區的回收

 

  Java虛擬機規範中不要求虛擬機在方法區實現垃圾收集,而且在方法區中進行垃圾收集的“性價比”一般比較低:在堆中,尤其是在新生代中,常規應用進行一次垃圾收集一般可以回收70%~95%的空間,而永久代的垃圾收集效率遠低於此。

  永久代的垃圾收集主要回收兩部分內容:廢棄常量無用的類

 

  回收廢棄常量與回收Java堆中的對象非常類似:

  以常量池中字面量的回收為例,假如一個字元串“abc”已經進入了常量池中,但是當前系統沒有任何一個String對象是叫做“abc”的,換句話說,就是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果這時發生記憶體回收,而且必要的話,這個“abc”常量就會被系統清理出常量池。常量池中的其他類(介面)、方法、欄位的符號引用也與此類似。

 

  判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下麵3個條件才能算是“無用的類”:

  ①該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例。

  ②載入該類的ClassLoader已經被回收。

  ③該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

 

  虛擬機可以對滿足上述3個條件的無用類進行回收,這裡說的僅僅是“可以”,而並不是和對象一樣,不使用了就必然會回收。是否對類進行回收,HotSpot虛擬機提供了-Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClassLoading、-XX:+TraceClassUnLoading查看類載入和卸載信息,其中-verbose:class和-XX:+TraceClassLoading可以在Product版的虛擬機中使用,-XX:+TraceClassUnLoading參數需要FastDebug版的虛擬機支持。

 

  在大量使用反射、動態代理、CGLib等ByteCode框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要虛擬機具備類卸載的功能,以保證永久代不會溢出。

 

 

三、如何回收?

 

下麵是幾種常見的垃圾收集演算法:

 

標記-清除演算法(Mark-Sweep)

 

  最基礎的收集演算法,演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象,(標記過程在上面對象標記判定時已經提過了)。

  之所以說它是最基礎的收集演算法,是因為後續的收集演算法都是基於這種思路並對其不足進行改進而得到的。它的主要不足有兩個:

  ①效率問題,標記和清除兩個過程的效率都不高;

  ②空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式運行過程中需要分配較大對象時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

 

標記—清除演算法的執行過程如圖所示

 

 

複製演算法(Copying)

 

  為瞭解決效率問題,一種稱為“複製”的收集演算法出現了,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。

  這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指針,按順序分配記憶體即可,實現簡單,運行高效。只是這種演算法的代價是將記憶體縮小為了原來的一半,未免太高了一點。

複製演算法的執行過程如圖所示

  

  研究表明,新生代中的對象98%是“朝生夕死”的,所以並不需要按照1:1的比例來劃分記憶體空間,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor當回收時,將Eden和Survivor中還存活著的對象一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛纔用過的Survivor空間。

  HotSpot虛擬機預設Eden和Survivor的大小比例是8:1,也就是每次新生代中可用記憶體空間為整個新生代容量的90%(80%+10%),只有10%的記憶體會被“浪費”。當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多於10%的對象存活,當Survivor空間不夠用時,需要依賴其他記憶體(這裡指老年代)進行分配擔保(Handle Promotion)。

 

標記-整理演算法

 

  複製收集演算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法。

 

  根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)演算法,標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體,“標記-整理”演算法的示意圖如圖所示。

 

分代收集演算法

 

  當前商業虛擬機的垃圾收集都採用“分代收集”(Generational Collection)演算法,這種演算法並沒有什麼新的思想,只是根據對象存活周期的不同將記憶體劃分為幾塊。

  一般是把Java堆分為新生代老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。

  新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製演算法,只需要付出少量存活對象的複製成本就可以完成收集。

  老年代中,因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”演算法來進行回收。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 問題描述 1、首先讓我們先看一張圖 2、從圖中,我們可以很清楚的看到當http請求的站點訪問https的資源的時候會報出“Cross-Origin”跨域的問題。為什麼會出現這樣的錯誤,這是因為涉及到“同源策略”的問題。。。blablabla…… 3、下麵依次說如何解決這個問題 問題解決 1、我們再來 ...
  • 引言: 以前在面試的過程中,總有面試官問道:你做過sql性能優化嗎?對此,我的答覆是沒有。一次沒有不是自己的錯誤,兩次也不是,但如果是多次呢?今天痛下決心,把有關sql性能優化的相關知識總結一下,以便在不久的將來,我的回答不是“沒有”,總能多多少少說一些東西。算是長進吧。說到性能優化,本人感覺到有必 ...
  • 簡介 之前做過一個文件名稱生成器,通過Webservice讀取XML文件並將其通過Json傳到客戶端中的combobx,用戶通過combobox選擇要生成文件的名稱模板,點擊生成則會產生一個文件名稱並保存到資料庫中。 涉及到的編程內容 webservice,XML,Access,Winform We ...
  • 1.繼上一篇隨筆,鏈接點我,解決手機端cookie的問題。 2.上次用cookie+redis實現了session,並且手機瀏覽器可能回傳cookies有問題,所以最後用js取出cookie跟在請求的url後面。 3.但是今天發現了新的問題,js取cookie存的sessionId為空,情況如下: ...
  • 今天在做一個聯繫人管理的C#設計時,遇到了這個問題,我需要將父窗體中的textBox中的值傳到子窗體併進行資料庫查詢操作,我用了new 父窗體().textBox.text;來進行值傳遞,然而並無卵用,經過多次試驗,找到了一個比較簡單的解決方法: 父窗體:Logout 子窗體:Affirm 父窗體文 ...
  • 1 1 <appSettings> 2 2 <add key="webpages:Version" value="2.0.0.0" /> 3 3 <add key="webpages:Enabled" value="false" /> 4 4 <add key="PreserveLoginUrl" ...
  • 面試的時候遇到的一個題目: 函數類型:List<string> FindOut(string s1, Dictionary<string,int> dict1) 題目要求:將沒有分隔符的字元串s1根據字典dict1進行拆分單詞,若能夠完全拆分,則依次輸出拆分後的單詞;若不能完全拆分,則輸出null。 ...
  • 最近工作的時候遇到一個問題,根據Web端接收到的對象obj1,更新對應的對象值ogj2。先判斷obj1中屬性值是否為null, 若不等於null,則更新obj2中對應屬性值;若等於null,則保持obj2中對應屬性值不變。 先創建Student Class: 1 using System; 2 us ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...