1. 程式人生 > >Java 內存分配及垃圾回收機制初探

Java 內存分配及垃圾回收機制初探

com 分配內存 str return ability 占用 基礎 而是 oat

一、運行時內存分配

Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分為若幹個不同的數據區域。 這些區域都有各自的用途,以及創建和銷毀的時間,有的區域隨著虛擬機進程的啟動而存在,有些區域則依賴用戶線程的啟動和結束而建立和銷毀。

技術分享圖片

線程私有區域(生命周期與線程相同)


a) 虛擬機棧

虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame[1])用於存儲局部變量表、 操作數棧、 動態鏈接、 方法出口等信息。 每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。

虛擬機棧中有一個局部變量表,存放了編譯期可知的各種基本數據類型(boolean、 byte、 char、 short、 int、float、 long、 double)、 對象引用(reference類型,它不等同於對象本身,可能是一個指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。

b)本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。

c) 程序計數器

由於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)都只會執行一條線程中的指令。 因此,為了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類內存區域為“線程私有”的內存。

共享數據區


a)堆

對於大多數應用來說,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。 此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。

根據Java虛擬機規範的規定,Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,就像我們的磁盤空間一樣。 在實現時,既可以實現成固定大小的,也可以是可擴展的,不過當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制)。

b)方法區

方法區(Method Area)與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛
擬機加載的類信息、 常量、 靜態變量、 即時編譯器編譯後的代碼等數據。

相對而言,垃圾收集行為在這個區域是比較少出現的,但並非數據進入了方法區就如永久代的名字一樣“永久”存在了。 這區域的內存回收目標主要是針對常量池的回收和對類型的卸載,一般來說,這個區域的回收“成績”比較難以令人滿意,尤其是類型的卸載,條件相當苛刻,但是這部分區域的回收確實是必要的。

Java對象創建過程


a) 虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、 解析和初始化過。 如果沒有,那必須先執行相應的類加載過程。

b) 為對象分配內存(對象所需內存大小在類加載完成後便完全確定),對象所需內存的大小在類加載完成後便可完全確定(如何確定將在2.3.2節中介紹),為對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。

二、垃圾對象判定

1. 引用計數法


每個對象都有一個引用計數的屬性,用來保存該對象被引用的次數。當引用次數為0時,就意味著該對象沒有被引用了,也就不會在使用這個對象了,可以判定為垃圾對象。但是,這種方式有一個很大的Bug,就是無法解決對象間相互引用或者循環引用的問題:當兩個對象相互引用,他們兩個和其他任何對象也沒有引用關系,它倆的引用次數都不為0,因此不會被回收,但實際上這兩個對象已經不再有用了。

2. 可達性分析(根搜索法)


在主流的商用程序語言(Java、 C#,甚至包括前面提到的古老的Lisp)的主流實現中,都是稱通過可達性分析(Reachability Analysis)來判定對象是否存活的。這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。 如圖3-1所示,對象object 5、 object 6、 object 7雖然互相有關聯,但是它們到GC Roots是不可達的,所以它們將會被判定為是可回收的對象。

技術分享圖片

這裏的GC Roots對象包括以下幾種:

虛擬機棧(棧幀中的本地變量表)中引用的對象。
方法區中類靜態屬性引用的對象。
方法區中常量引用的對象。
本地方法棧中JNI(即一般說的Native方法)引用的對象。

註: 這裏涉及Java中到四種引用,不再細說。

三、典型的垃圾收集算法

 在確定了哪些垃圾可以被回收後,垃圾收集器要做的事情就是開始進行垃圾回收,但是這裏面涉及到一個問題是:如何高效地進行垃圾回收。由於Java虛擬機規範並沒有對如何實現垃圾收集器做出明確的規定,因此各個廠商的虛擬機可以采用不同的方式來實現垃圾收集器,所以在此只討論幾種常見的垃圾收集算法的核心思想。

1.Mark-Sweep(標記-清除)算法


這是最基礎的垃圾回收算法,之所以說它是最基礎的是因為它最容易實現,思想也是最簡單的。標記-清除算法分為兩個階段:標記階段和清除階段。標記階段的任務是標記出所有需要被回收的對象,清除階段就是回收被標記的對象所占用的空間。具體過程如下圖所示:

技術分享圖片

  從圖中可以很容易看出標記-清除算法實現起來比較容易,但是有一個比較嚴重的問題就是容易產生內存碎片,碎片太多可能會導致後續過程中需要為大對象分配空間時無法找到足夠的空間而提前觸發新的一次垃圾收集動作。

 2.Copying(復制)算法


  為了解決Mark-Sweep算法的缺陷,Copying算法就被提了出來。它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然後再把已使用的內存空間一次清理掉,這樣一來就不容易出現內存碎片的問題。具體過程如下圖所示:

技術分享圖片

  這種算法雖然實現簡單,運行高效且不容易產生內存碎片,但是卻對內存空間的使用做出了高昂的代價,因為能夠使用的內存縮減到原來的一半。

  很顯然,Copying算法的效率跟存活對象的數目多少有很大的關系,如果存活對象很多,那麽Copying算法的效率將會大大降低。

3.Mark-Compact(標記-整理)算法


  為了解決Copying算法的缺陷,充分利用內存空間,提出了Mark-Compact算法。該算法標記階段和Mark-Sweep一樣,但是在完成標記之後,它不是直接清理可回收對象,而是將存活對象都向一端移動,然後清理掉端邊界以外的內存。具體過程如下圖所示:

技術分享圖片

4.Generational Collection(分代收集)算法


分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根據對象存活的生命周期將內存劃分為若幹個不同的區域。一般情況下將堆區劃分為老年代(Tenured Generation)和新生代(Young Generation),老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麽就可以根據不同代的特點采取最適合的收集算法。

  目前大部分垃圾收集器對於新生代都采取Copying算法,因為新生代中每次垃圾回收都要回收大部分對象,也就是說需要復制的操作次數較少,但是實際中並不是按照1:1的比例來劃分新生代的空間的,一般來說是將新生代劃分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象復制到另一塊Survivor空間中,然後清理掉Eden和剛才使用過的Survivor空間。

  而由於老年代的特點是每次回收都只回收少量對象,一般使用的是Mark-Compact算法。

註意,在堆區之外還有一個代就是永久代(Permanet Generation),它用來存儲class類、常量、方法描述等。對永久代的回收主要回收兩部分內容:廢棄常量和無用的類。

如果從事java開發相關工作,可以買一本《深入理解java虛擬機》看一下

參考:

https://www.cnblogs.com/baizhanshi/p/5817845.html

《深入理解java虛擬機》

Java 內存分配及垃圾回收機制初探