一、Java內(nèi)存結(jié)構(gòu)
1加派、Java堆(Java Heap)
java堆是java虛擬機(jī)所管理的內(nèi)存中最大的一塊叫确,是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建芍锦。此內(nèi)存區(qū)域的唯一目的就是存放對象實(shí)例竹勉,這一點(diǎn)在Java虛擬機(jī)規(guī)范中的描述是:所有的對象實(shí)例以及數(shù)組都要在堆上分配。
java堆是垃圾收集器管理的主要區(qū)域娄琉,因此也被成為“GC堆”(Garbage Collected Heap)次乓。從內(nèi)存回收角度來看java堆可分為:新生代和老生代吓歇。從內(nèi)存分配的角度看,線程共享的Java堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer票腰,TLAB)照瘾。無論怎么劃分,都與存放內(nèi)容無關(guān)丧慈,無論哪個(gè)區(qū)域,存儲(chǔ)的都是對象實(shí)例主卫,進(jìn)一步的劃分都是為了更好的回收內(nèi)存逃默,或者更快的分配內(nèi)存。
根據(jù)Java虛擬機(jī)規(guī)范的規(guī)定簇搅,java堆可以處于物理上不連續(xù)的內(nèi)存空間中完域。當(dāng)前主流的虛擬機(jī)都是可擴(kuò)展的(通過 -Xmx 和 -Xms 控制)。如果堆中沒有內(nèi)存完成實(shí)例分配瘩将,并且堆也無法再擴(kuò)展時(shí)吟税,將會(huì)拋出OutOfMemoryError異常。
2姿现、Java虛擬機(jī)棧(Java Virtual Machine Stacks)
java虛擬機(jī)也是線程私有的肠仪,它的生命周期和線程相同。虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame)用于存儲(chǔ)局部變量表备典、操作數(shù)棧异旧、動(dòng)態(tài)鏈接、方法出口等信息提佣。
咱們常說的堆內(nèi)存吮蛹、棧內(nèi)存中,棧內(nèi)存指的就是虛擬機(jī)棧拌屏。局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(8個(gè)基本數(shù)據(jù)類型)潮针、對象引用(地址指針)、returnAddress類型倚喂。
局部變量表所需的內(nèi)存空間在編譯期間完成分配每篷。在運(yùn)行期間不會(huì)改變局部變量表的大小。
這個(gè)區(qū)域規(guī)定了兩種異常狀態(tài):如果線程請求的棧深度大于虛擬機(jī)所允許的深度务唐,則拋出StackOverflowError異常雳攘;如果虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展枫笛,在擴(kuò)展是無法申請到足夠的內(nèi)存吨灭,就會(huì)拋出OutOfMemoryError異常。
3刑巧、本地方法棧(Native Method Stack)
本地方法棧與虛擬機(jī)棧所發(fā)揮作用非常相似喧兄,它們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù)无畔,而本地方法棧則為虛擬機(jī)使用到的native方法服務(wù)。本地方法棧也是拋出兩個(gè)異常吠冤。
4浑彰、方法區(qū)(Method Area)
方法區(qū)與java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域拯辙,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息郭变、常量、靜態(tài)變量涯保、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)诉濒。它有個(gè)別命叫Non-Heap(非堆)。當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí)夕春,拋出OutOfMemoryError異常未荒。
5、直接內(nèi)存(Direct Memory)
直接內(nèi)存不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分及志,也不是java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域片排。但這部分區(qū)域也唄頻繁使用,而且也可能導(dǎo)致OutOfMemoryError異常
在JDK1.4中新加入的NIO(New Input/Output)類速侈,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式率寡,它可以使用Native函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作倚搬。
6勇劣、運(yùn)行時(shí)常量池(Runtime Constant Pool)
運(yùn)行時(shí)常量池是方法區(qū)的一部分。Class文件中除了有類的版本潭枣、字段比默、方法、接口等描述信息外盆犁,還有一項(xiàng)信息是常量池命咐,用于存放編譯期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放谐岁。
7醋奠、程序計(jì)數(shù)器(Program Counter Register)
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器伊佃。
由于Java虛擬機(jī)的多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的窜司,一個(gè)處理器都只會(huì)執(zhí)行一條線程中的指令。因此航揉,為了線程切換后能恢復(fù)到正確的執(zhí)行位置塞祈,每條線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各個(gè)線程之間計(jì)數(shù)器互不影響帅涂,獨(dú)立存儲(chǔ)议薪。稱之為“線程私有”的內(nèi)存尤蛮。程序計(jì)數(shù)器內(nèi)存區(qū)域是虛擬機(jī)中唯一沒有規(guī)定OutOfMemoryError情況的區(qū)域。
8斯议、執(zhí)行引擎
虛擬機(jī)核心的組件就是執(zhí)行引擎产捞,它負(fù)責(zé)執(zhí)行虛擬機(jī)的字節(jié)碼,一般戶先進(jìn)行編譯成機(jī)器碼后執(zhí)行哼御。
9坯临、垃圾收集系統(tǒng)
垃圾收集系統(tǒng)是Java的核心,也是不可少的恋昼,Java有一套自己進(jìn)行垃圾清理的機(jī)制尿扯,開發(fā)人員無需手工清理
二、垃圾回收機(jī)制算法分析
1焰雕、什么是垃圾回收機(jī)制
不定時(shí)去堆內(nèi)存中清理不可達(dá)對象。不可達(dá)的對象并不會(huì)馬上就會(huì)直接回收芳杏, 垃圾收集器在一個(gè)Java程序中的執(zhí)行是自動(dòng)的矩屁,不能強(qiáng)制執(zhí)行,即使程序員能明確地判斷出有一塊內(nèi)存已經(jīng)無用了爵赵,是應(yīng)該回收的吝秕,程序員也不能強(qiáng)制垃圾收集器回收該內(nèi)存塊。程序員唯一能做的就是通過調(diào)用System.gc 方法來"建議"執(zhí)行垃圾收集器空幻,但其是否可以執(zhí)行烁峭,什么時(shí)候執(zhí)行卻都是不可知的。這也是垃圾收集器的最主要的缺點(diǎn)秕铛。
public class Test {
public static void main(String[] args) {
Test test = new Test();
test = null;
System.gc(); // 手動(dòng)回收垃圾
}
@Override
protected void finalize() throws Throwable {
// gc回收垃圾之前調(diào)用
System.out.println("垃圾回收機(jī)制...");
}
}
2约郁、finalize方法作用
Java技術(shù)使用finalize()方法在垃圾收集器將對象從內(nèi)存中清除出去前,做必要的清理工作但两。這個(gè)方法是由垃圾收集器在確定這個(gè)對象沒有被引用時(shí)對這個(gè)對象調(diào)用的鬓梅。它是在Object類中定義的,因此所有的類都繼承了它谨湘。子類覆蓋finalize()方法以整理系統(tǒng)資源或者執(zhí)行其他清理工作绽快。finalize()方法是在垃圾收集器刪除對象之前對這個(gè)對象調(diào)用的。
3紧阔、新生代與老年代
Java 中的堆是 JVM 所管理的最大的一塊內(nèi)存空間坊罢,主要用于存放各種類的實(shí)例對象。
在 Java 中擅耽,堆被劃分成兩個(gè)不同的區(qū)域:新生代 ( Young )活孩、老年代 ( Old )。新生代 ( Young ) 又被劃分為三個(gè)區(qū)域:Eden乖仇、From Survivor诱鞠、To Survivor挎挖。
這樣劃分的目的是為了使 JVM 能夠更好的管理堆內(nèi)存中的對象,包括內(nèi)存的分配以及回收航夺。
堆的內(nèi)存模型大致為:
默認(rèn)的蕉朵,新生代 ( Young ) 與老年代 ( Old ) 的比例的值為 1:2 ( 該值可以通過參數(shù) –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小阳掐。老年代 ( Old ) = 2/3 的堆空間大小始衅。其中,新生代 ( Young ) 被細(xì)分為 Eden 和 兩個(gè) Survivor 區(qū)域缭保,這兩個(gè) Survivor 區(qū)域分別被命名為 from 和 to汛闸,以示區(qū)分。
默認(rèn)的艺骂,Eden : from : to = 8 : 1 : 1 ( 可以通過參數(shù) –XX:SurvivorRatio 來設(shè)定 )诸老,即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小钳恕。
根據(jù)垃圾回收機(jī)制的不同别伏,Java堆有可能擁有不同的結(jié)構(gòu),最為常見的就是將整個(gè)Java堆分為
新生代和老年代忧额。其中新生帶存放新生的對象或者年齡不大的對象途事,老年代則存放老年對象电爹。
新生代分為den區(qū)气嫁、s0區(qū)母市、s1區(qū),s0和s1也被稱為from和to區(qū)域托嚣,他們是兩塊大小相等并且可以互相角色的空間巩检。
絕大多數(shù)情況下,對象首先分配在eden區(qū)示启,在新生代回收后碴巾,如果對象還存活,則進(jìn)入s0或s1區(qū)丑搔,之后每經(jīng)過一次
新生代回收厦瓢,如果對象存活則它的年齡就加1,對象達(dá)到一定的年齡后啤月,則進(jìn)入老年代煮仇。
三、如何判斷對象是否存活
1谎仲、引用計(jì)數(shù)法
概念
引用計(jì)數(shù)法就是如果一個(gè)對象沒有被任何引用指向浙垫,則可視之為垃圾。這種方法的缺點(diǎn)就是不能檢測到環(huán)的存在。
首先需要聲明夹姥,至少主流的Java虛擬機(jī)里面都沒有選用引用計(jì)數(shù)算法來管理內(nèi)存杉武。
什么是引用計(jì)數(shù)算法:給對象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí)辙售,計(jì)數(shù)器值加1轻抱;當(dāng)引用失效時(shí),計(jì)數(shù)器值減1.任何時(shí)刻計(jì)數(shù)器值為0的對象就是不可能再被使用的旦部。那為什么主流的Java虛擬機(jī)里面都沒有選用這種算法呢祈搜?其中最主要的原因是它很難解決對象之間相互循環(huán)引用的問題。
2士八、根搜索算法
概念
根搜索算法的基本思路就是通過一系列名為”GC Roots”的對象作為起始點(diǎn)容燕,從這些節(jié)點(diǎn)開始向下搜索,搜索所走過的路徑稱為引用鏈(Reference Chain)婚度,當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈相連時(shí)蘸秘,則證明此對象是不可用的。
這個(gè)算法的基本思想是通過一系列稱為“GC Roots”的對象作為起始點(diǎn)蝗茁,從這些節(jié)點(diǎn)向下搜索醋虏,搜索所走過的路徑稱為引用鏈,當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈(即GC Roots到對象不可達(dá))時(shí)评甜,則證明此對象是不可用的。
那么問題又來了仔涩,如何選取GCRoots對象呢忍坷?在Java語言中,可以作為GCRoots的對象包括下面幾種:
- (1). 虛擬機(jī)棧(棧幀中的局部變量區(qū)熔脂,也叫做局部變量表)中引用的對象佩研。
- (2). 方法區(qū)中的類靜態(tài)屬性引用的對象。
- (3). 方法區(qū)中常量引用的對象霞揉。
- (4). 本地方法棧中JNI(Native方法)引用的對象旬薯。
下面給出一個(gè)GCRoots的例子,如下圖适秩,為GCRoots的引用鏈绊序。
user3和user5沒有引用鏈
根搜索算法的基本思路就是通過一系列名為”GC Roots”的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始向下搜索秽荞,搜索所走過的路徑稱為引用鏈(Reference Chain)骤公,當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈相連時(shí),則證明此對象是不可用的扬跋。
四阶捆、垃圾回收機(jī)制策略
1、標(biāo)記清除算法
概念
該算法有兩個(gè)階段。
- 標(biāo)記階段:找到所有可訪問的對象洒试,做個(gè)標(biāo)記
- 清除階段:遍歷堆倍奢,把未被標(biāo)記的對象回收
應(yīng)用場景
該算法一般應(yīng)用于老年代,因?yàn)槔夏甏膶ο笊芷诒容^長。
優(yōu)缺點(diǎn)
標(biāo)記清除算法的優(yōu)點(diǎn)和缺點(diǎn)
- 1.優(yōu)點(diǎn)
- 是可以解決循環(huán)引用的問題
- 必要時(shí)才回收(內(nèi)存不足時(shí))
- 2.缺點(diǎn):
- 回收時(shí)垒棋,應(yīng)用需要掛起卒煞,也就是stop the world。
- 標(biāo)記和清除的效率不高捕犬,尤其是要掃描的對象比較多的時(shí)候
- 會(huì)造成內(nèi)存碎片(會(huì)導(dǎo)致明明有內(nèi)存空間,但是由于不連續(xù),申請稍微大一些的對象無法做到),
2跷坝、復(fù)制算法
概念
如果jvm使用了coping算法,一開始就會(huì)將可用內(nèi)存分為兩塊碉碉,from域和to域柴钻, 每次只是使用from域,to域則空閑著垢粮。當(dāng)from域內(nèi)存不夠了贴届,開始執(zhí)行GC操作,這個(gè)時(shí)候蜡吧,會(huì)把from域存活的對象拷貝到to域,然后直接把from域進(jìn)行內(nèi)存清理毫蚓。
應(yīng)用場景
coping算法一般是使用在新生代中,因?yàn)樾律械膶ο笠话愣际浅λ赖奈羯疲婊顚ο蟮臄?shù)量并不多元潘,這樣使用coping算法進(jìn)行拷貝時(shí)效率比較高。jvm將Heap 內(nèi)存劃分為新生代與老年代君仆,又將新生代劃分為Eden(伊甸園) 與2塊Survivor Space(幸存者區(qū)) ,然后在Eden –>Survivor Space 以及From Survivor Space 與To Survivor Space 之間實(shí)行Copying 算法翩概。 不過jvm在應(yīng)用coping算法時(shí),并不是把內(nèi)存按照1:1來劃分的返咱,這樣太浪費(fèi)內(nèi)存空間了钥庇。一般的jvm都是8:1。也即是說,Eden區(qū):From區(qū):To區(qū)域的比例是
始終有90%的空間是可以用來創(chuàng)建對象的,而剩下的10%用來存放回收后存活的對象咖摹。
- 1评姨、當(dāng)Eden區(qū)滿的時(shí)候,會(huì)觸發(fā)第一次young gc,把還活著的對象拷貝到Survivor From區(qū);當(dāng)Eden區(qū)再次觸發(fā)young gc的時(shí)候,會(huì)掃描Eden區(qū)和From區(qū)域,對兩個(gè)區(qū)域進(jìn)行垃圾回收,經(jīng)過這次回收后還存活的對象,則直接復(fù)制到To區(qū)域,并將Eden和From區(qū)域清空萤晴。
- 2吐句、當(dāng)后續(xù)Eden又發(fā)生young gc的時(shí)候,會(huì)對Eden和To區(qū)域進(jìn)行垃圾回收,存活的對象復(fù)制到From區(qū)域,并將Eden和To區(qū)域清空。
- 3店读、可見部分對象會(huì)在From和To區(qū)域中復(fù)制來復(fù)制去,如此交換15次(由JVM參數(shù)MaxTenuringThreshold決定,這個(gè)參數(shù)默認(rèn)是15),最終如果還是存活,就存入到老年代
注意: 萬一存活對象數(shù)量比較多蕴侧,那么To域的內(nèi)存可能不夠存放,這個(gè)時(shí)候會(huì)借助老年代的空間两入。
優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):在存活對象不多的情況下净宵,性能高,能解決內(nèi)存碎片和java垃圾回收算法之-標(biāo)記清除 中導(dǎo)致的引用更新問題。
- 缺點(diǎn): 會(huì)造成一部分的內(nèi)存浪費(fèi)择葡。不過可以根據(jù)實(shí)際情況紧武,將內(nèi)存塊大小比例適當(dāng)調(diào)整;如果存活對象的數(shù)量比較大敏储,coping的性能會(huì)變得很差阻星。
3、標(biāo)記壓縮算法
標(biāo)記清除算法和標(biāo)記壓縮算法非常相同已添,但是標(biāo)記壓縮算法在標(biāo)記清除算法之上解決內(nèi)存碎片化
概念
壓縮算法簡單介紹
- 任意順序 : 即不考慮原先對象的排列順序妥箕,也不考慮對象之間的引用關(guān)系,隨意移動(dòng)對象更舞;
- 線性順序 : 考慮對象的引用關(guān)系畦幢,例如a對象引用了b對象,則盡可能將a和b移動(dòng)到一塊缆蝉;
- 滑動(dòng)順序 : 按照對象原來在堆中的順序滑動(dòng)到堆的一端宇葱。
優(yōu)缺點(diǎn)
- 優(yōu)點(diǎn):解決內(nèi)存碎片問題,
- 缺點(diǎn):壓縮階段刊头,由于移動(dòng)了可用對象黍瞧,需要去更新引用。
4原杂、Minor GC和Full GC區(qū)別
概念:
新生代 GC(Minor GC):指發(fā)生在新生代的垃圾收集動(dòng)作印颤,因?yàn)?Java 對象大多都具
備朝生夕滅的特性,所以 Minor GC 非常頻繁穿肄,一般回收速度也比較快年局。
老年代 GC(Major GC / Full GC):指發(fā)生在老年代的 GC,出現(xiàn)了 Major GC被碗,經(jīng)常
會(huì)伴隨至少一次的 Minor GC(但非絕對的某宪,在 ParallelScavenge 收集器的收集策略里
就有直接進(jìn)行 Major GC 的策略選擇過程) 仿村。MajorGC 的速度一般會(huì)比 Minor GC 慢 10
倍以上锐朴。
Minor GC觸發(fā)機(jī)制:
當(dāng)年輕代滿時(shí)就會(huì)觸發(fā)Minor GC,這里的年輕代滿指的是Eden代滿蔼囊,Survivor滿不會(huì)引發(fā)GCFull GC觸發(fā)機(jī)制:
當(dāng)年老代滿時(shí)會(huì)引發(fā)Full GC焚志,F(xiàn)ull GC將會(huì)同時(shí)回收年輕代、年老代畏鼓,當(dāng)永久代滿時(shí)也會(huì)引發(fā)Full GC酱酬,會(huì)導(dǎo)致Class、Method元信息的卸載其中
Minor GC如下圖所示
虛擬機(jī)給每個(gè)對象定義了一個(gè)對象年齡(Age)計(jì)數(shù)器云矫。如果對象在 Eden 出生并經(jīng)過第一次 Minor GC 后仍然存活膳沽,并且能被 Survivor 容納的話,將被移動(dòng)到 Survivor 空間中,并將對象年齡設(shè)為 1挑社。對象在 Survivor 區(qū)中每熬過一次 Minor GC陨界,年齡就增加 1 歲,當(dāng)它的年齡增加到一定程度(默認(rèn)為 15 歲)時(shí)痛阻,就會(huì)被晉升到老年代中菌瘪。對象晉升老年代的年齡閾值,可以通過參數(shù) -XX:MaxTenuringThreshold (閾值)來設(shè)置阱当。
JVM的永久代中會(huì)發(fā)生垃圾回收么俏扩?
垃圾回收不會(huì)發(fā)生在永久代,如果永久代滿了或者是超過了臨界值弊添,會(huì)觸發(fā)完全垃圾回收(Full GC)录淡。如果你仔細(xì)查看垃圾收集器的輸出信息,就會(huì)發(fā)現(xiàn)永久代也是被回收的表箭。這就是為什么正確的永久代大小對避免Full GC是非常重要的原因赁咙。請參考下Java8:從永久代到元數(shù)據(jù)區(qū)
(注:Java8中已經(jīng)移除了永久代,新加了一個(gè)叫做元數(shù)據(jù)區(qū)的native內(nèi)存區(qū))
5免钻、分代算法
概述
這種算法彼水,根據(jù)對象的存活周期的不同將內(nèi)存劃分成幾塊,新生代和老年代极舔,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴ǚ锔病?梢杂米ブ攸c(diǎn)的思路來理解這個(gè)算法拆魏。
新生代對象朝生夕死,對象數(shù)量多盯桦,只要重點(diǎn)掃描這個(gè)區(qū)域,那么就可以大大提高垃圾收集的效率渤刃。另外老年代對象存儲(chǔ)久拥峦,無需經(jīng)常掃描老年代,避免掃描導(dǎo)致的開銷卖子。
新生代
在新生代略号,每次垃圾收集器都發(fā)現(xiàn)有大批對象死去,只有少量存活洋闽,采用復(fù)制算法玄柠,只需要付出少量存活對象的復(fù)制成本就可以完成收集;
老年代
而老年代中因?yàn)閷ο蟠婊盥矢呓刖恕]有額外空間對它進(jìn)行分配擔(dān)保羽利,就必須“標(biāo)記-清除-壓縮”算法進(jìn)行回收。參看java垃圾回收算法之-標(biāo)記_清除壓縮
新創(chuàng)建的對象被分配在新生代刊懈,如果對象經(jīng)過幾次回收后仍然存活这弧,那么就把這個(gè)對象劃分到老年代娃闲。
老年代區(qū)存放Young區(qū)Survivor滿后觸發(fā)minor GC后仍然存活的對象,當(dāng)Eden區(qū)滿后會(huì)將存活的對象放入Survivor區(qū)域匾浪,如果Survivor區(qū)存不下這些對象畜吊,GC收集器就會(huì)將這些對象直接存放到Old區(qū)中,如果Survivor區(qū)中的對象足夠老户矢,也直接存放到Old區(qū)中玲献。如果Old區(qū)滿了,將會(huì)觸發(fā)Full GC回收整個(gè)堆內(nèi)存梯浪。
個(gè)人博客 蝸牛