Java會對內(nèi)存進行自動分配與回收管理啄寡,使上層業(yè)務更加安全割卖,方便地使用內(nèi)存實現(xiàn)程序邏輯
在不同的JVM實現(xiàn)及不同的回收機制中绷旗,堆內(nèi)存的劃分方式是不一樣的
這里簡要介紹垃圾回收( Garbage Collection, GC)灾部。垃圾回收的主要目的是清除不再使用的對象,自動釋放內(nèi)存.
靜態(tài)內(nèi)存分配和回收
在程序開始運行時由編譯器分配的內(nèi)存
在被編譯時就已經(jīng)能夠確定需要的空間,當程序被加載時系統(tǒng)把內(nèi)存一次性分配給它,這些內(nèi)存不會在程序執(zhí)行時發(fā)生變化,直到程序執(zhí)行結束時才回收內(nèi)存.
包括原生數(shù)據(jù)類型及對象的引用
這些靜態(tài)內(nèi)存空間在棧上分配,方法運行結束,對應的棧幀撤銷,內(nèi)存空間被回收.
每個棧幀中的本地變量表都是在類被加載的時候就確定的,每一個棧幀中分配多少內(nèi)存基本上是在類結構確定時就已知了,因此這幾塊區(qū)域內(nèi)存分配和回收都具備確定性,就不需要過多考慮回收問題了
動態(tài)內(nèi)存分配和回收
- 在程序執(zhí)行時才知道要分配的存儲空間大小,對象何時被回收也是不確定的,只有等到該對象不再使用才會被回收.
堆和方法區(qū)的內(nèi)存回收具有不確定性,因此垃圾收集器在回收堆和方法區(qū)內(nèi)存的時候花了一點心思.
1 Java堆內(nèi)存的回收
1.1 判定回收的對象
GC是如何判斷對象是否可以被回收的呢?為了判斷對象是否存活棒卷,JVM引入了GC Roots
如果一個對象與GC Roots之間沒有直接或間接的引用關系,比如某個失去任何引用的對象祝钢,或者兩個互相環(huán)島狀循環(huán)引用的對象等比规,判決這些對象“死緩”,是可以被回收的
在對堆進行對象回收之前,首先要判斷哪些是無效對象(一個對象不被任何對象或變量引用)需要被回收
一般有兩種判別方式:
- 引用計數(shù)法 (Reference Counting)
每個對象都有一個整型的計數(shù)器,當這個對象被一個變量或對象引用時拦英,該計數(shù)器加一;當該引用失效時,計數(shù)器值減一.當計數(shù)器為0時,就認為該對象是無效對象. - 可達性分析法 (Reachability Analysis)
所有和GC Roots直接或間接關聯(lián)的對象都是有效對象,和GC Roots沒有關聯(lián)的對象就是無效對象.
GC Roots對象
- 虛擬機棧(棧幀中的本地變量表)中引用的對象
- 方法區(qū)中靜態(tài)屬性引用的對象
- 方法區(qū)中常量引用的對象
- 本地方法棧JNI(native方法)引用的對象
GC Roots并不包括堆中對象所引用的對象蜒什!這樣就不會出現(xiàn)循環(huán)引用.
- 兩者對比
引用計數(shù)法雖然簡單,但存在無法解決對象之間相互循環(huán)引用的嚴重問題,且伴隨加減法操作的性能影響.
因此,目前主流語言均使用可達性分析方法來判斷對象是否有效.
2 回收無效對象的過程
當經(jīng)可達性算法篩選出失效的對象之后,并不是立即清除,而是再給對象一次重生的機會
- 判斷是否覆蓋finalize()
- 未覆蓋該或已調用過該方法,直接釋放對象內(nèi)存
- 已覆蓋該方法且還未被執(zhí)行,則將finalize()扔到F-Queue隊列中
- 執(zhí)行F-Queue中的finalize()
虛擬機會以較低的優(yōu)先級執(zhí)行這些finalize(),不會確保所有的finalize()都會執(zhí)行結束
如果finalize()中出現(xiàn)耗時操作,虛擬機就直接停止執(zhí)行,將該對象清除 - 對象重生或死亡
- 如果在執(zhí)行finalize()方法時,將this賦給了某一個引用,則該對象重生
- 如果沒有,那么就會被垃圾收集器清除
注意:強烈不建議使用finalize()進行任何操作!
如果需要釋放資源,請用try-finally或者其他方式都能做得更好.
因為finalize()不確定性大,開銷大,無法保證各個對象的調用順序.
以下代碼示例看到:一個對象的finalize被執(zhí)行,但依然可以存活
/**
* 演示兩點:
* 1.對象可以在被GC時自救
* 2.這種自救機會只有一次,因為一個對象的finalize()最多只能被系統(tǒng)自動調用一次,因此第二次自救失敗
* @author sss
* @since 17-9-17 下午12:02
*
*/
public class FinalizeEscapeGC {
private static FinalizeEscapeGC SAVE_HOOK = null;
private void isAlive() {
System.out.println("yes,I am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize methodd executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
// 對象第一次成功自救
SAVE_HOOK = null;
System.gc();
// 因為finalize方法優(yōu)先級很低,所以暫停0.5s以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no,I am dead :(");
}
// 自救失敗
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no,I am dead :(");
}
}
}
運行結果
finalize methodd executed!
yes,I am still alive :)
no,I am dead :(
3 方法區(qū)的內(nèi)存回收
使用復制算法
實現(xiàn)堆的內(nèi)存回收,堆被分為新生代
和老年代
- 新生代中的對象"朝生夕死",每次垃圾回收都會清除掉大量對象
- 老年代中的對象生命較長,每次垃圾回收只有少量的對象被清除
由于方法區(qū)中存放生命周期較長的類信息、常量疤估、靜態(tài)變量.
因此方法區(qū)就像堆的老年代,每次GC只有少量垃圾被清除.
方法區(qū)中主要清除兩種垃圾
- 廢棄常量
- 無用類
3.1 回收廢棄常量
回收廢棄常量和回收對象類似,只要常量池中的常量不被任何變量或對象引用,那么這些常量就會被清除.
3.2 回收無用類
判定無用類的條件則較為苛刻
- 該類所有實例都已被回收
即Java堆不存在該類的任何實例 - 加載該類的ClassLoader已被回收
- 該類的java.lang.Class對象沒有被任何對象或變量引用,無法通過反射訪問該類的方法
只要一個類被虛擬機加載進方法區(qū),那么在堆中就會有一個代表該類的對象:java.lang.Class.這個對象在類被加載進方法區(qū)的時候創(chuàng)建,在方法區(qū)中該類被刪除時清除.
4 垃圾收集算法
4.1 標記-清除(Mark-Sweep)
最基礎
的收集算法,后續(xù)算法也都是基于此并改進其不足而得.
該算法會從每個GC Roots出發(fā)灾常,依次標記有引用關系的對象,最后將沒有被標記的對象清除
不足
這種算法會帶來大量的空間碎片铃拇,導致需要分配一個較大連續(xù)空間時容易觸發(fā)FullGC,降低了空間利用率.
為了解決這個問題钞瀑,又提出了“標記-整理算法”,該算法類似計算機的磁盤整理慷荔,首先會從GC Roots出發(fā)標記存活的對象雕什,然后將存活對象整理到內(nèi)存空間的一端,形成連續(xù)的已使用空間显晶,最后把已使用空間之外的部分全部清理掉贷岸,這樣就不會產(chǎn)生空間碎片的問題
4.2 復制算法(Mark-Copy)
為了能夠并行地標記和整理將空間分為兩塊,每次只激活其中一塊,垃圾回收時只需把存活的對象復制到另一塊未激活空間上,將未激活空間標記為已激活,將已激活空間標記為未激活,然后清除原空間中的原對象
將內(nèi)存分成大小相等兩份,只將數(shù)據(jù)存儲在其中一塊上
- 當需要回收時,首先標記廢棄數(shù)據(jù)
- 然后將有用數(shù)據(jù)復制到另一塊內(nèi)存
-
最后將第一塊內(nèi)存空間全部清除
4.2.1 分析
- 這種算法避免了空間碎片,但內(nèi)存縮小了一半.
- 每次都需將有用數(shù)據(jù)全部復制到另一片內(nèi)存,效率不高
4.2.2 解決空間利用率問題
堆內(nèi)存空間分為較大的Eden和兩塊較小的Survivor,每次只使用Eden和Survivor區(qū)的一塊。這種情形下的“ Mark-Copy"減少了內(nèi)存空間的浪費吧碾』丝“Mark-Copy”現(xiàn)作為主流的YGC算法進行新生代的垃圾回收。
在新生代中,由于大量對象都是"朝生夕死",也就是一次垃圾收集后只有少量對象存活
因此我們可以將內(nèi)存劃分成三塊
Eden倦春、Survior1户敬、Survior2
- 內(nèi)存大小分別是8:1:1
分配內(nèi)存時,只使用Eden和一塊Survior1.
- 當發(fā)現(xiàn)Eden+Survior1的內(nèi)存即將滿時,JVM會發(fā)起一次
Minor GC
,清除掉廢棄的對象, - 并將所有存活下來的對象復制到另一塊Survior2中.
- 接下來就使用Survior2+Eden進行內(nèi)存分配
通過這種方式,只需要浪費10%的內(nèi)存空間即可實現(xiàn)帶有壓縮功能的垃圾收集方法,避免了內(nèi)存碎片的問題.
4.2.3 分配擔保
準備為一個對象分配內(nèi)存時,發(fā)現(xiàn)此時Eden+Survior中空閑的區(qū)域無法裝下該對象
就會觸發(fā)MinorGC
(新生代 GC 算法),對該區(qū)域的廢棄對象進行回收.
但如果MinorGC過后只有少量對象被回收,仍然無法裝下新對象
- 那么此時需要將Eden+Survior中的
所有對象
都轉移到老年代
中,然后再將新對象存入Eden區(qū).這個過程就是"分配擔保".
在發(fā)生 minor gc 前,虛擬機會檢測老年代最大可用連續(xù)空間是否大于新生代所有對象總空間
若成立睁本,minor gc 可確保安全
若不成立尿庐,JVM會查看 HandlePromotionFailure
是否允許擔保失敗
- 若允許
那么會繼續(xù)檢測老年代最大可用的連續(xù)空間是否 > 歷次晉升到老年代對象的平均大小- 若大于
則將嘗試進行一次 minor gc,盡管這次 minor gc 是有風險的 - 若小于或 HandlePromotionFailure 設置不允許冒險
改為進行一次 full gc (老年代GC)
- 若大于
4.3 標記-壓縮算法(Mark-Compact)
在回收前,標記過程仍與"標記-清除"一樣
但后續(xù)不是直接清理可回收對象,而是
- 將所有存活對象移到一端
-
直接清掉端邊界之外內(nèi)存
分析
這是一種老年代垃圾收集算法.
老年代中對象一般壽命較長,每次垃圾回收會有大量對象存活
因此如果選用"復制"算法,每次需要較多的復制操作,效率低
而且,在新生代中使用"復制"算法
當 Eden+Survior 都裝不下某個對象時,可使用老年代內(nèi)存進行"分配擔保"
而如果在老年代使用該算法,那么在老年代中如果出現(xiàn) Eden+Survior 裝不下某個對象時,沒有其他區(qū)域給他作分配擔保
因此,老年代中一般使用"標記-壓縮"算法
4.4 分代收集算法(Generational Collection)
當前商業(yè)虛擬機都采用此算法.
根據(jù)對象存活周期的不同將Java堆劃分為老年代和新生代,根據(jù)各個年代的特點使用最佳的收集算法.
- 老年代中對象存活率高,無額外空間對其分配擔保,必須使用"標記-清除"或"標記-壓縮"算法
- 新生代中存放"朝生夕死"的對象,用復制算法,只需要付出少量存活對象的復制成本,就可完成收集
5 Java中引用的種類
Java中根據(jù)生命周期的長短,將引用分為4類
- 強引用
我們平時所使用的引用就是強引用
類似A a = new A();
即通過關鍵字new創(chuàng)建的對象所關聯(lián)的引用就是強引用
只要強引用還存在,該對象永遠不會被回收 - 軟引用
一些還有用但并非必需的對象
只有當堆即將發(fā)生OOM異常時,JVM才會回收軟引用所指向的對象.
軟引用通過SoftReference類實現(xiàn)
軟引用的生命周期比強引用短一些 - 弱引用
也是描述非必需對象,比軟引用更弱
所關聯(lián)的對象只能存活到下一次GC發(fā)生前.
只要垃圾收集器工作,無論內(nèi)存是否足夠,弱引用所關聯(lián)的對象都會被回收.
弱引用通過WeakReference類實現(xiàn). - 虛引用
也叫幽靈(幻影)引用,最弱的引用關系.
它和沒有引用沒有區(qū)別,無法通過虛引用取得對象實例.
設置虛引用唯一的作用就是在該對象被回收之前收到一條系統(tǒng)通知.
虛引用通過PhantomReference類來實現(xiàn).