當前位置:
首頁 > 知識 > 從Java虛擬機的內存區域、垃圾收集器及內存分配原則談Java的內存回收機制

從Java虛擬機的內存區域、垃圾收集器及內存分配原則談Java的內存回收機制

一、引言:

在Java中我們只需要輕輕地new一下,就可以為實例化一個類,並分配對應的內存空間,而後似乎我們也可以不用去管它,Java自帶垃圾回收器,到了對象死亡的時候垃圾回收器就會將死亡對象的內存回收。

真的只要根據需要巴拉巴拉地new而不用管內存回收了嗎?那為什麼會存在這麼多的內存溢出情況呢?下面我們就需要了解一下Java內存的回收機制,只有了解了其虛擬機的回收原理才能更好的管理內存,避免內存溢出。

二、Java虛擬機的內存區域

首先,我們得知道在我們的虛擬機中內存到底是怎麼劃分區域的,下面借用《深入理解Java虛擬機》一書中的一張圖。

從Java虛擬機的內存區域、垃圾收集器及內存分配原則談Java的內存回收機制

我們首先是把上述5個內存區域劃分為了左右兩塊,姑且假定左邊的為區域A,右邊的為區域B。這邊我們將內存劃分為左右兩塊是有依據的,依據是什麼呢?依據主要是根據線程所有性來劃分的,區域A中的方法區和堆是各個線程共享的內存區域,而與之對應的區域B中的虛擬機棧、本地方法棧、程序計數器都是線程私有的。

2.1 程序計數器

程序計數器可以看做是當前線程所執行的位元組碼的行號指示器。位元組碼解釋器通過改變這個計數器的值倆選取下一條要執行的位元組碼指令。

因為Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式實現的,在任一時刻,一個處理器內核只會執行一條線程中的命令。因此,網路線程切換後能夠恢復到特定位置,每個線程都需要有一個獨立的程序計數器。

註:在程序計數器中沒有規定任何的內存溢出錯誤。

2.2虛擬機棧

虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行過程中都會創建一個棧幀用於存儲局部變數表、操作數棧、動態鏈接、方法出口燈信息。

每一個方法從調用到執行完畢就對應一個棧幀在虛擬機棧都從入棧到出棧的過程。

局部變數表存放了編譯期可知的各種基本數據類型、對象引用(可能是指向對象起始地址的引用指針,也可能是指向代表對象的句柄)和returnAddress(指向一條位元組碼指向的地址)

在虛擬機棧中規定了棧溢出和內存溢出兩種異常。

2.3 本地方法棧

本地方法棧的作用與虛擬機棧是類似的,只不過本地方法棧是為虛擬機用到的native方法服務的。同樣在本地方法棧也規定了棧溢出和內存溢出兩種異常。

2.4 Java堆

Java堆是Java虛擬機所管理的內存中最大的一塊,Java堆是被所有線程共享的,它的功能很單一,就是存放對象實例。此外因為存放的是對象的實例,Java堆是垃圾回收器管理的主要區域,因此也被稱為GC堆。Java堆可以處於物理上不連續的內存空間,只要求其邏輯上是連續的即可。一般而言,Java堆是可擴展的(當然也可以實現為固定的),通過-Xmx和-Xms來控制。當沒有內存可供分配且堆也無法擴展的時候,就會拋出內存溢出異常

2.5 方法區

方法區用於存儲已經被載入的類信息、常量、靜態變數和即時編譯器編譯後的代碼等。方法區也常被人們稱為永久代,當然主要原因是因為在這塊區域中發生垃圾收集行為比較少。在Jdk1.7已經著手去永久代了,而在Jdk1.8中已經將永久代替換為了元空間(Metaspace)

運行時常量池是方法區的一部分,用於存放編譯器生成的各種字面量和符號引用。

因為是線程私有,程序計數器、虛擬機棧和本地方法棧隨線程而生,隨線程而死,每一個棧幀隨著方法的進入和退出有序地執行出棧和入棧的操作,每一個棧幀分配的內存也是在編譯期可知的,因此,因為這些區域的內存分配和回收具有確定性,所以我們不需要考慮回收的問題。(當方法或者線程結束的時候,內存自然也就回收了)

三、垃圾收集器及內存回收

在談垃圾收集器之前,我們首先需要明確的是:哪些內存需要回收?什麼時候回收?怎麼回收?

3.1 哪些垃圾需要回收?

我們怎麼判斷哪些是需要回收的垃圾呢?正如前面提到的,GC的重點是在Java堆中,那麼在垃圾收集器在對堆中的對象進行回收前,第一件需要判斷的就是哪些對象已死(即不可能再被任何途徑使用的對象)

