你真的懂 Java 的內(nèi)存管理和引用類型嗎?

對于 Java 程序員來說,在 Java 虛擬機自動內(nèi)存管理機制的幫助下,不再需要為每一個 new 操作去寫對應(yīng)的 delete/free 代碼,不容易出現(xiàn)內(nèi)存泄露和內(nèi)存溢出的問題。不過,也正是因為 Java 程序員把內(nèi)存控制的權(quán)力交給了 Java 虛擬機,一旦出現(xiàn)內(nèi)存泄露和內(nèi)存溢出的問題,如果不了解虛擬機是怎樣使用內(nèi)存的,那么排查錯誤將會非常艱難。

本文將會對 Java 的內(nèi)存管理以及四種引用類型,做一個總結(jié)。

一、Java 內(nèi)存管理


Java 內(nèi)存管理就是對象的分配和釋放問題。在 Java 中,內(nèi)存的分配是由「程序」完成的,而內(nèi)存的釋放是由 Java 垃圾回收器(GC)完成的,這種方式確實簡化了程序員的工作,但也同時加重了 JVM 的工作。這也是 Java 程序運行速度較慢的原因之一。

為了能夠正確釋放對象,GC 必須監(jiān)控每一個對象的運行狀態(tài),包括對象的申請、引用、被引用、賦值等,監(jiān)控對象狀態(tài)是為了更加準(zhǔn)確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用。

1、Java 內(nèi)存分配策略

Java 程序運行時的內(nèi)存分配策略有三種,分別是靜態(tài)分配、棧式分配和堆式分配,三種方式所使用的內(nèi)存空間分別是靜態(tài)存儲區(qū)(方法區(qū))、棧區(qū)和堆區(qū)。

  • 靜態(tài)存儲區(qū)(方法區(qū)):主要存放靜態(tài)變量。這塊「內(nèi)存」在程序編譯時就已經(jīng)分配好了,并且在程序整個運行期間都存在。

  • 棧區(qū):當(dāng)方法被執(zhí)行時,方法體內(nèi)的局部變量(包括基礎(chǔ)數(shù)據(jù)類型、對象的引用)都在棧上創(chuàng)建,并在方法執(zhí)行結(jié)束時。這些局部變量所持有的內(nèi)存將會自動被釋放。因為棧內(nèi)存分配運算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。

  • 堆區(qū):又稱動態(tài)內(nèi)存分配,通常就是指程序運行時直接 new 出來的內(nèi)存,也就是對象的實例,這部分「內(nèi)存」在不使用時將會被 Java 垃圾回收器來負責(zé)回收。

下面通過一個例子,來詳細說明一下:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();
    }
}

Sample mSample3 = new Sample();

Sample 類的局部變量 s2 和引用變量 mSample2 都是存在于棧中,但 mSample2 指向的對象是存在于堆上的。mSample3 指向的對象實體存放于堆上,包括這個對象的所有成員變量 s1 和 mSample1,但它的引用變量是存在于棧中的。

結(jié)論:
  • 局部變量的基本數(shù)據(jù)類型和引用存儲于棧中,引用的對象實體存儲在堆中 —— 因為他們屬于方法中的變量,生命周期隨方法而結(jié)束

  • 成員變量全部存儲于堆中(包括基本數(shù)據(jù)類型,引用和引用的對象實體)—— 因為它們屬于類,類對象終究是要被 new 出來使用的

2、Java 垃圾回收器

在 Java 堆和靜態(tài)存儲區(qū)(方法區(qū))中,一個接口中的多個實現(xiàn)類需要的內(nèi)存可能不一樣,一個方法中的多個分支需要的內(nèi)存也可能不一樣,我們只有在程序處于運行期間時才能知道會創(chuàng)建哪些對象,這部分內(nèi)存的分配和回收都是動態(tài)的,垃圾回收器所關(guān)注的便是這部分的內(nèi)存。

2.1 判斷對象是否存活的方法

在堆里面存放著 Java 世界中幾乎所有的對象實例,垃圾收集器在對堆進行回收前,第一件事就是要確定這些對象之中哪些還「存活」著,哪些已經(jīng)「死去」。

引用計數(shù)算法

給對象添加一個引用計數(shù)器,每當(dāng)有一個地方引用它時,計數(shù)器就加 1,當(dāng)引用失效
時,就減 1。任何時刻計數(shù)器為 0 的對象就是不可能再被使用的。

引用計數(shù)算法的實現(xiàn)比較簡單,判定效率也很高,在大部分情況下它都是一個不錯的算法。但是,至少主流的 Java 虛擬機里面沒有選用引用計數(shù)算法來管理內(nèi)存,其中最主要的原因是它很難解決對象之間相互循環(huán)引用的問題。

