No.4 Java的內存回收(內存回收)


1. Java引用的種類

 內存管理分為:內存分配和內存回收。都是由JVM自動處理的

  • 對象在內存中的狀態:可達、可恢復(回收前調用finalize方法)、不可達
    • JVM回收標准:是否還有引用變量引用該對象
    • 有向圖理解。線程對象作為根節點,變量、對象作為節點,引用關系作為有向邊。在有向圖中,從線程節點<當然線程對象也要存在,沒有被銷毀>可達的對象都是可達狀態。
  • 強引用
    • 一般的引用/大部分 都是強引用,被強引用的對象不會被回收,是內存泄露的主要原因之一。
  • 軟引用
    • 通過SoftReference類實現(該類的用法)
    • 只有軟引用的對象,當系統內存充足時不會被回收,當內存緊張時會被回收
    • 可以用來解決系統內存緊張的難題
  • 弱引用
    •  String str = "Java程序引用"; 方式定義的字符串直接量會被系統緩存(會使用強引用來引用),系統不會回收被緩存的字符串常量。 String str = new String("Java程序引用"); 方式則不會緩存

    • 當垃圾回收機制運行(具有不確定性)時,不管系統內存是否緊張,都會被回收(不確定性)。
    • WeakReference類,WeakHashMap更常用
  • 虛引用
    • 主要用於跟蹤對象被垃圾回收的狀態:程序可以通過檢查與虛引用關聯的引用隊列中是否已經包含指定的虛引用,從而了解虛引用所引用的對象是否即將被回收。
    • 不能單獨使用,需要和引用隊列聯合使用
      • 軟引用和弱引用與引用隊列聯合使用,系統回收被引用對象之后,將會把被回收對象的對應的引用添加到關聯的引用隊列中去。
      • 而虛引用與引用隊列聯合使用的時候,在對象被釋放之前,將把引用它的虛引用添加到引用隊列中去,這使得可以在對象被回收之前采取行動。
    • 無法通過虛引用獲取它所引用的對象

2. Java的內存泄露

  • 不在使用的內存卻沒有被回收,就是內存泄露
  • Java中,可達狀態但是不再使用的內存,會引起內存泄露(垃圾回收機制不會回收可達狀態的內存)
  • eg:ArrayList中的remove方法elementData[--size] = null;

