JAVA GC
與C語(yǔ)言(一般由程序員分配釋放,若程序員不釋放,程序結(jié)束時(shí)可能由OS回收(堆內(nèi)存))不同(由此程序會(huì)經(jīng)常擔(dān)心會(huì)不會(huì)出現(xiàn)內(nèi)存泄露和溢出的問(wèn)題),Java內(nèi)存(堆內(nèi)存)的分配與回收由JVM垃圾收集器自動(dòng)完成。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,所有對(duì)象實(shí)例和數(shù)組都在堆上進(jìn)行內(nèi)存分配。Java GC機(jī)制主要完成3件事:確定哪些內(nèi)存需要回收,確定什么時(shí)候需要執(zhí)行GC,如何執(zhí)行GC。學(xué)習(xí)Java GC機(jī)制,可以幫助我們?cè)谌粘9ぷ髦信挪楦鞣N內(nèi)存溢出或泄露問(wèn)題,解決性能瓶頸,達(dá)到更高的并發(fā)量,寫(xiě)出更高效的程序。
要理解javaGC,首先我們需要了解java的內(nèi)存機(jī)制
在Java運(yùn)行時(shí)的數(shù)據(jù)區(qū)里,由JVM管理的內(nèi)存區(qū)域分為下圖幾個(gè)模塊:
1.程序計(jì)數(shù)器(Program Counter Register):程序計(jì)數(shù)器是一個(gè)比較小的內(nèi)存區(qū)域,用于指示當(dāng)前線程所執(zhí)行的字節(jié)碼執(zhí)行到了第幾行,可以理解為是當(dāng)前線程的行號(hào)指示器。字節(jié)碼解釋器在工作時(shí),會(huì)通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)取下一條語(yǔ)句指令。
2.虛擬機(jī)棧(JVM Stack):一個(gè)線程的每個(gè)方法在執(zhí)行的同時(shí),都會(huì)創(chuàng)建一個(gè)棧幀(Statck Frame),棧幀中存儲(chǔ)的有局部變量表、操作站、動(dòng)態(tài)鏈接、方法出口等,當(dāng)方法被調(diào)用時(shí),棧幀在JVM棧中入棧,當(dāng)方法執(zhí)行完成時(shí),棧幀出棧。
虛擬機(jī)棧中定義了兩種異常,如果線程調(diào)用的棧深度大于虛擬機(jī)允許的最大深度,則拋出StatckOverFlowError(棧溢出);不過(guò)多數(shù)Java虛擬機(jī)都允許動(dòng)態(tài)擴(kuò)展虛擬機(jī)棧的大小(有少部分是固定長(zhǎng)度的),所以線程可以一直申請(qǐng)棧,直到內(nèi)存不足,此時(shí),會(huì)拋出OutOfMemoryError(內(nèi)存溢出)。每個(gè)線程對(duì)應(yīng)著一個(gè)虛擬機(jī)棧,因此虛擬機(jī)棧也是線程私有的。
3.本地方法棧(Native Method Statck):本地方法棧在作用,運(yùn)行機(jī)制,異常類型等方面都與虛擬機(jī)棧相同,唯一的區(qū)別是:虛擬機(jī)棧是執(zhí)行Java方法的,而本地方法棧是用來(lái)執(zhí)行native方法的,在很多虛擬機(jī)中(如Sun的JDK默認(rèn)的HotSpot虛擬機(jī)),會(huì)將本地方法棧與虛擬機(jī)棧放在一起使用。
本地方法棧也是線程私有的。
4.堆區(qū)(Heap):堆區(qū)是理解Java GC機(jī)制最重要的區(qū)域,沒(méi)有之一。在JVM所管理的內(nèi)存中,堆區(qū)是最大的一塊,堆區(qū)也是Java GC機(jī)制所管理的主要內(nèi)存區(qū)域,堆區(qū)由所有線程共享,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。堆區(qū)的存在是為了存儲(chǔ)對(duì)象實(shí)例,原則上講,所有的對(duì)象都在堆區(qū)上分配內(nèi)存(不過(guò)現(xiàn)代技術(shù)里,也不是這么絕對(duì)的,也有棧上直接分配的)。
一般的,根據(jù)Java虛擬機(jī)規(guī)范規(guī)定,堆內(nèi)存需要在邏輯上是連續(xù)的(在物理上不需要),在實(shí)現(xiàn)時(shí),可以是固定大小的,也可以是可擴(kuò)展的,目前主流的虛擬機(jī)都是可擴(kuò)展的。如果在執(zhí)行垃圾回收之后,仍沒(méi)有足夠的內(nèi)存分配,也不能再擴(kuò)展,將會(huì)拋出OutOfMemoryError:Java heap space異常。
5,方法區(qū)(Method Area):在Java虛擬機(jī)規(guī)范中,將方法區(qū)作為堆的一個(gè)邏輯部分來(lái)對(duì)待,但事實(shí)上,方法區(qū)并不是堆(Non-Heap);另外,不少人的博客中,將Java GC的分代收集機(jī)制分為3個(gè)代:青年代,老年代,永久代,這些作者將方法區(qū)定義為“永久代”,這是因?yàn)?#xff0c;對(duì)于之前的HotSpot Java虛擬機(jī)的實(shí)現(xiàn)方式中,將分代收集的思想擴(kuò)展到了方法區(qū),并將方法區(qū)設(shè)計(jì)成了永久代。不過(guò),除HotSpot之外的多數(shù)虛擬機(jī),并不將方法區(qū)當(dāng)做永久代,HotSpot本身,也已經(jīng)取消永久代。
方法區(qū)是各個(gè)線程共享的區(qū)域,用于存儲(chǔ)已經(jīng)被虛擬機(jī)加載的類信息(即加載類時(shí)需要加載的信息,包括版本、field、方法、接口等信息)、final常量、靜態(tài)變量、編譯器即時(shí)編譯的代碼等。
6.直接內(nèi)存(Direct Memory):直接內(nèi)存并不是JVM管理的內(nèi)存,可以這樣理解,直接內(nèi)存,就是JVM以外的機(jī)器內(nèi)存,比如,你有4G的內(nèi)存,JVM占用了1G,則其余的3G就是直接內(nèi)存,JDK中有一種基于通道(Channel)和緩沖區(qū)(Buffer)的內(nèi)存分配方式,將由C語(yǔ)言實(shí)現(xiàn)的native函數(shù)庫(kù)分配在直接內(nèi)存中,用存儲(chǔ)在JVM堆中的DirectByteBuffer來(lái)引用。由于直接內(nèi)存收到本機(jī)器內(nèi)存的限制,所以也可能出現(xiàn)OutOfMemoryError的異常。
Java內(nèi)存分配和回收的機(jī)制概括的說(shuō),就是:分代分配,分代回收。
為了進(jìn)行高效的垃圾回收,虛擬機(jī)(我們將在下一節(jié)中詳細(xì)介紹java虛擬機(jī))把堆內(nèi)存劃分成新生代(Young Generation)、老年代(Old Generation)和永久代(Permanent Generation)3個(gè)區(qū)域。
新生代
新生代由 Eden 與 Survivor Space(S0,S1)構(gòu)成,大小通過(guò)-Xmn參數(shù)指定,Eden 與 Survivor Space 的內(nèi)存大小比例默認(rèn)為8:1,可以通過(guò)-XX:SurvivorRatio 參數(shù)指定,比如新生代為10M 時(shí),Eden分配8M,S0和S1各分配1M。
Eden:希臘語(yǔ),意思為伊甸園,在圣經(jīng)中,伊甸園含有樂(lè)園的意思,根據(jù)《舊約·創(chuàng)世紀(jì)》記載,上帝耶和華照自己的形像造了第一個(gè)男人亞當(dāng),再用亞當(dāng)?shù)囊粋€(gè)肋骨創(chuàng)造了一個(gè)女人夏娃,并安置他們住在了伊甸園。
大多數(shù)情況下,對(duì)象在Eden中分配,當(dāng)Eden沒(méi)有足夠空間時(shí),會(huì)觸發(fā)一次Minor GC,虛擬機(jī)提供了-XX:+PrintGCDetails參數(shù),告訴虛擬機(jī)在發(fā)生垃圾回收時(shí)打印內(nèi)存回收日志。
Survivor:意思為幸存者,是新生代和老年代的緩沖區(qū)域。
當(dāng)新生代發(fā)生GC(Minor GC)時(shí),會(huì)將存活的對(duì)象移動(dòng)到S0內(nèi)存區(qū)域,并清空Eden區(qū)域,當(dāng)再次發(fā)生Minor GC時(shí),將Eden和S0中存活的對(duì)象移動(dòng)到S1內(nèi)存區(qū)域。
存活對(duì)象會(huì)反復(fù)在S0和S1之間移動(dòng),當(dāng)對(duì)象從Eden移動(dòng)到Survivor或者在Survivor之間移動(dòng)時(shí),對(duì)象的GC年齡自動(dòng)累加,當(dāng)GC年齡超過(guò)默認(rèn)閾值15時(shí),會(huì)將該對(duì)象移動(dòng)到老年代,可以通過(guò)參數(shù)-XX:MaxTenuringThreshold 對(duì)GC年齡的閾值進(jìn)行設(shè)置。
老年代
老年代的空間大小即-Xmx 與-Xmn 兩個(gè)參數(shù)之差,用于存放經(jīng)過(guò)幾次Minor GC之后依舊存活的對(duì)象。當(dāng)老年代的空間不足時(shí),會(huì)觸發(fā)Major GC/Full GC,速度一般比Minor GC慢10倍以上。
永久代
在JDK8之前的HotSpot實(shí)現(xiàn)中,類的元數(shù)據(jù)如方法數(shù)據(jù)、方法信息(字節(jié)碼,棧和變量大小)、運(yùn)行時(shí)常量池、已確定的符號(hào)引用和虛方法表等被保存在永久代中,32位默認(rèn)永久代的大小為64M,64位默認(rèn)為85M,可以通過(guò)參數(shù)-XX:MaxPermSize進(jìn)行設(shè)置,一旦類的元數(shù)據(jù)超過(guò)了永久代大小,就會(huì)拋出OOM異常。
擬機(jī)團(tuán)隊(duì)在JDK8的HotSpot中,把永久代從Java堆中移除了,并把類的元數(shù)據(jù)直接保存在本地內(nèi)存區(qū)域(堆外內(nèi)存),稱之為元空間。
如何判斷對(duì)象是否存活
GC動(dòng)作發(fā)生之前,需要確定堆內(nèi)存中哪些對(duì)象是存活的,一般有兩種方法:引用計(jì)數(shù)法和可達(dá)性分析法。
1、引用計(jì)數(shù)法
在對(duì)象上添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)對(duì)象引用它時(shí),計(jì)數(shù)器加1,當(dāng)使用完該對(duì)象時(shí),計(jì)數(shù)器減1,計(jì)數(shù)器值為0的對(duì)象表示不可能再被使用。
引用計(jì)數(shù)法實(shí)現(xiàn)簡(jiǎn)單,判定高效,但不能解決對(duì)象之間相互引用的問(wèn)題。
2.可達(dá)性分析法
通過(guò)一系列稱為 “GC Roots” 的對(duì)象作為起點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索,搜索路徑稱為 “引用鏈”,以下對(duì)象可作為GC Roots:
- 本地變量表中引用的對(duì)象
- 方法區(qū)中靜態(tài)變量引用的對(duì)象
- 方法區(qū)中常量引用的對(duì)象
- Native方法引用的對(duì)象
當(dāng)一個(gè)對(duì)象到 GC Roots 沒(méi)有任何引用鏈時(shí),意味著該對(duì)象可以被回收。
在可達(dá)性分析法中,判定一個(gè)對(duì)象objA是否可回收,至少要經(jīng)歷兩次標(biāo)記過(guò)程:
1、如果對(duì)象objA到 GC Roots沒(méi)有引用鏈,則進(jìn)行第一次標(biāo)記。
2、如果對(duì)象objA重寫(xiě)了finalize()方法,且還未執(zhí)行過(guò),那么objA會(huì)被插入到F-Queue隊(duì)列中,由一個(gè)虛擬機(jī)自動(dòng)創(chuàng)建的、低優(yōu)先級(jí)的Finalizer線程觸發(fā)其finalize()方法。finalize()方法是對(duì)象逃脫死亡的最后機(jī)會(huì),GC會(huì)對(duì)隊(duì)列中的對(duì)象進(jìn)行第二次標(biāo)記,如果objA在finalize()方法中與引用鏈上的任何一個(gè)對(duì)象建立聯(lián)系,那么在第二次標(biāo)記時(shí),objA會(huì)被移出“即將回收”集合。
總結(jié)
- 上一篇: mysql主表一条数据对应从表多条数据需
- 下一篇: API性能优化之异步