可達性分析算法

在主流的商用程序語言(Java、C#)的主流實現(xiàn)中,都是稱通過可達性分析來判定對象是否存活的。這個算法的基本思想就是通過一系列的稱為「GC Roots」的對象作為起始點,從這些節(jié)點開始向下搜索,搜索所走過的路徑稱為引用鏈,當(dāng)一個對象到 GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的。

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

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

2.2 垃圾收集算法

2.2.1 標(biāo)記 — 清除算法

最基礎(chǔ)的收集算法就是「標(biāo)記 — 清除」(Mark - Sweep)算法,如同它的名字一樣,算法分為「標(biāo)記」和「清除」兩個階段:

  • 標(biāo)記出所有需要回收的對象

  • 在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象

之所以說它是最基礎(chǔ)的收集算法,是因為后續(xù)的收集算法都是基于這種思路并對其不足進行改進而得到的。它的主要不足主要有兩個:

  • 效率問題,標(biāo)記和清除兩個過程的效率都不高

  • 空間問題,標(biāo)記清除之后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片

內(nèi)存碎片太多,可能會導(dǎo)致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。

2.2.2 復(fù)制算法

為了解決效率問題,一種稱為「復(fù)制」的收集算法出現(xiàn)了,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。

這樣使得每次都是對整個半?yún)^(qū)進行內(nèi)存回收,內(nèi)存分配時也就不用考慮內(nèi)存碎片等復(fù)雜情況,只要移動堆頂指針,按順序分配內(nèi)存即可,實現(xiàn)簡單,運行高效。只是這種算法的代價是將內(nèi)存縮小為原來的一半。

2.2.3 標(biāo)記 — 整理算法

復(fù)制算法在對象存活率較高時就要進行較多的復(fù)制操作,效率將會變低。更關(guān)鍵的是,如果不想浪費 50 % 的空間,就需要有額外的空間進行擔(dān)保,以應(yīng)對被使用的內(nèi)存中所有對象都 100% 存活的極端情況,所以在老年代一般不能直接選用這種算法。

根據(jù)老年代的特點,提出了另一種「標(biāo)記 — 整理」算法,標(biāo)記過程仍然與「標(biāo)記 — 清理」算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉邊界以外的內(nèi)存。

2.2.4 分代收集算法

當(dāng)前商業(yè)虛擬機的垃圾收集都采用「分代收集」算法,這種算法并沒有什么新的思想,只是根據(jù)對象存活周期的不同將內(nèi)存劃分為幾塊,一般是把 Java 堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當(dāng)?shù)氖占惴ā?/p>

在新生代中,每次垃圾收集都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量存活對象的復(fù)制成本就可以完成收集,而老年代中因為對象存活率高、沒有額外空間對它進行擔(dān)保,就必須采用「標(biāo)記 — 清理」或者「標(biāo)記 — 整理」算法來回收。

二、Java 的引用類型


在 JDK 1.2 以前,Java 中引用的定義很傳統(tǒng):如果 reference 類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱這塊內(nèi)存代表著一個引用。一個對象在這種定義下只有被引用或沒有被引用兩種狀態(tài),對于描述一些「食之無味,棄之可惜」的對象就顯得無能為力了。

我們希望能描述這樣一類對象:當(dāng)內(nèi)存空間還足夠時,則能保留在內(nèi)存之中,如果內(nèi)存空間在進行垃圾回收后還是非常緊張,則可以拋棄這些對象,很多系統(tǒng)的緩存功能都符合這樣的應(yīng)用場景。

在 JDK 1.2 之后,Java 對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4 中,這四種引用強度一次逐漸減弱

  • 強引用:指在程序代碼之中普遍存在的,類似 Object obj = new Object() 這類的引用,只要強引用還存在,垃圾回收器「永遠」不會回收掉被引用的對象

  • 軟引用:用來描述一些還有用但并非必需的對象。對于軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會把這些對象列進回收范圍之中進行第二次回收。如果這次回收還沒有足夠的內(nèi)存,才會拋出內(nèi)存溢出異常

  • 弱引用:用來描述非必須對象的,但是它的強度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只能生存到下一次垃圾收集之前。當(dāng)垃圾收集器工作時,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只被弱引用關(guān)聯(lián)的對象

  • 虛引用:也被稱為幽靈引用或幻影引用,它是最弱的一種引用關(guān)系。一個對象是否有虛引用的存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設(shè)置虛引用關(guān)聯(lián)的 唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知。

最后,用一張圖總結(jié)下它們之間的區(qū)別:

參考資料

《深入理解 Java 虛擬機》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容