怎麼判斷對象是否存活,不得不提一下廣為人知的引用計數法,即給一個對象添加一個引用計數器,每引用一次,計數器值加1,引用失效時,計數器值減1,當值為0時,該對象死亡。然而這個方法固然高效,但卻存在一個很大的問題,它很難解決對象間的相互循環引用,即A引用B,B引用A,但其實二者都沒有其他地方被引用,其二者已經不可能被訪問到了,從合理性角度,這兩個對象已經死了。

下面請出我們要介紹了主角,也是在Java虛擬機中所採用的方法---可達性分析演算法

這個演算法的基本思想就是通過一系列被稱為「GC Roots」對象作為起始點,從這些節點向下所示,走過的路徑被稱為引用鏈,游離在外的對象即為不可用的對象。

從Java虛擬機的內存區域、垃圾收集器及內存分配原則談Java的內存回收機制

那麼顯然這個方法的關鍵在於那些對象是可以作為GC Root:

1)虛擬機棧中引用的對象

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

3)方法區中常量引用的對象

4)本地方法棧中native方法引用的對象。

3.2 什麼時候需要回收?

從一般來講什麼時候需要回收,即當對象已死的時候需要回收,但從嚴格意義上來講,真正宣告一個對象死亡需要經歷上述兩次不可達的標記才會導致這個對象被收集。

當對象第一次被標記為不可達時,會對它進行一次篩選,判斷其是否有必要執行finalize方法,當對象沒有覆蓋finalize方法或者finalize方法已經被虛擬機調用過了,則不會執行finalize方法。

那我們就可以在finalize方法中對對象進行最後的自救了,即在finalize方法中為對象和GC ROOT的引用鏈中的任一對象建立關聯即可。

除了這個,因為考慮到內存的有限性,不僅僅是對象死亡後才需要回收,為了有效利用內存,我們還需要有一些具有類似性質的對象,在內存足夠時可以保留在內存中,當內存不夠時即可被回收。這個特效其實就是很多系統緩存中用到的。

為了實現上述特效,Java中對引用進行了擴展,將引用分為了強引用、軟引用、弱引用和虛引用4種,其引用強度依次減弱。

  1. 強引用:即我們一般的new出來的對象引用即為強引用
  2. 軟引用:用來描述一些還有用但不必需的對象,對於軟引用關聯的對象,當系統內存不足時會將這些對象列入到回收範圍內進行回收。通過SoftReference類實現軟引用
  3. 弱引用:用來描述非必需的對象,被弱引用關聯的對象只能生產到下一次垃圾收集發生之前,但是因為垃圾收集器的線程優先順序低,所以他也不一定會被回收。通過WeakReference類實現弱引用
  4. 虛引用:最弱的引用,一個對象是否有虛引用對其生存時間毫無影響,也無法通過虛引用來獲取到一個對象實例,它的唯一作用就是能夠在這個對象被回收時收到一個系統通知。通過PhantomReference類實現虛引用。

3.3 怎麼回收?

利用垃圾收集器進行回收。不同的垃圾收集器採用的收集演算法或許不同,而這也會使得其收集時的細節不同。

3.3.1下面主要描述幾種常用的收集演算法:

(1)標記-清除演算法:

1)標記:標記出所有需要回收的對象

2)清除:在標記完成後統一回收被標記的對象。

從Java虛擬機的內存區域、垃圾收集器及內存分配原則談Java的內存回收機制

上述圖片其實將這個演算法的主要缺點暴露無遺,可以發現回收後會產生大量不連續的內存碎片,空間碎片太多會導致以後在程序分配較大對象時無法找到連續內存而提前出發另一次垃圾回收,此外標記和清除過程效率也不高。

(2)複製演算法

複製演算法將可用內存分為了大小相等的兩塊,每次我們只使用其中的一塊,當這一塊內存用完了,我們將這一塊還存活的對象複製到另一塊中,然後將已使用過的那一塊內存空間一次性清空。

這樣我們相當於每次只對其中一塊進行內存回收,並且不會產生碎片,只需要一移動堆頂的指針,按順序分配內存即可。但缺點很明顯:我們可以使用的內存縮小為原來的一半。

從Java虛擬機的內存區域、垃圾收集器及內存分配原則談Java的內存回收機制

現在的商業虛擬機都採用這種演算法來回收新生代,不過與之不同的是,它是將新生代的內存分為較大的Eden空間和兩塊較小的Survivor控制項,每次使用Eden和其中一塊Survivor空間。當回收時,將Eden和Survivor中還存活的對象一次性複製到另外一塊Survivor上,最後清理到前面兩個空間中的對象。

