JAVA虚拟机之垃圾收集与内存分配策略
最近再看《深入理解JAVA虛擬機(jī)》周志明寫的第二版。現(xiàn)將學(xué)習(xí)筆記分享出來,方便日后復(fù)習(xí),理解有誤的地方歡迎指正!
1、運(yùn)行時(shí)數(shù)據(jù)區(qū):
程序計(jì)數(shù)器:一塊較小的內(nèi)存空間,保存當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。
java虛擬機(jī)棧:生命周期與線程相同,每個(gè)方法在執(zhí)行的同事都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。這個(gè)區(qū)域存在2種異常情況:①線程請(qǐng)求的棧深度大于虛擬機(jī)允許的深度,拋出StackOverflowError.②虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展,如果擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存,拋出OutOfMemoryError.
本地方法棧:虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法服務(wù),本地方法棧為虛擬機(jī)使用到的Navtive方法服務(wù)。本地方法棧也會(huì)拋出StackOverflowError和OutOfMemoryError異常。
java堆:在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建,此內(nèi)存區(qū)的唯一目的就是存放對(duì)象實(shí)例,別名GC堆
方法區(qū):用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。別名非堆
運(yùn)行時(shí)常量池:是方法區(qū)的一部分,用于存放編譯期生成的各種字面量和符號(hào)引用
直接內(nèi)存:并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分。避免在Java堆和Native堆種來回復(fù)制數(shù)據(jù)。
2、hotspot虛擬機(jī)對(duì)象
對(duì)象的創(chuàng)建:虛擬機(jī)遇到new時(shí):①在常量池中定位這個(gè)類的符號(hào)引用,并檢查這個(gè)符號(hào)代表的類是否被加載過②虛擬機(jī)為新生對(duì)象分配內(nèi)存空間③采用CAS保證內(nèi)存分配的原子性,或每個(gè)線程在java堆中預(yù)先分配一小塊內(nèi)存(本地線程分配緩沖)④保存類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的GC分代年齡信息設(shè)置
對(duì)象的內(nèi)存布局:分為3塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)象填充
對(duì)象頭:①存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)的數(shù)據(jù)②保存類型指針(對(duì)象指向它的類的元數(shù)據(jù)的指針)
實(shí)例數(shù)據(jù):對(duì)象真正存儲(chǔ)的有效信息
對(duì)象填充:不是必然存在的,沒有特別的含義,僅僅起著占位符的作用。
對(duì)象的訪問定位:主流的訪問方式有使用句柄和直接指針兩種(詳細(xì)見書)
內(nèi)存泄漏or內(nèi)存溢出?
異常實(shí)踐:
堆溢出:-Xms20m -Xmx20m
棧溢出:-Xss128k
? ? ?注:-Xss設(shè)置棧內(nèi)存:表示給每個(gè)線程分配得大小,值設(shè)置得越大,多線程時(shí)oom的可能性越高。
方法區(qū)和運(yùn)行時(shí)常量池溢出:在JDK1.6版本,常量池被分配在永久帶。1.7常量池屬于方法區(qū)的一部分
3、垃圾收集器與內(nèi)存分配策略
①判斷對(duì)象是否還存活(不再被任何途徑使用的對(duì)象)
引用計(jì)數(shù)算法:給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每次被引用+1,引用失效-1,計(jì)數(shù)器為0時(shí)就是不再被使用的,進(jìn)行g(shù)c。優(yōu)點(diǎn):效率高,但是主流的虛擬機(jī)沒有選用引用計(jì)數(shù)法來管理內(nèi)存,因?yàn)楹茈y解決對(duì)象和之間互相引用的問題。
可達(dá)性分析算法:通過GC Roots對(duì)象作為起始節(jié)點(diǎn),向下搜索(搜索走過的路徑稱為引用鏈)對(duì)象,從GC Roots到這個(gè)對(duì)象不可達(dá)時(shí),證明這個(gè)對(duì)象是不可用的。
注:在java中,可作為GC Roots的對(duì)象包括:
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象
方法區(qū)中類靜態(tài)屬性引用的對(duì)象
方法區(qū)中常量引用的對(duì)象
本地方法棧中JNI(即一般說的Native方法)引用的對(duì)象
缺點(diǎn):可達(dá)性算法必須保證在一致性的快照中進(jìn)行,避免分析過程中對(duì)象的引用關(guān)系還在不斷發(fā)生著變化。這點(diǎn)導(dǎo)致GC進(jìn)行時(shí)必須停頓所有java線程(stop the world)
生存還是死亡(是否要在本次被回收):
不可達(dá)的對(duì)象會(huì)經(jīng)歷2次標(biāo)記的過程:
<1>第一次標(biāo)記且篩選(篩選條件:是否有必要執(zhí)行finalize()方法)
<2>如果有必要執(zhí)行,將對(duì)象放到F-Queue隊(duì)列之中,稍后GC會(huì)對(duì)F-Queue隊(duì)列中的對(duì)象進(jìn)行第二次標(biāo)記。如果次是對(duì)象與引用鏈上的對(duì)象關(guān)聯(lián),這個(gè)對(duì)象將會(huì)被移出“即將回收的集合”,否則就會(huì)被執(zhí)行GC
②垃圾收集算法
標(biāo)記-清除算法:先標(biāo)記需要回收的對(duì)象,然后統(tǒng)一清除
缺點(diǎn):效率不高(2個(gè)過程的效率都不高)、空間問題(清除之后會(huì)產(chǎn)生大量碎片)
復(fù)制算法:將可用內(nèi)存按容量大小分為相等的2塊,每次只使用其中的一塊,當(dāng)這一塊的內(nèi)存用完了,就將還存活的對(duì)象復(fù)制到另一塊上,然后把已用的內(nèi)存空間一次性清理掉。(現(xiàn)在商業(yè)虛擬機(jī)都采用這種方式來回收新生代)
通常將內(nèi)存分為一塊較大的Eden空間和2塊較小的survivor空間,每次使用Enden和其中一塊survivor。
E+S1->S2 ?E+S2->S1 ? HotSpot默認(rèn)E和S的大小比例是8:1
如果另一塊Survivor空間沒有足夠空間存放上一次新生代手機(jī)下來的存活對(duì)象,就通過分配擔(dān)保機(jī)制進(jìn)入老年代。
標(biāo)記-整理算法:先標(biāo)記需要回收的對(duì)象,讓所有存活的對(duì)象都向一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。
分代收集算法:新生代只有少量存活,選用復(fù)制算法。老年代存活率高,沒有額外空間分配擔(dān)保,選用標(biāo)記整理算法。
③HotSpot算法實(shí)現(xiàn)
枚舉根節(jié)點(diǎn):HotSpot引用一組OopMap的數(shù)據(jù)結(jié)構(gòu),在類加載完成的時(shí)候,就把對(duì)象內(nèi)什么偏移量上是什么類型的數(shù)據(jù)計(jì)算出來。在JIT編譯過程中,也會(huì)在特定的位置記錄下棧和寄存器中哪些位置是引用。
注:由于可達(dá)性算法對(duì)時(shí)間的敏感性,即使號(hào)稱(幾乎)不會(huì)發(fā)生停頓的CMS收集器中,枚舉根節(jié)點(diǎn)也是要必須停頓的。
安全點(diǎn):HotSpot并沒有為每一條指令都生成OopMap,只是在安全點(diǎn)才記錄這些信息,也就是說程序只有到達(dá)安全點(diǎn)時(shí)才能停頓下來開始GC.安全點(diǎn)的選定標(biāo)準(zhǔn)“是否具有讓程序長時(shí)間執(zhí)行的特征”。例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等具有長時(shí)間執(zhí)行的功能的指令才會(huì)產(chǎn)生安全點(diǎn)。
如何讓GC發(fā)生時(shí),所有線程都跑到最近的安全點(diǎn)上停下來?
搶先式中斷:把所有線程都中斷,如果又不在安全點(diǎn)上的就恢復(fù)線程,讓它跑到安全點(diǎn)上。(幾乎沒有虛擬機(jī)使用這種方式)
主動(dòng)式中斷:不直接對(duì)線程操作,在安全點(diǎn)和創(chuàng)建對(duì)象需要分配內(nèi)存的地方設(shè)一個(gè)標(biāo)志,各線程主動(dòng)輪詢這個(gè)標(biāo)志,發(fā)現(xiàn)中斷標(biāo)識(shí)時(shí)自己中斷掛起。
如果程序沒有分配CPU時(shí)間(sleep或blocked),就無法響應(yīng)JVM的中斷請(qǐng)求。此時(shí)需要安全區(qū)域來解決。
安全區(qū)域:是指在一段代碼片段中,引用關(guān)系不會(huì)發(fā)生變化,在這個(gè)區(qū)域中的任意地方開始GC都是安全的。在線程執(zhí)行到安全區(qū)域時(shí),先給自己打個(gè)標(biāo)識(shí),如果這段時(shí)間JVM要發(fā)起GC,就不需要管已經(jīng)打標(biāo)識(shí)的線程。在線程要離開安全區(qū)域時(shí),要先檢查
系統(tǒng)是否已經(jīng)完成了根節(jié)點(diǎn)枚舉(或者整個(gè)GC過程),如果沒完成就需要等待接受可以離開安全區(qū)域的信號(hào)。
④垃圾收集器
serial、parnew、parallel scavenge、serial old、parallel old、cms收集器
并行:指多條垃圾收集線程并行工作,但此時(shí)用戶線程仍處于等待狀態(tài)
并發(fā):指用戶線程與垃圾收集線程同時(shí)執(zhí)行(但不一定是并行的,可能會(huì)交替執(zhí)行),用戶程序在繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行在另一個(gè)CPU上。
CMS收集器:基于“標(biāo)記-清除”算法實(shí)現(xiàn)的。
<1>初始標(biāo)記:stop the world,標(biāo)記GC Root能直接關(guān)聯(lián)到的對(duì)象,速度快
<2>并發(fā)標(biāo)記:進(jìn)行g(shù)c roots tracing
<3>重新標(biāo)記:stop the world,標(biāo)記并發(fā)標(biāo)記期間程序運(yùn)行導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄。這個(gè)階段的停頓時(shí)間一般比初始標(biāo)記階段稍長,但是遠(yuǎn)比并發(fā)標(biāo)記時(shí)間短。
<4>并發(fā)清除:
參數(shù)-XX:CMSInitiatingOccupancyFraction老年代內(nèi)存空間百分比閾值觸發(fā)CMS收集器進(jìn)行GC
參數(shù)-XX:UseCMSCompactAtFullCollection開關(guān)(默認(rèn)開始)在CMS要開始Full GC之前開啟內(nèi)存碎片的合并整理
參數(shù)-XX:CMSFullGCsBeforeCompaction用于設(shè)置執(zhí)行多少次不壓縮的Full GC后執(zhí)行一次壓縮(默認(rèn)0,表示每次進(jìn)入Full GC都進(jìn)行碎片整理)
G1收集器特點(diǎn):
并行與并發(fā):利用多cpu多核的硬件優(yōu)勢(shì)
分代收集:保留分代收集的概念
空間整合:G1運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片
可預(yù)測(cè)的停頓:G1在后臺(tái)維護(hù)一個(gè)優(yōu)先級(jí)列表,優(yōu)先回收價(jià)值最大的Region(garage-first)提高回收效率,可以建立可預(yù)測(cè)的停頓時(shí)間模型
G1收集器運(yùn)作步驟:初始標(biāo)記、并發(fā)標(biāo)記、最終標(biāo)記、篩選回收
理解GC日志:
①前面的數(shù)字:代表了GC發(fā)生的時(shí)間,從java虛擬機(jī)啟動(dòng)以來經(jīng)過的秒數(shù)
②GC日志開頭的 [GC 和 [Full GC 代表停頓類型,如果有Full 代表發(fā)生了stop the world
③[DefNew:不同收集器定義的新生代、老年代、永久代名字,表示GC發(fā)生的區(qū)域
④方括號(hào)內(nèi)部:3324K -> 152K(3712K) ?GC前該內(nèi)存區(qū)域已使用容量->GC后改內(nèi)存區(qū)域已使用容量(該內(nèi)存區(qū)域總?cè)萘?#xff09;
方括號(hào)之外:GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆總?cè)萘?#xff09;
⑤0.0025825 secs 表示該內(nèi)存區(qū)域GC所占用的時(shí)間,單位是秒
垃圾收集器參數(shù)總結(jié):
?
內(nèi)存分配與回收策略:
①對(duì)象優(yōu)先在Eden分配
②大對(duì)象直接進(jìn)入老年代:-XX:PretenureSizeThreshold參數(shù)可以設(shè)置令大于這個(gè)參數(shù)值的對(duì)象直接在老年代分配。
③長期存活的對(duì)象將進(jìn)入老年代:-XX:MaxTenuringThreshold參數(shù)可以設(shè)置對(duì)象在新生代存活超過多少次進(jìn)入老年代。
④動(dòng)態(tài)對(duì)象年齡判定:如果survivor空間中相同年齡所有對(duì)象大小的綜合大于survivor空間的一半,年齡大于等于該年齡的對(duì)象就可以直接進(jìn)入老年代。
⑤空間分配擔(dān)保:發(fā)生Minor GC之前會(huì)檢查老年代的空間是否大于新生代,如果比新生代大就進(jìn)行Minor GC,如果比新生代小就看HandlePromotionFailure設(shè)置是否允許冒險(xiǎn),如果允許冒險(xiǎn)就檢查老年代剩余的連續(xù)內(nèi)存大小是否比歷代進(jìn)入老年代的內(nèi)存大,符合就Minor GC,否則就Full GC。(見下面流程圖)
注:
新生代GC(Minor GC):指發(fā)生在新生代的GC。執(zhí)行頻繁,回收速度較快。
老年代GC(Major GC/Full GC):指發(fā)生在老年代的GC,老年代GC一般比新生代GC慢10倍
?
?
總結(jié)
以上是生活随笔為你收集整理的JAVA虚拟机之垃圾收集与内存分配策略的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分布式系统Lease机制
- 下一篇: 分布式系统Quorum机制