目錄
- 什么是內(nèi)存签财?
- 什么是 Java 內(nèi)存模型?
- 什么是 JVM淹朋?
- JVM 是怎么劃分內(nèi)存的笙各?
- 棧幀中的數(shù)據(jù)有什么用?
- 什么是可達(dá)性算法础芍?
- Java 中有哪幾種引用杈抢?
- 什么是垃圾回收器?
- 參考文獻(xiàn)
前言
這篇文章是我自己回顧和再學(xué)習(xí) Java 內(nèi)存管理相關(guān)知識(shí)的過程中整理出來的仑性。
整理的目的是讓我自己能對(duì) Java 內(nèi)存管理相關(guān)的知識(shí)的認(rèn)識(shí)更全面一些惶楼,分享的目的是希望大家也能從這些知識(shí)中得到一些啟發(fā)。
1. 什么是內(nèi)存诊杆?
內(nèi)存是計(jì)算機(jī)中重要的部件之一歼捐,是與 CPU 進(jìn)行溝通的橋梁,是 CPU 能直接尋址的存儲(chǔ)空間晨汹,由半導(dǎo)體器件制成豹储。
如果說數(shù)據(jù)是商品,那硬盤就是商店的倉(cāng)庫(kù)淘这,內(nèi)存就是商店的貨架剥扣,倉(cāng)庫(kù)里的商品你是不能直接買的,你只能買貨架上的商品慨灭。
每一個(gè)程序中使用的內(nèi)存區(qū)域相當(dāng)于是不同的貨架朦乏,當(dāng)一個(gè)貨架上需要擺放的商品超過這個(gè)貨架所能容納的最大值,就會(huì)出現(xiàn)放不下的情況氧骤,也就是內(nèi)存溢出呻疹。
2. 什么是 JVM?
JVM(Java 虛擬機(jī))是 Java Virtual Machine 的縮寫筹陵,它是一個(gè)虛構(gòu)出來的計(jì)算機(jī)刽锤,通過在實(shí)際的計(jì)算機(jī)上仿真模擬各種計(jì)算機(jī)功能來實(shí)現(xiàn)的。
JVM 有自己的硬件架構(gòu)朦佩,如處理器并思、堆棧、寄存器等语稠,還有對(duì)應(yīng)分指令系統(tǒng)宋彼。
假如一個(gè)程序使用的內(nèi)存區(qū)域是一個(gè)貨架弄砍,那 JVM 就相當(dāng)于是一個(gè)淘寶店鋪,它不是真實(shí)存在的貨架输涕,但它和真實(shí)貨架一樣可以上架和下架商品音婶,而且上架的商品數(shù)量也是有限的。
假如貨架是在深圳莱坎,那 JVM 的平臺(tái)無(wú)關(guān)性就相當(dāng)于是客人可以在各個(gè)地方購(gòu)買你在淘寶上發(fā)布的商品衣式,不是只有在深圳才能購(gòu)買貨架上的商品。
3. 什么是 Java 內(nèi)存模型檐什?
Java 內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問規(guī)則碴卧,也就是在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存,以及從內(nèi)存中取出變量這樣的底層細(xì)節(jié)乃正。
下面我們就來看下 Java 內(nèi)存模型的具體介紹住册。
3.1 主內(nèi)存與工作內(nèi)存
Java 內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存(Main Memory)中,每條線程有自己的工作內(nèi)存(Working Memory)瓮具,線程的工作內(nèi)存中保存了線程使用到的變量的內(nèi)存副本界弧。
線程對(duì)變量副本的所有操作都必須在工作內(nèi)存中進(jìn)行,不能直接讀寫主內(nèi)存中的變量搭综。
不同線程之間無(wú)法直接訪問其他線程工作內(nèi)存中的變量垢箕,線程間變量值的傳遞都要通過主內(nèi)存來完成。
3.2 執(zhí)行引擎
所謂執(zhí)行引擎兑巾,就是一個(gè)運(yùn)算器条获,能夠識(shí)別輸入的指令,并根據(jù)輸入的指令執(zhí)行一套特定的邏輯蒋歌,最終輸出特定的結(jié)果
執(zhí)行引擎對(duì)于 JVM 的作用就像是 CPU 對(duì)于實(shí)體機(jī)器的作用帅掘,都可以識(shí)別指令,并且根據(jù)指令完成特定的運(yùn)算堂油。
3.3 主內(nèi)存與工作內(nèi)存的交互操作
Java 內(nèi)存模型中定義了 8 種操作來完成主內(nèi)存與工作內(nèi)存之間具體的交互協(xié)議修档,虛擬機(jī)實(shí)現(xiàn)時(shí)必須保證每一種操作都是原子、不可再分的府框。
這 8 種操作又可分為作用于主內(nèi)存的和作用于工作內(nèi)存的操作吱窝。
3.3.1 作用于主內(nèi)存的操作
-
lock(鎖定)
作用于主內(nèi)存的變量,它把一個(gè)變量標(biāo)識(shí)為一條線程獨(dú)占的狀態(tài)迫靖。
-
unlock(解鎖)
作用于主內(nèi)存的變量院峡,它把一個(gè)處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才能被其他線程鎖定系宜。
read(讀日占ぁ)
作用于主內(nèi)存的變量,它把一個(gè)變量的值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中盹牧,以便 load 時(shí)使用俩垃。-
write(寫入)
作用于主內(nèi)存的變量励幼,它把 store 操作從工作內(nèi)存中得到的變量值放入主內(nèi)存的變量中。
3.3.2 作用于工作內(nèi)存的操作
-
load(載入)
作用于工作內(nèi)存的變量口柳,它把 read 操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中赏淌。
use(使用 )
作用于工作內(nèi)存的變量,它把一個(gè)工作內(nèi)存中一個(gè)變量的值傳遞給執(zhí)行引擎啄清,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用的變量的值的字節(jié)碼執(zhí)行時(shí)會(huì)執(zhí)行這個(gè)操作。assign(賦值)
作用于工作內(nèi)存的變量俺孙,它把一個(gè)執(zhí)行引擎接收到的值賦給工作內(nèi)存的變量辣卒,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼執(zhí)行時(shí)執(zhí)行這個(gè)操作。store(存儲(chǔ))
作用于工作內(nèi)存的變量睛榄,它把工作內(nèi)存中的一個(gè)變量的值傳送到主內(nèi)存中荣茫,以便隨后的 write 操作使用。
4. JVM 是怎么劃分內(nèi)存的场靴?
JVM 在執(zhí)行 Java 程序的過程中會(huì)把它管理的內(nèi)存分為若干個(gè)數(shù)據(jù)區(qū)域啡莉,而這些區(qū)域又可以分為線程私有的數(shù)據(jù)區(qū)域和線程共享的數(shù)據(jù)區(qū)域。
4.1 線程私有的數(shù)據(jù)區(qū)域
4.1.1 程序計(jì)數(shù)器
程序計(jì)數(shù)器有下面兩個(gè)特點(diǎn)旨剥。
-
較小
程序計(jì)數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間咧欣,它可以看作是當(dāng)前線程執(zhí)行的字節(jié)碼的行號(hào)指示器。
-
線程私有
為了線程切換后能恢復(fù)到正確的執(zhí)行位置轨帜,每條線程都有一個(gè)私有的程序計(jì)數(shù)器魄咕。
-
無(wú)異常
程序計(jì)數(shù)器是唯一一個(gè)在 Java 虛擬機(jī)規(guī)范中沒有規(guī)定任何 OOM 情況的區(qū)域。
4.1.2 虛擬機(jī)棧
虛擬機(jī)棸龈福可以說是 Java 方法棧哮兰,它有下面三個(gè)特點(diǎn)。
-
描述方法執(zhí)行
虛擬機(jī)棧描述的是 Java 方法執(zhí)行的內(nèi)存模型苟弛,每個(gè)方法在執(zhí)行時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)喝滞,棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧膏秫、動(dòng)態(tài)鏈接右遭、方法出口等信息。
一個(gè)方法從調(diào)用到執(zhí)行完成的過程缤削,對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過程狸演。
關(guān)于棧幀在第 5 大節(jié)會(huì)有一個(gè)更多的介紹。
-
線程私有
與程序計(jì)數(shù)器一樣僻他,Java 虛擬機(jī)棧也是線程私有的宵距,它的生命周期與線程相同。
-
異常
在 Java 虛擬機(jī)規(guī)范中吨拗,對(duì)虛擬機(jī)棧規(guī)定了下面兩種異常满哪。
-
StackOverflowError
當(dāng)執(zhí)行 Java 方法時(shí)會(huì)進(jìn)行壓棧的操作婿斥,在棧中會(huì)保存局部變量、操作數(shù)棧和方法出口等信息哨鸭。
JVM 規(guī)定了棧的最大深度民宿,如果線程請(qǐng)求執(zhí)行方法時(shí)棧的深度大于規(guī)定的深度,就會(huì)拋出棧溢出異常 StackOverflowError像鸡。
-
OutOfMemoryError
如果虛擬機(jī)在擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存活鹰,就會(huì)拋出內(nèi)存溢出異常 OutOfMemoryError。
-
4.1.3 本地方法棧
本地方法棧(Native Method Stack)的作用與虛擬機(jī)棧非常相似只估,它有下面兩個(gè)特點(diǎn)志群。
-
為 Native 方法服務(wù)
本地方法棧與虛擬機(jī)棧的區(qū)別是虛擬機(jī)棧為 Java 方法服務(wù),而本地方法棧為 Native 方法服務(wù)蛔钙。
-
異常
與虛擬機(jī)棧一樣锌云,本地方法棧也會(huì)拋出 StackOverflowError 和 OutOfMemoryError 異常。
4.2 所有線程共享的數(shù)據(jù)區(qū)域
4.2.1 Java 堆
Java 堆(Java Heap)也就是實(shí)例堆吁脱,它用于存放我們創(chuàng)建的對(duì)象實(shí)例桑涎,它有下面幾個(gè)特點(diǎn)。
-
最大
對(duì)于大多數(shù)應(yīng)用來說兼贡,Java 堆是 JVM 管理的內(nèi)存中最大的一塊內(nèi)存區(qū)域攻冷。
-
線程共享
Java 堆是所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建遍希。
-
存放實(shí)例
堆的唯一作用就是存放對(duì)象實(shí)例讲衫,幾乎所有的對(duì)象實(shí)例都是在這里分配內(nèi)存。
-
GC
堆是垃圾收集器管理的主要區(qū)域孵班,所以有時(shí)也叫 GC 堆涉兽。
4.2.2 方法區(qū)
方法區(qū)(Method Area)存儲(chǔ)的是已被虛擬機(jī)加載的數(shù)據(jù),它有下面幾個(gè)特點(diǎn)篙程。
-
線程共享
方法區(qū)和堆一樣枷畏,是所有線程共享的內(nèi)存區(qū)域。
-
存儲(chǔ)的數(shù)據(jù)類型
- 類信息
- 常量
- 靜態(tài)變量
- 即時(shí)編譯器編譯后的代碼
-
異常
方法區(qū)的大小決定了系統(tǒng)可以保存多少個(gè)類虱饿,如果系統(tǒng)定義了太多的類拥诡,導(dǎo)致方法區(qū)溢出,虛擬機(jī)同樣會(huì)拋出內(nèi)存溢出異常 OutOfMemoryError氮发。
方法區(qū)又可分為運(yùn)行時(shí)常量池和直接內(nèi)存兩部分渴肉。
-
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Constant Pool)是方法區(qū)的一部分。
Class 文件中除了有類的版本爽冕、字段仇祭、方法和接口等描述信息,還有一項(xiàng)信息就是常量池(Constant Pool Table)颈畸。
常量池用于存放編譯期生成的各種字面量和符號(hào)引用乌奇,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放没讲。
運(yùn)行時(shí)常量池受到方法區(qū)內(nèi)存的限制,當(dāng)常量池?zé)o法再申請(qǐng)到內(nèi)存時(shí)會(huì)拋出 OutOfMemoryError 異常礁苗。
-
直接內(nèi)存
直接內(nèi)存(Direct Memory)有下面幾個(gè)特點(diǎn)爬凑。
-
在虛擬機(jī)數(shù)據(jù)區(qū)外
直接內(nèi)存不是從虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域试伙。
-
直接分配
在 JDK 1.4 中新加入了 NIO(New Input/Output)類嘁信,引入了一種基于通道與緩沖區(qū)的 I/O 方式,它可以使用 Native 函數(shù)庫(kù)直接分配堆外內(nèi)存疏叨,然后通過一個(gè)存儲(chǔ)在 Java 堆中的 DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作潘靖,這樣能避免在 Java 堆和 Native 堆中來回復(fù)制數(shù)據(jù)。
-
受設(shè)備內(nèi)存大小限制
直接內(nèi)存的分配不會(huì)受到 Java 堆大小的限制考廉,但是會(huì)受到設(shè)備總內(nèi)存(RAM 以及 SWAP 區(qū))大小以及處理器尋址空間的限制。
-
異常
直接內(nèi)存的容量默認(rèn)與 Java 堆的最大值一樣携御,如果超額申請(qǐng)內(nèi)存昌粤,也有可能導(dǎo)致 OOM 異常出現(xiàn)。
-
5. 棧幀中的數(shù)據(jù)有什么用啄刹?
當(dāng) Java 程序出現(xiàn)異常時(shí)涮坐,程序會(huì)打印出對(duì)應(yīng)的異常堆棧,通過這個(gè)堆棧我們可以知道方法的調(diào)用鏈路誓军,而這個(gè)調(diào)用鏈路就是由一個(gè)個(gè) Java 方法棧幀組成的袱讹。
我們來看下棧幀中包含的局部變量表、操作數(shù)棧昵时、動(dòng)態(tài)連接和返回地址分別有著什么作用捷雕。
5.1 局部變量表
局部變量表(Local Variable Table)中的變量只在當(dāng)前函數(shù)調(diào)用中有效,當(dāng)函數(shù)調(diào)用結(jié)束后壹甥,隨著函數(shù)棧幀的銷毀救巷,局部變量表也會(huì)隨之銷毀。
局部變量表中存放的編譯期可知的各種數(shù)據(jù)有如下三種句柠。
-
基本數(shù)據(jù)類型
如 boolean浦译、char、int 等
-
對(duì)象引用
reference 類型溯职,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔樉眩部赡苁侵赶蛞粋€(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置
-
returnAddress 類型
指向了一條字節(jié)碼指令的地址。
5.2 操作數(shù)棧
操作數(shù)棧(Operand Stack)也叫操作棧谜酒,它主要用于保存計(jì)算過程的中間結(jié)果叹俏,同時(shí)作為計(jì)算過程中臨時(shí)變量的存儲(chǔ)空間。
操作數(shù)棧也是一個(gè)先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu)僻族,只支持入棧和出棧兩種操作她肯。
當(dāng)一個(gè)方法剛開始執(zhí)行時(shí)佳头,操作數(shù)棧是空的,在方法執(zhí)行的過程中晴氨,會(huì)有各種字節(jié)碼執(zhí)行往操作數(shù)棧中寫入和提取內(nèi)容康嘉,也就是出棧/入棧操作。
比如下面的這張圖中籽前,當(dāng)調(diào)用了虛擬機(jī)的 iadd 指令后亭珍,它就會(huì)在操作數(shù)棧中彈出兩個(gè)整數(shù)并進(jìn)行加法計(jì)算,并將計(jì)算結(jié)果入棧枝哄。
5.3 動(dòng)態(tài)連接
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用肄梨,持有這個(gè)引用是為了支持方法調(diào)用過程中的動(dòng)態(tài)連接(Dynamic Linking)。
5.4 方法返回地址
當(dāng)一個(gè)方法開始執(zhí)行后挠锥,只有兩種方式可以退出這個(gè)方法众羡,一種是正常完成出口,另一種是異常完成出口蓖租。
-
正常完成出口
執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令粱侣,這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者。
是否有返回值和返回值的類型將根據(jù)遇到哪種方法返回指令來決定蓖宦,這種退出方法的方式稱為正常完成出口(Normal Method Invocation Completion)齐婴。
-
異常完成出口
在方法執(zhí)行過程中遇到異常,并且這個(gè)異常沒有在方法體內(nèi)得到處理稠茂,就會(huì)導(dǎo)致方法退出柠偶,這種退出方式稱為異常完成出口(Abrupt Method Invocation Completion)。
一個(gè)方法使用異常完成出口的方式退出睬关,任何值都不會(huì)返回給它的調(diào)用者诱担。
無(wú)論采用哪種退出方式,在方法退出后电爹,都需要返回到方法被調(diào)用的位置该肴,程序才能繼續(xù)執(zhí)行。
6. 什么是可達(dá)性算法藐不?
在主流的商用程序語(yǔ)言(Java匀哄、C# 和 Lisp 等)的主流實(shí)現(xiàn)中,都是通過可達(dá)性分析(Reachability Analysis)判定對(duì)象是否存活的雏蛮。
這個(gè)算法的基本思路就是通過一系列“GC Roots”對(duì)象作為起始點(diǎn)涎嚼,從這些節(jié)點(diǎn)開始向下搜索,搜索走過的路徑就叫引用鏈挑秉。
當(dāng)一個(gè)對(duì)象到 GC Roots 沒有任何引用鏈相連時(shí)法梯,則證明此對(duì)象是不可用的。
比如下圖中的 object5、object6立哑、object7夜惭,雖然它們互有關(guān)聯(lián),但是它們到 GC Roots 是不可達(dá)的铛绰,所以它們會(huì)被判定為可回收對(duì)象诈茧。
在 Java 中,不同內(nèi)存區(qū)域中可作為 GC Roots 的對(duì)象包括下面幾種捂掰。
-
虛擬機(jī)棧
虛擬機(jī)棧的棧幀中的局部變量表中引用的對(duì)象敢会,比如某個(gè)方法正在使用的類字段。
-
方法區(qū)
- 類靜態(tài)屬性引用的對(duì)象
- 常量引用的對(duì)象
-
本地方法棧
本地方法棧中 Native 方法引用的對(duì)象这嚣。
7. Java 中有哪幾種引用鸥昏?
無(wú)論是通過引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量,還是通過可達(dá)性分析算法判斷對(duì)象的引用鏈?zhǔn)欠窨蛇_(dá)姐帚,判定對(duì)象是否存活都與引用有關(guān)吏垮。
在 JDK 1.2 之后,Java 對(duì)引用的概念進(jìn)行了擴(kuò)充罐旗,將引用分為強(qiáng)引用膳汪、軟引用、弱引用和虛引用四種尤莺,這四種引用強(qiáng)度按順序依次減弱旅敷。
7.1 強(qiáng)引用
強(qiáng)引用有下面幾個(gè)特點(diǎn)生棍。
-
普遍存在
強(qiáng)引用是指代碼中普遍存在的颤霎,比如 "Object obj = new Object()" 這類引用。
-
直接訪問
強(qiáng)引用可以直接訪問目標(biāo)對(duì)象涂滴。
-
不會(huì)回收
強(qiáng)引用指向的對(duì)象在任何時(shí)候都不會(huì)被系統(tǒng)回收友酱,虛擬機(jī)即使拋出 OOM 異常,也不會(huì)回收強(qiáng)引用指向的對(duì)象柔纵。
使用 obj = null 不會(huì)觸發(fā) GC缔杉,但是在下次 GC 的時(shí)候這個(gè)強(qiáng)引用對(duì)象就可以被回收了。
-
OOM 隱患
強(qiáng)引用可能導(dǎo)致內(nèi)存泄漏搁料。
7.2 軟引用
軟引用有下面幾個(gè)特點(diǎn)或详。
-
有用但非必需
軟引用用于描述一些還有用但非必需的對(duì)象。
-
二次回收
對(duì)于軟引用關(guān)聯(lián)的對(duì)象郭计,在系統(tǒng)即將發(fā)生內(nèi)存溢出前霸琴,會(huì)把這些對(duì)象列入回收范圍中進(jìn)行二次回收。
-
OOM 隱患
如果二次回收后還沒有足夠的內(nèi)存昭伸,就會(huì)拋出內(nèi)存溢出異常梧乘。
-
SoftReference
在 JDK 1.2 后,Java 提供了 SoftReference 類來實(shí)現(xiàn)軟引用。
7.3 弱引用
弱引用有下面幾個(gè)特點(diǎn)选调。
-
比軟引用弱
弱引用的強(qiáng)度比軟引用更弱一些夹供,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次 GC 前。
-
發(fā)現(xiàn)即回收
在 GC 時(shí)仁堪,只要發(fā)現(xiàn)弱引用哮洽,不管系統(tǒng)堆空間使用情況如何,都會(huì)將對(duì)象進(jìn)行回收枝笨。
-
可有可無(wú)
軟引用袁铐、弱引用適合保存可有可無(wú)的緩存數(shù)據(jù)。
-
WeakReference
JDK 1.2 后横浑,提供了 WeakReference 類來實(shí)現(xiàn)弱引用剔桨。
7.4 虛引用
虛引用是最弱的一種引用關(guān)系,它有以下幾個(gè)特點(diǎn)徙融。
-
無(wú)法獲取
一個(gè)對(duì)象是否有虛引用的存在洒缀,都不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過虛引用取得一個(gè)對(duì)象實(shí)例欺冀。
-
收到通知
為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知树绩。
-
PhatomReference
在 JDK 1.2 后,提供了 PhantomReference 類來實(shí)現(xiàn)虛引用隐轩。
8. 什么是垃圾回收器饺饭?
地上有臟東西是不可避免的,但是天天都要掃地又太麻煩了职车,有沒有什么辦法可以讓我們不用掃地呢瘫俊?
掃地機(jī)器人就可以幫我們做這件事,而垃圾回收器 GC(Garbage Collector)就相當(dāng)于是掃地機(jī)器人悴灵。
我們 Java 開發(fā)者不用像 C++ 開發(fā)者那樣關(guān)心內(nèi)存釋放的問題扛芽,但是我們也不能擋著掃地機(jī)器人的路。
當(dāng)我們操作不當(dāng)導(dǎo)致某塊內(nèi)存泄漏時(shí)积瞒,GC 就不能對(duì)這塊內(nèi)存進(jìn)行回收川尖。
GC 可不是個(gè)好伺候的主,如果你讓“GC 很忙”茫孔,那它就會(huì)讓你“應(yīng)用很卡”叮喳。
拿 Android 來說,進(jìn)行 GC 時(shí)缰贝,所有線程都要暫停馍悟,包括主線程,16ms 是 Android 要求的每幀繪制時(shí)間揩瞪,而當(dāng) GC 的時(shí)間超過 16ms赋朦,就會(huì)造成丟幀的情況,也就是界面卡頓。
垃圾回收器回收資源的方式就是垃圾回收算法宠哄,下面我們來看下四個(gè)主要的垃圾回收算法壹将。
8.1 標(biāo)記-清除算法
標(biāo)記-清除算法(Mark-Sweep)相當(dāng)于是先把貨架上有人買的、沒人買的毛嫉、空著的商品和位置都記錄下來诽俯,然后再把沒人買的商品統(tǒng)一進(jìn)行下架。
-
工作原理
第一步:標(biāo)記所有需要回收的對(duì)象
第二步:標(biāo)記完成后承粤,統(tǒng)一回收所有被標(biāo)記的對(duì)象
-
缺點(diǎn)
-
效率低
標(biāo)記和清除的效率都不高
-
內(nèi)存碎片
標(biāo)記清除后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片暴区,內(nèi)存碎片太多會(huì)導(dǎo)致當(dāng)程序需要分配較大的對(duì)象時(shí),無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā) GC
-
8.2 復(fù)制算法
為了解決效率問題辛臊,復(fù)制(Copying)收集算法出現(xiàn)了仙粱。
- 工作原理
復(fù)制算法把可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊彻舰。
當(dāng)使用中的這塊內(nèi)存用完了伐割,就把存活的對(duì)象復(fù)制到另一塊內(nèi)存上,然后把已使用的空間一次清理掉刃唤。
這樣每次都是對(duì)半個(gè)內(nèi)存區(qū)域進(jìn)行回收隔心,內(nèi)存分配時(shí)也不用考慮內(nèi)存碎片等復(fù)雜問題。
-
優(yōu)點(diǎn)
復(fù)制算法的優(yōu)點(diǎn)是每次只對(duì)半個(gè)內(nèi)存區(qū)域進(jìn)行內(nèi)存回收尚胞,分配內(nèi)存時(shí)也不用考慮內(nèi)存碎片等復(fù)雜情況硬霍,只要一動(dòng)堆頂指針,按順序分配內(nèi)存即可笼裳。
-
缺點(diǎn)
-
浪費(fèi)空間
把內(nèi)存縮小一半來使用太浪費(fèi)空間唯卖。
-
有時(shí)效率較低
在對(duì)象存活率高時(shí),要進(jìn)行較多的復(fù)制操作侍咱,這時(shí)效率就變低了
-
8.3 標(biāo)記-整理算法
在復(fù)制算法中耐床,如果不想浪費(fèi) 50% 的空間密幔,就需要有額外的空間進(jìn)行分配擔(dān)保楔脯,以應(yīng)對(duì)被使用內(nèi)存中所有對(duì)象都存活的低端情況,所以養(yǎng)老區(qū)不能用這種算法胯甩。
根據(jù)養(yǎng)老區(qū)的特點(diǎn)昧廷,有人提出了一種標(biāo)記-整理(Mark-Compact)算法。
-
工作原理
標(biāo)記-整理算法的標(biāo)記過程與標(biāo)記-清除算法一樣偎箫,但后續(xù)步驟是讓所有存活的對(duì)象向一端移動(dòng)木柬,然后直接清理掉邊界外的內(nèi)存。
8.4 分代收集算法
現(xiàn)代商業(yè)虛擬機(jī)的垃圾回收都采用分代收集(Generational Collection)算法淹办,這種算法會(huì)根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊眉枕,這樣就可以根據(jù)各個(gè)區(qū)域的特點(diǎn)采用最適當(dāng)?shù)氖占惴ā?/p>
在新生區(qū),每次垃圾收集都有大批對(duì)象死去,只有少量存活速挑,所以可以用復(fù)制算法谤牡。
養(yǎng)老區(qū)中因?yàn)閷?duì)象存活率高、沒有額外空間對(duì)它進(jìn)行擔(dān)保姥宝,就必須使用標(biāo)記-清理或標(biāo)記-整理算法進(jìn)行回收翅萤。
堆內(nèi)存可分為新生區(qū)、養(yǎng)老區(qū)和永久存儲(chǔ)區(qū)三個(gè)區(qū)域腊满。
-
新生區(qū)
新生區(qū)(Young Generation Space)是類的誕生套么、成長(zhǎng)和消亡的區(qū)域。
新生區(qū)又分為伊甸區(qū)(Eden space)碳蛋、幸存者區(qū)(Survivor space)兩部分胚泌。
-
伊甸區(qū)
大多數(shù)情況下,對(duì)象都是在伊甸區(qū)中分配的肃弟,當(dāng)伊甸區(qū)沒有足夠的空間進(jìn)行分配時(shí)诸迟,虛擬機(jī)將發(fā)起一次 Minor GC。
Minor GC 是指發(fā)生在新生區(qū)的垃圾收集動(dòng)作愕乎,Minor GC 非常頻繁阵苇,回收速度也比較快。
當(dāng)伊甸區(qū)的空間用完時(shí)感论,GC 會(huì)對(duì)伊甸區(qū)進(jìn)行垃圾回收绅项,然后把伊甸區(qū)剩下的對(duì)象移動(dòng)到幸存 0 區(qū)。
-
幸存 0 區(qū)
如果幸存 0 區(qū)滿了比肄,GC 會(huì)對(duì)該區(qū)域進(jìn)行垃圾回收快耿,然后再把該區(qū)剩下的對(duì)象移動(dòng)到幸存 1 區(qū)。
-
幸存 1 區(qū)
如果幸存 1 區(qū)滿了芳绩,GC 會(huì)對(duì)該區(qū)域進(jìn)行垃圾回收掀亥,然后把幸存 1 區(qū)中的對(duì)象移動(dòng)到養(yǎng)老區(qū)。
-
-
養(yǎng)老區(qū)
養(yǎng)老區(qū)(Tenure Generation Space)用于保存從新生區(qū)篩選出來的 Java 對(duì)象妥色。
當(dāng)幸存 1 區(qū)移動(dòng)嘗試對(duì)象到養(yǎng)老區(qū)搪花,但是發(fā)現(xiàn)空間不足時(shí),虛擬機(jī)會(huì)發(fā)起一次 Major GC嘹害。
Major GC 的速度一般比 Minor GC 慢 10 倍以上撮竿。
大對(duì)象會(huì)直接進(jìn)入養(yǎng)老區(qū),比如很大的數(shù)字和很長(zhǎng)的字符串笔呀。
-
永久存儲(chǔ)區(qū)
永久存儲(chǔ)區(qū)(Permanent Space)是一個(gè)常駐內(nèi)存區(qū)域幢踏,用于存放 JDK 自身攜帶的 Class Interface 元數(shù)據(jù)。
永久存儲(chǔ)區(qū)存儲(chǔ)的是運(yùn)行環(huán)境必需的類信息许师,被裝載進(jìn)該區(qū)域的數(shù)據(jù)是不會(huì)被垃圾回收器回收掉的房蝉,只有 JVM 關(guān)閉時(shí)才會(huì)釋放此區(qū)域的內(nèi)存僚匆。