對於 Java 程序員來講,在 Java 虛擬機自動內存管理機制的幫助下,再也不須要爲每個 new 操做去寫對應的 delete/free 代碼,不容易出現內存泄露和內存溢出的問題。不過,也正是由於 Java 程序員把內存控制的權力交給了 Java 虛擬機,一旦出現內存泄露和內存溢出的問題,若是不瞭解虛擬機是怎樣使用內存的,那麼排查錯誤將會很是艱難。程序員
本文將會對 Java 的內存管理以及四種引用類型,作一個總結。算法
Java 內存管理就是對象的分配和釋放問題。在 Java 中,內存的分配是由「程序」完成的,而內存的釋放是由 Java 垃圾回收器(GC)完成的,這種方式確實簡化了程序員的工做,但也同時加劇了 JVM 的工做。這也是 Java 程序運行速度較慢的緣由之一。緩存
爲了可以正確釋放對象,GC 必須監控每個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,監控對象狀態是爲了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象再也不被引用。bash
Java 程序運行時的內存分配策略有三種,分別是靜態分配、棧式分配和堆式分配,三種方式所使用的內存空間分別是靜態存儲區(方法區)、棧區和堆區。spa
靜態存儲區(方法區):主要存放靜態變量。這塊「內存」在程序編譯時就已經分配好了,而且在程序整個運行期間都存在。指針
棧區:當方法被執行時,方法體內的局部變量(包括基礎數據類型、對象的引用)都在棧上建立,並在方法執行結束時。這些局部變量所持有的內存將會自動被釋放。由於棧內存分配運算內置於處理器的指令集中,效率很高,可是分配的內存容量有限。code
堆區:又稱動態內存分配,一般就是指程序運行時直接 new 出來的內存,也就是對象的實例,這部分「內存」在不使用時將會被 Java 垃圾回收器來負責回收。cdn
下面經過一個例子,來詳細說明一下:對象
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,但它的引用變量是存在於棧中的。blog
局部變量的基本數據類型和引用存儲於棧中,引用的對象實體存儲在堆中 —— 由於他們屬於方法中的變量,生命週期隨方法而結束
成員變量所有存儲於堆中(包括基本數據類型,引用和引用的對象實體)—— 由於它們屬於類,類對象終究是要被 new 出來使用的
在 Java 堆和靜態存儲區(方法區)中,一個接口中的多個實現類須要的內存可能不同,一個方法中的多個分支須要的內存也可能不同,咱們只有在程序處於運行期間時才能知道會建立哪些對象,這部份內存的分配和回收都是動態的,垃圾回收器所關注的即是這部分的內存。
在堆裏面存放着 Java 世界中幾乎全部的對象實例,垃圾收集器在對堆進行回收前,第一件事就是要肯定這些對象之中哪些還「存活」着,哪些已經「死去」。
給對象添加一個引用計數器,每當有一個地方引用它時,計數器就加 1,當引用失效 時,就減 1。任什麼時候刻計數器爲 0 的對象就是不可能再被使用的。
引用計數算法的實現比較簡單,斷定效率也很高,在大部分狀況下它都是一個不錯的算法。可是,至少主流的 Java 虛擬機裏面沒有選用引用計數算法來管理內存,其中最主要的緣由是它很難解決對象之間相互循環引用的問題。
在主流的商用程序語言(Java、C#)的主流實現中,都是稱經過可達性分析來斷定對象是否存活的。這個算法的基本思想就是經過一系列的稱爲「GC Roots」的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連時,則證實此對象是不可用的。
在 Java 語言中,可做爲 GC Roots 的對象包括下面幾種:
最基礎的收集算法就是「標記 — 清除」(Mark - Sweep)算法,如同它的名字同樣,算法分爲「標記」和「清除」兩個階段:
標記出全部須要回收的對象
在標記完成後統一回收全部被標記的對象
之因此說它是最基礎的收集算法,是由於後續的收集算法都是基於這種思路並對其不足進行改進而獲得的。它的主要不足主要有兩個:
效率問題,標記和清除兩個過程的效率都不高
空間問題,標記清除以後會產生大量不連續的內存碎片
內存碎片太多,可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。
爲了解決效率問題,一種稱爲「複製」的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。
這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半。
複製算法在對象存活率較高時就要進行較多的複製操做,效率將會變低。更關鍵的是,若是不想浪費 50 % 的空間,就須要有額外的空間進行擔保,以應對被使用的內存中全部對象都 100% 存活的極端狀況,因此在老年代通常不能直接選用這種算法。
根據老年代的特色,提出了另外一種「標記 — 整理」算法,標記過程仍然與「標記 — 清理」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉邊界之外的內存。
當前商業虛擬機的垃圾收集都採用「分代收集」算法,這種算法並無什麼新的思想,只是根據對象存活週期的不一樣將內存劃分爲幾塊,通常是把 Java 堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。
在新生代中,每次垃圾收集都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集,而老年代中由於對象存活率高、沒有額外空間對它進行擔保,就必須採用「標記 — 清理」或者「標記 — 整理」算法來回收。
在 JDK 1.2 之前,Java 中引用的定義很傳統:若是 reference 類型的數據中存儲的數值表明的是另一塊內存的起始地址,就稱這塊內存表明着一個引用。一個對象在這種定義下只有被引用或沒有被引用兩種狀態,對於描述一些「食之無味,棄之惋惜」的對象就顯得無能爲力了。
咱們但願能描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中,若是內存空間在進行垃圾回收後仍是很是緊張,則能夠拋棄這些對象,不少系統的緩存功能都符合這樣的應用場景。
在 JDK 1.2 以後,Java 對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4 中,這四種引用強度一次逐漸減弱
強引用:指在程序代碼之中廣泛存在的,相似 Object obj = new Object() 這類的引用,只要強引用還存在,垃圾回收器「永遠」不會回收掉被引用的對象
軟引用:用來描述一些還有用但並不是必需的對象。對於軟引用關聯着的對象,在系統將要發生內存溢出異常以前,將會把這些對象列進回收範圍之中進行第二次回收。若是此次回收尚未足夠的內存,纔會拋出內存溢出異常
弱引用:用來描述非必須對象的,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集以前。當垃圾收集器工做時,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象
虛引用:也被稱爲幽靈引用或幻影引用,它是最弱的一種引用關係。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的 惟一目的就是能在這個對象被收集器回收時收到一個系統通知。
《深刻理解 Java 虛擬機》