3. 垃圾回收機制

  • 兩件事情:回收不可達對象;清理內存分配、回收過程中產生的內存碎片(不連續的內存空間)
  • 垃圾回收的基本算法
    • 串行回收和並行回收
    • 並發執行和應用程序停止
    • 壓縮、不壓縮、復制
      • 復制、標記清楚、標記壓縮
  • 堆內存的分代回收(堆內存被分為三個代來存放對象)
    • 依據:對象生存時間的長短,然后根據不同代采用不同的垃圾回收策略,充分發揮各自的優勢。
      • 基於如下兩點事實:①絕大多數對象不會被長時間引用,Young代就會被回收;②很老的對象和很新的對象之間很少存在相互引用的情況
    • Young代:復制算法;可達狀態的對象較少,復制成本不大
      • 1個Eden區和2個Survivor區:對象先被分配到Eden區(一些大的對象可能直接分配到Old代),Survivor區中的對象至少經歷一次垃圾回收。一次復制算法會將Eden區和1個Survivor區中的可達對象復制到另一個空的Survivor區中,然后進入下一個循環,即兩個Survivor區同時間有一個是用來保存對象,另一個是空的
    • Old代
      • Young代多次回收仍然存在的對象會進入到Old代中,其中對象的特點:不容易死;隨時間的流逝,其中的對象會越來越多,因此Old代的空間成本要比Young代大
      • 回收特點:垃圾回收的執行頻率無需太高,因為死去的對象較少;每次執行需要花費更多的時間完成
      • 標記壓縮算法,不會大量地產生內存碎片
    • Permanent代
      • 主要用於裝載Class、方法等信息,垃圾回收機制通常不會回收該代中的對象
    • 次要回收:當Young代的內存將要用完的時候,垃圾回收機制會對該代進行垃圾回收,回收頻率較高,系統開銷小。
    • 主要回收:當Old代的內存將要用完的時候,垃圾回收機制會進行全回收,即對Young代和Old代都要進行回收,此時回收成本較大,因此成為主要回收
    • 通常來說,Young代先被回收,Old代的回收頻率要低的多;內存壓縮時,每個代都獨立的進行壓縮
  • 常見垃圾回收器
    • 串行回收器
      • Young代:串行復制算法
      • Old代:串行標記壓縮算法,三個階段:mark、sweep、compact(壓縮階段,執行sliding compaction,將活動對象往Old代的前端移動,尾部保留連續的空間)
    • 並行回收器
      • Young:與串行回收器基本相似,增加多CPU並行的能力,即啟動了多個線程來並行回收(並不是與主線程並發,而是多個回收線程並行執行
      • Old:與串行完全相同
    • 並行壓縮回收器(將取代並行回收器)
      • Young:與並行完全相同
      • Old:將Old代分成幾個固定大小的區域
        • mark:多個回收線程並行標記可達對象(會更新可達對象所在區域的大小及其位置信息)
        • summary:操作Old代區域(而不是單個對象)。從最左邊檢驗區域密度,當某區域的密度達到某個數值時,判定該區域及其右邊區域應該回收(進行壓縮及回收空間),其左邊區域標識為密集區域(不會將新對象移到這里,也不會對該區域進行壓縮)。該階段:串行實現
        • compact:利用上階段生成的數據識別出需要裝填的區域,多個回收線程並行地將數據復制到該區域中。該階段后,Old代一端密集存儲大量的活動對象,另一端則為大塊的空閑塊。
    • 並發標識-清理 回收器(CMS),適用於實時性要求較高的程序,對Old代回收:並發執行,程序僅僅需要兩次很短的暫停
      • Young:與並行回收器完全相同,依然會導致應用程序暫停,
      • Old:並發操作
        • mark:
          • 垃圾回收開始時,短暫的暫停,標識直接引用的可達對象(initial mark);
          • 並發標識階段(concurrent marking phase),依據初始標識中發現的可達對象來尋找其他可達對象;
          • 再標記階段(remark),因為並發標識過程中 應用程序可能會重新產生可達對象,為避免漏掉這些,需要再次很短的暫停下,多線程並行地再標識
        • concurrent sweep:並發清理
      • 默認在Old代68%滿時就開始回收,因為是並行操作的,如果等到Old代滿了,應用程序就沒有可用內存了;而其他回收器回收時,不是並行操作的,在回收時,程序會暫停,應用程序不產生新的對象。
      • CMS不會對Old代進行內存壓縮,即它的可用空間是不連續的,保存了一份可用空間列表,分配內存效率下降。
      • CMS需要更大的堆內存,因為:並發標識時,此時應用程序也在分配對象,Old代會同時增長;在並發清理階段新成為垃圾的對象並不能立即被回收,只有等到下次垃圾回收階段時才被回收。<浮動垃圾>(mine:串行,mark之后執行sweep,之間程序暫停,不會再產生新的對象;並發,mark(含有並發mark)之后並發sweep,程序並發執行,仍然可能產生新的對象,待清理的對象和新產生的對象在一起自然要大一點的內存。)(總之,因為並行,可能在清理的時候又產生新的對象,所以需要更大點的內存;而串行時,清理的時候不會產生新的對象,清理完畢后才會產生新的對象,需要的內存沒有這么大)
      • 可執行附加選項強制回收permanent代的內存
      • 串行回收和並發回收對比(mine)
        • 串行:應用程序暫停——>回收器執行回收階段(mark——>....——>sweep...)——>程序繼續執行
        • 並發:應用程序短暫暫停1——>CMS執行initial mark(標記直接引用的可達對象)——>程序執行,CMS並發標記concurrent mark(標記通過初始標識標識的可達對象能尋找到的其他可達對象)——>程序短暫暫停2——>remark(標記並發標記時程序新產生的可達對象)——>程序繼續執行,並發清理<此時才真正開始清理操作,之前都是標識操作>

4. 內存管理的小技巧

  • 盡量使用直接量
    • 使用字符串以及Byte、Short、Interger...等包裝類時,直接使用直接量創建,而不用new 關鍵字new新的對象
  • 進行字符串拼接時使用StringBuilder/StringBuffer,而不是String(String不可變,用其拼接時會產生大量的臨時字符串)
  • 盡早釋放無用對象的引用
    • 一般情況下局部變量的作用時間比較段,對其引用值無需 = null;但是若是其所在的方法中執行了耗內存、費時的操作時,應該將對無用對象的引用變量的值賦值null,盡早釋放無用對象
  • 盡量少用靜態變量,尤其是靜態變量引用對象。因為靜態變量屬於Class類對象,類對象在permanent代中(它的類變量自然也在該代中),其存在時間很長
  • 避免在經常調用的方法、循環中創建對象。
    • 導致不斷的為對象分配(創建)、回收(銷毀)內存,這些操作都是影響性能的
  • 緩存經常使用的對象(eg:數據庫連接池)
    • 避免不斷的分配、回收操作
    • 方法:
      • 使用HashMap:控制容器中的key-value對不能太多,否則HashMap占用過大的內存將導致性能下降
      • 使用開源緩存項目
    • 緩存設計:
      • 犧牲空間來換取時間,都是使用容器保存已用過的對象。如何控制容器占用的內存,又保留大部分已用過的對象,是程序設計的關鍵。(一些緩存算法)
  • 盡量不使用finalize方法
    • 垃圾回收器回收資源前會調用該方法。
    • 回收機制本身已經教嚴重制約應用程序的性能
    • 本身回收機制的負擔就比較重,而且回收Young代內存會導致程序暫停,影響性能;再在finalize方法中執行資源清理會加重回收機制的負擔,導致運行效率更差
  • 考慮使用SoftReference
    • 創建長度很大的數組時,考慮使用軟引用進行包裝數組元素(內存夠時引用,不足時釋放)
    • 軟引用具有不確定性,當獲取引用對象時,需要顯示判斷對象是否為空(避免出現異常),若為空,應該重新創建。
  • 吼吼吼

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM