今天工作中遇到一個(gè)用于緩存數(shù)據(jù)到內(nèi)存的靜態(tài)變量Stringbuffer熟丸;當(dāng)緩存數(shù)據(jù)大小達(dá)到5M的時(shí)候就把該緩存數(shù)據(jù)寫(xiě)到S3上华望;然后清空該緩存buffer;看了這段代碼我覺(jué)得是不是有點(diǎn)問(wèn)題唠倦;先貼大概的代碼
package com.sanpang.demo;
public class StaticVariableDemo {
//定義一個(gè)緩存buffer;存放數(shù)據(jù)到內(nèi)存;數(shù)據(jù)達(dá)到5M的時(shí)候就寫(xiě)到S3上面;
public static StringBuffer cachebuffer = new StringBuffer();
public static void main(String[] args) {
cachebuffer.append("存的數(shù)據(jù) 達(dá)到5M");
//將數(shù)據(jù)存到S3 ;然后清空buffer
cachebuffer = new StringBuffer();
}
}
看了以上代碼;我擔(dān)心一個(gè)問(wèn)題是這里通過(guò)new 個(gè)新對(duì)象來(lái)起到清空buffer的作用:
cachebuffer = new StringBuffer();
這樣的寫(xiě)法會(huì)不會(huì)導(dǎo)致這個(gè)靜態(tài)變量cachebuffer 之前引用的實(shí)例還存在不;有沒(méi)有被GC回收;如果沒(méi)有會(huì)不會(huì)出現(xiàn)OOM的問(wèn)題丛塌?
帶著這個(gè)疑惑問(wèn)了很多技術(shù)小伙伴得到的回復(fù)都沒(méi)有解決我疑惑的問(wèn)題;于是自己分析了一下畜疾;自己主要疑惑的一個(gè)是這個(gè)靜態(tài)變量在JVM 中的內(nèi)存存儲(chǔ)赴邻;還有一個(gè)疑惑就是這個(gè)靜態(tài)對(duì)象之前引用的地址所指向的實(shí)例會(huì)不會(huì)被GC立即回收;
帶著問(wèn)題看了些關(guān)于JVM GC 的文章啡捶;下面寫(xiě)下我看了這些文章的自己的理解姥敛;不對(duì)的地方希望可以指出;
首先是JVM內(nèi)存結(jié)構(gòu)
根據(jù)《java虛擬機(jī)規(guī)范》規(guī)定届慈,JVM的基本結(jié)構(gòu)一般如下圖所示
從左圖可知徒溪,JVM主要包括四個(gè)部分:
1.類加載器(ClassLoader):在JVM啟動(dòng)時(shí)或者在類運(yùn)行時(shí)將需要的class加載到JVM中忿偷。(右圖表示了從java源文件到JVM的整個(gè)過(guò)程金顿,可配合理解。 關(guān)于類的加載機(jī)制鲤桥,可以參考http://blog.csdn.net/tonytfjing/article/details/47212291)
2.執(zhí)行引擎:負(fù)責(zé)執(zhí)行class文件中包含的字節(jié)碼指令(執(zhí)行引擎的工作機(jī)制揍拆,這里也不細(xì)說(shuō)了,這里主要介紹JVM結(jié)構(gòu))茶凳;
3.內(nèi)存區(qū)(也叫運(yùn)行時(shí)數(shù)據(jù)區(qū)):是在JVM運(yùn)行的時(shí)候操作所分配的內(nèi)存區(qū)嫂拴。運(yùn)行時(shí)內(nèi)存區(qū)主要可以劃分為5個(gè)區(qū)域,如圖:
- 方法區(qū)(Method Area):用于存儲(chǔ)類結(jié)構(gòu)信息的地方贮喧,包括常量池筒狠、靜態(tài)變量、構(gòu)造函數(shù)等箱沦。雖然JVM規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分辩恼, 但它卻有個(gè)別名non-heap(非堆),所以大家不要搞混淆了。方法區(qū)還包含一個(gè)運(yùn)行時(shí)常量池灶伊。
- java堆(Heap):存儲(chǔ)java實(shí)例或者對(duì)象的地方疆前。這塊是GC的主要區(qū)域(后面解釋)。從存儲(chǔ)的內(nèi)容我們可以很容易知道聘萨,方法區(qū)和堆是被所有java線程共享的竹椒。
- java棧(Stack):java棧總是和線程關(guān)聯(lián)在一起米辐,每當(dāng)創(chuàng)建一個(gè)線程時(shí)胸完,JVM就會(huì)為這個(gè)線程創(chuàng)建一個(gè)對(duì)應(yīng)的java棧。在這個(gè)java棧中又會(huì)包含多個(gè)棧幀儡循,每運(yùn)行一個(gè)方法就創(chuàng)建一個(gè)棧幀舶吗,用于存儲(chǔ)局部變量表、操作棧择膝、方法返回值等誓琼。每一個(gè)方法從調(diào)用直至執(zhí)行完成的過(guò)程,就對(duì)應(yīng)一個(gè)棧幀在java棧中入棧到出棧的過(guò)程肴捉。所以java棧是現(xiàn)成私有的腹侣。
- 程序計(jì)數(shù)器(PC Register):用于保存當(dāng)前線程執(zhí)行的內(nèi)存地址。由于JVM程序是多線程執(zhí)行的(線程輪流切換)齿穗,所以為了保證線程切換回來(lái)后傲隶,還能恢復(fù)到原先狀態(tài),就需要一個(gè)獨(dú)立的計(jì)數(shù)器窃页,記錄之前中斷的地方跺株,可見(jiàn)程序計(jì)數(shù)器也是線程私有的。
- 本地方法棧(Native Method Stack):和java棧的作用差不多脖卖,只不過(guò)是為JVM使用到的native方法服務(wù)的乒省。
4.本地方法接口:主要是調(diào)用C或C++實(shí)現(xiàn)的本地方法及返回結(jié)果。二畦木、內(nèi)存分配
我覺(jué)得了解垃圾回收之前袖扛,得先了解JVM是怎么分配內(nèi)存的,然后識(shí)別哪些內(nèi)存是垃圾需要回收十籍,最后才是用什么方式回收蛆封。
Java的內(nèi)存分配原理與C/C++不同,C/C++每次申請(qǐng)內(nèi)存時(shí)都要malloc進(jìn)行系統(tǒng)調(diào)用勾栗,而系統(tǒng)調(diào)用發(fā)生在內(nèi)核空間惨篱,每次都要中斷進(jìn)行切換,這需要一定的開(kāi)銷围俘,而Java虛擬機(jī)是先一次性分配一塊較大的空間砸讳,然后每次new時(shí)都在該空間上進(jìn)行分配和釋放机断,減少了系統(tǒng)調(diào)用的次數(shù),節(jié)省了一定的開(kāi)銷绣夺,這有點(diǎn)類似于內(nèi)存池的概念吏奸;二是有了這塊空間過(guò)后,如何進(jìn)行分配和回收就跟GC機(jī)制有關(guān)了陶耍。
java一般內(nèi)存申請(qǐng)有兩種:靜態(tài)內(nèi)存和動(dòng)態(tài)內(nèi)存奋蔚。很容易理解,編譯時(shí)就能夠確定的內(nèi)存就是靜態(tài)內(nèi)存烈钞,即內(nèi)存是固定的泊碑,系統(tǒng)一次性分配,比如int類型變量毯欣;動(dòng)態(tài)內(nèi)存分配就是在程序執(zhí)行時(shí)才知道要分配的存儲(chǔ)空間大小馒过,比如java對(duì)象的內(nèi)存空間。根據(jù)上面我們知道酗钞,java棧腹忽、程序計(jì)數(shù)器、本地方法棧都是線程私有的砚作,線程生就生窘奏,線程滅就滅,棧中的棧幀隨著方法的結(jié)束也會(huì)撤銷葫录,內(nèi)存自然就跟著回收了着裹。所以這幾個(gè)區(qū)域的內(nèi)存分配與回收是確定的,我們不需要管的米同。但是java堆和方法區(qū)則不一樣骇扇,我們只有在程序運(yùn)行期間才知道會(huì)創(chuàng)建哪些對(duì)象,所以這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的面粮。一般我們所說(shuō)的垃圾回收也是針對(duì)的這一部分少孝。
總之Stack的內(nèi)存管理是順序分配的,而且定長(zhǎng)但金,不存在內(nèi)存回收問(wèn)題韭山;而Heap 則是為java對(duì)象的實(shí)例隨機(jī)分配內(nèi)存郁季,不定長(zhǎng)度冷溃,所以存在內(nèi)存分配和回收的問(wèn)題;
三梦裂、垃圾檢測(cè)似枕、回收算法
垃圾收集器一般必須完成兩件事:檢測(cè)出垃圾;回收垃圾年柠。怎么檢測(cè)出垃圾凿歼?一般有以下幾種方法:
引用計(jì)數(shù)法:給一個(gè)對(duì)象添加引用計(jì)數(shù)器,每當(dāng)有個(gè)地方引用它,計(jì)數(shù)器就加1答憔;引用失效就減1味赃。
好了,問(wèn)題來(lái)了虐拓,如果我有兩個(gè)對(duì)象A和B心俗,互相引用,除此之外蓉驹,沒(méi)有其他任何對(duì)象引用它們城榛,實(shí)際上這兩個(gè)對(duì)象已經(jīng)無(wú)法訪問(wèn),即是我們說(shuō)的垃圾對(duì)象态兴。但是互相引用狠持,計(jì)數(shù)不為0,導(dǎo)致無(wú)法回收瞻润,所以還有另一種方法:
可達(dá)性分析算法:以根集對(duì)象為起始點(diǎn)進(jìn)行搜索喘垂,如果有對(duì)象不可達(dá)的話,即是垃圾對(duì)象绍撞。這里的根集一般包括java棧中引用的對(duì)象王污、方法區(qū)常良池中引用的對(duì)象
本地方法中引用的對(duì)象等。
總之楚午,JVM在做垃圾回收的時(shí)候昭齐,會(huì)檢查堆中的所有對(duì)象是否會(huì)被這些根集對(duì)象引用,不能夠被引用的對(duì)象就會(huì)被垃圾收集器回收矾柜。一般回收算法也有如下幾種:
1.標(biāo)記-清除(Mark-sweep)
算法和名字一樣阱驾,分為兩個(gè)階段:標(biāo)記和清除。標(biāo)記所有需要回收的對(duì)象怪蔑,然后統(tǒng)一回收里覆。這是最基礎(chǔ)的算法,后續(xù)的收集算法都是基于這個(gè)算法擴(kuò)展的缆瓣。
不足:效率低喧枷;標(biāo)記清除之后會(huì)產(chǎn)生大量碎片。效果圖如下:
2. 復(fù)制(Copying)
此算法把內(nèi)存空間劃為兩個(gè)相等的區(qū)域弓坞,每次只使用其中一個(gè)區(qū)域隧甚。垃圾回收時(shí),遍歷當(dāng)前使用區(qū)域渡冻,把正在使用中的對(duì)象復(fù)制到另外一個(gè)區(qū)域中戚扳。此算法每次只處理正在使用中的對(duì)象,因此復(fù)制成本比較小族吻,同時(shí)復(fù)制過(guò)去以后還能進(jìn)行相應(yīng)的內(nèi)存整理帽借,不會(huì)出現(xiàn)“碎片”問(wèn)題珠增。當(dāng)然,此算法的缺點(diǎn)也是很明顯的砍艾,就是需要兩倍內(nèi)存空間蒂教。效果圖如下:
3.標(biāo)記-整理(Mark-Compact)
此算法結(jié)合了“標(biāo)記-清除”和“復(fù)制”兩個(gè)算法的優(yōu)點(diǎn)。也是分兩階段脆荷,第一階段從根節(jié)點(diǎn)開(kāi)始標(biāo)記所有被引用對(duì)象悴品,第二階段遍歷整個(gè)堆,把清除未標(biāo)記對(duì)象并且把存活對(duì)象“壓縮”到堆的其中一塊简烘,按順序排放苔严。此算法避免了“標(biāo)記-清除”的碎片問(wèn)題,同時(shí)也避免了“復(fù)制”算法的空間問(wèn)題孤澎。效果圖如下:
4.分代收集算法
這是當(dāng)前商業(yè)虛擬機(jī)常用的垃圾收集算法届氢。分代的垃圾回收策略,是基于這樣一個(gè)事實(shí):不同的對(duì)象的生命周期是不一樣的覆旭。因此退子,不同生命周期的對(duì)象可以采取不同的收集方式,以便提高回收效率型将。
- 年輕代:是所有新對(duì)象產(chǎn)生的地方寂祥。年輕代被分為3個(gè)部分——Enden區(qū)和兩個(gè)Survivor區(qū)(From和to)當(dāng)Eden區(qū)被對(duì)象填滿時(shí),就會(huì)執(zhí)行Minor GC七兜。并把所有存活下來(lái)的對(duì)象轉(zhuǎn)移到其中一個(gè)survivor區(qū)(假設(shè)為from區(qū))丸凭。Minor GC同樣會(huì)檢查存活下來(lái)的對(duì)象,并把它們轉(zhuǎn)移到另一個(gè)survivor區(qū)(假設(shè)為to區(qū))腕铸。這樣在一段時(shí)間內(nèi)惜犀,總會(huì)有一個(gè)空的survivor區(qū)。經(jīng)過(guò)多次GC周期后狠裹,仍然存活下來(lái)的對(duì)象會(huì)被轉(zhuǎn)移到年老代內(nèi)存空間虽界。通常這是在年輕代有資格提升到年老代前通過(guò)設(shè)定年齡閾值來(lái)完成的。需要注意涛菠,Survivor的兩個(gè)區(qū)是對(duì)稱的莉御,沒(méi)先后關(guān)系,from和to是相對(duì)的俗冻。
- 年老代:在年輕代中經(jīng)歷了N次回收后仍然沒(méi)有被清除的對(duì)象礁叔,就會(huì)被放到年老代中,可以說(shuō)他們都是久經(jīng)沙場(chǎng)而不亡的一代言疗,都是生命周期較長(zhǎng)的對(duì)象晴圾。對(duì)于年老代和永久代颂砸,就不能再采用像年輕代中那樣搬移騰挪的回收算法噪奄,因?yàn)槟切?duì)于這些回收戰(zhàn)場(chǎng)上的老兵來(lái)說(shuō)是小兒科死姚。通常會(huì)在老年代內(nèi)存被占滿時(shí)將會(huì)觸發(fā)Full GC,回收整個(gè)堆內(nèi)存。
- 持久代:用于存放靜態(tài)文件勤篮,比如java類都毒、方法等。持久代對(duì)垃圾回收沒(méi)有顯著的影響碰缔。
分代回收的效果圖如下:
最后講分代账劲,是因?yàn)榉执锷婕傲饲懊鎺追N算法。年輕代:涉及了復(fù)制算法金抡;年老代:涉及了“標(biāo)記-整理(Mark-Sweep)”的算法瀑焦。
總結(jié)
通過(guò)對(duì)以上知識(shí)點(diǎn)的理解;再來(lái)看我的問(wèn)題:
這里就先不說(shuō)那么細(xì)梗肝;我的理解是在JVM啟動(dòng)時(shí)候通過(guò)類裝載器將class加載到JVM中榛瓮;
靜態(tài)變量
public static StringBuffer cachebuffer = new StringBuffer();
cachebuffer 被JVM存儲(chǔ)在方法區(qū);
new StringBuffer(); 這個(gè)對(duì)象被JVM存儲(chǔ)在java堆(Heap)中
后面創(chuàng)建的StringBuffer();也存在java堆(Heap)中巫击;
當(dāng)靜態(tài)變量指向新的對(duì)象的時(shí)候
cachebuffer = new StringBuffer();
之前的5M的StringBuffer對(duì)象是還在內(nèi)存中的禀晓;沒(méi)有立馬被GC回收的;
按照GC收集的機(jī)制:觸發(fā)GC回收年輕代的數(shù)據(jù)是年輕代的堆內(nèi)存滿了坝锰;才會(huì)觸發(fā)粹懒;
如果之前cachebuffer 還沒(méi)被清空(創(chuàng)建新的對(duì)象取代)的時(shí)候就觸發(fā)了年輕代的GC 那么cachebuffer 就會(huì)會(huì)被從Enden區(qū)到survivor區(qū)甚至到Old Memory區(qū)域;
可以想象出這個(gè)代碼是有問(wèn)題的顷级;
后來(lái)又研究了下這個(gè)靜態(tài)變量會(huì)什么時(shí)候被GC回收:
靜態(tài)變量是屬于類的不是屬于實(shí)例對(duì)象的凫乖;所以這個(gè)類存在靜態(tài)變量就會(huì)一直存在;靜態(tài)變量存儲(chǔ)在方法區(qū)弓颈;用于保存類常量以及字符串常量拣凹。注意,這個(gè)區(qū)域不是用于存儲(chǔ)那些從老年代存活下來(lái)的對(duì)象恨豁,按照分代屬于持久代嚣镜;這個(gè)區(qū)域也可能發(fā)生GC。發(fā)生在這個(gè)區(qū)域的GC事件也被算為 Major GC 橘蜜。只不過(guò)在這個(gè)區(qū)域發(fā)生GC的條件非常嚴(yán)苛菊匿,必須符合以下三種條件才會(huì)被回收:
1、所有實(shí)例被回收
2计福、加載該類的ClassLoader 被回收
3跌捆、Class 對(duì)象無(wú)法通過(guò)任何途徑訪問(wèn)(包括反射)
所以一般來(lái)說(shuō) 靜態(tài)變量很少會(huì)被GC回收的;
參考鏈接:(準(zhǔn)確的來(lái)說(shuō)是copy鏈接~下面兩篇寫(xiě)的都很不錯(cuò))
https://zhuanlan.zhihu.com/p/25539690
http://blog.csdn.net/tonytfjing/article/details/44278233