從Java虛擬機的內存區域、垃圾收集器及內存分配原則談Java的內存回收機制

當然了,我們無法保證每次一塊Survivor中可以供所有存活的對象存活,所有依賴於老年代的內存進行分配擔保。

(3)標記-整理演算法

在標記清除演算法的基礎上,提出了標記整理演算法,這個演算法在標記清除演算法的基礎上,在標記完可回收對象後,將所有存活的對象向一端移動,然後直接清理掉端邊界以外的內存。

從Java虛擬機的內存區域、垃圾收集器及內存分配原則談Java的內存回收機制

(4)分代收集演算法

當前虛擬機的垃圾收集都採用分代收集的思想,其實這個演算法的核心在於根據對象存活的周期不同將內存劃分為幾塊,一般分為新生代和老年代,這樣就可以根據各個年代的特點選擇合適的演算法收集。

1)在新生代中,對象朝生夕死,只有少量存活,可以選擇複製演算法。當新生代中的內存空間不夠時,可以依賴老年代的內存空間。(即老年代為新生代進行分配擔保)

2)在老年代中對象存活率高。沒有額外空間進行分配擔保,就必須使用「標記-清除」或者是「標記-整理」演算法。

3.3.2 Java的回收策略:

為了更好了解Java的回收策略,我們得首先對Java的內存分配規則。

1)Java的內存分配規則

從Java虛擬機的內存區域、垃圾收集器及內存分配原則談Java的內存回收機制

Java對象的內存分配,即在堆上進行分配,對象主要被分配在新生代的Eden區(如果啟動了本地線程分配緩存,將按線程優先在TLAB上分配),少數情況下直接分配在老年代中。下面是幾條規則:

(1)對象優先在Eden分配:

大多數情況下,對象在新生代Eden區分配,當Eden去沒有足夠空間分配時,虛擬機將發起一次Minor GC。

(2)大對象直接進行老年代:

所謂的大對象即是需要大量練習內存空間的Java對象,最典型的大對象就是那種很長的字元串以及數組。

(3)長期存活的對象將進入老年代:

虛擬機為每個對象定義了一個Age計數器,當對象在Eden出生,並經過一次Minor GC仍然存活並能夠被Survivor容納,將被移動到Survivor空間中,當其在Survivor區中每熬過一次Minor GC,年齡加1,當年齡到一定歲數後即會升級到老年代中,這是第一種升級的方法。

第二種升級發方法是如果在Survivor空間中相同年齡的所有對象大小總和大於Survivor控制項的一半,年齡大於等於這個閾值的對象就可以進入老年代。

2)垃圾回收

上面提到了Minor GC,什麼是Minor GC,Minor GC是指發生在新生代的垃圾回收動作。而與之對應的Major/Full GC是指發生了老年代的GC。

前面提到了Minor GC的觸發條件,即Eden沒有足夠空間分配內存的時候,那什麼時候會觸發Major GC。

一般而言,當老年代的連續空間大於新生代對象總大小或者歷次升級到老年代的平均大小就會進行Minor GC,否則才會進行Major GC。

這其中就涉及到一個先前提到的概念,分配擔保。因為老年代需要為新生代分配內存做擔保,當老年代無法為新生代的對象分配空間進行擔保時,就可能會觸發Major GC,從而騰出一定空間給新生代的對象升級。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 科技優家 的精彩文章:

Vulkan Tutorial 24 Descriptor pool and sets
Java總結之線程(1)
如何使用Node.js編寫命令工具——以vue-cli為例
限制input「type=number」的輸入位數策略整理
ASP.NET MVC 重寫RazorViewEngine實現多主題切換

TAG:科技優家 |

您可能感興趣

內存管理+ linux內核並發與同步機制
JVM內存分配、GC原理與垃圾收集器
Linux 內存的分配和釋放
外形確定 廉價iPhone X配置曝光 處理器/內存狠縮水
mariadb 內存佔用優化
Oracle 關係型分散式內存資料庫
Chrome瀏覽器中的GPU及內存運行
linux 手工釋放內存 高內存 內存回收 方法思路
HashCache:低內存緩存存儲系統
Linux內存管理
Linux內核內存管理演算法Buddy和Slab
Redis 內存淘汰機制詳解
用 Bash 腳本監控 Linux 上的內存使用情況
Spark 動態內存分析
從PowerShell內存轉儲中提取執行的腳本內容
Linux增加虛擬內存
西數發布新款Ultrastar內存固態盤 進軍內存計算細分市場
Android進程&內存管理及內存泄露簡析
jvm 內存管理-hotspot虛擬機對象創建
新MacBook Air拆解:模塊化設計便於維修,板載內存和存儲是硬傷