最近學習Python的GC機制時喂链,想到了java的GC返十,忘得差不多了,(⊙﹏⊙)bM治ⅰ洞坑!這里便做一下回顧總結。推薦周志明譯本的《深入理解Java虛擬機》蝇率。
1. Java內(nèi)存模型
1.1 程序計數(shù)器
程序計數(shù)器迟杂,是一塊較小的內(nèi)存空間刽沾,它可以看作當前線程所執(zhí)行的字節(jié)碼的行號指示器。字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值排拷,來獲取下一條需要執(zhí)行的字節(jié)碼指令侧漓,分支、循環(huán)监氢、跳轉(zhuǎn)布蔗、異常處理、線程恢復等基礎功能都需要依賴計數(shù)器來完成浪腐。
這部分的內(nèi)存區(qū)域是線程私有的纵揍。JVM中的多線程是通過線程輪流切換,每個線程在CPU分配的時間片執(zhí)行的方式來實現(xiàn)的牛欢。任何一時刻骡男,每個CPU內(nèi)核都只會執(zhí)行一個線程,線程切換的時候會保存上一個任務的狀態(tài)傍睹,以便下次切換會這個任務時再加載這個任務隔盛。程序計數(shù)器的作用就是在做上下文切換的時候,可以讓程序恢復到正確的位置拾稳。
1.2 Java虛擬機棧
Java虛擬機棧也是線程私有的吮炕,它的生命周期和線程相同。虛擬機描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法在執(zhí)行的時候都會創(chuàng)建一個棧幀访得,用于存儲局部變量表龙亲、操作數(shù)棧、返回值等信息悍抑。每一個方法從調(diào)用直至執(zhí)行完成的過程鳄炉,就雪瑩這一個棧幀在虛擬機棧中入棧到出棧的過程。
通常會粗粒度的把Java內(nèi)存劃分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack)搜骡,這里的棧內(nèi)存講的就是虛擬機棧(局部變量表部分)拂盯。
局部變量存放了編譯器可知的各種基本數(shù)據(jù)類型、引用類型记靡。64位的long和double類型數(shù)據(jù)會占用2個局部變量空間谈竿,其余數(shù)據(jù)類型只占用1個。局部變量表所需要的內(nèi)存空間在編譯期間完成分配摸吠,當進入一個方法時空凸,這個方法在棧中分配多大的空間是確定的,在方法運行期間不會改變局部變量表的大小寸痢。
1.3 本地方法棧
本地方法棧和虛擬機棧的作用是非常類似的呀洲,在HotSpot虛擬機中把這兩部分合到了一起。本地方法棧和虛擬機棧的區(qū)別是:虛擬機棧為虛擬機執(zhí)行Java方法(即字節(jié)碼)服務,而本地方法棧則為虛擬機使用到的native方法服務两嘴。
1.4 Java堆
Java堆是JVM所管理的內(nèi)存中最大的一塊丛楚,它是被所有線程所共享的一塊內(nèi)存區(qū)域族壳。在虛擬機啟動時創(chuàng)建憔辫,此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存仿荆。
Java堆是GC管理的主要區(qū)域贰您。從內(nèi)存回收的角度來看,由于現(xiàn)在基本上都采用分代回收算法拢操,Java堆還可以分為新生代和老年代锦亦,后面小節(jié)會詳細介紹。
Java堆可以處于物理上不連續(xù)的內(nèi)存空間令境,只要邏輯上是連續(xù)的即可杠园。在實現(xiàn)中,既可以實現(xiàn)成固定大小的舔庶,也可以是可擴展的抛蚁。可以通過-Xmx(最大值)和-Xms(最小值)配置惕橙。
1.5 方法區(qū)
方法區(qū)也是各個線程共享的內(nèi)存區(qū)域瞧甩,它用于存儲已被虛擬機加載的類信息、常量弥鹦、靜態(tài)變量肚逸、即時編譯后的代碼等數(shù)據(jù)。雖然JVM規(guī)范把方法區(qū)描述為堆的一部分彬坏,它卻有一個別名(Non-Heap)非堆朦促。
有人稱方法區(qū)為永久代,但其實永久代只是HotSpot虛擬機對方法去這個概念的實現(xiàn)栓始。在JDK1.8务冕,永久代已被移除,用元空間代替混滔,元空間不再使用虛擬機內(nèi)存洒疚,而直接使用本地的系統(tǒng)內(nèi)存。
方法區(qū)中有一部分叫做常量池坯屿,用來存放編譯期生成的各種字面量和符號引用油湖,這部分內(nèi)容在類加載后放入方法區(qū)的常量池中。
1.6 直接內(nèi)存
主要用在NIO中领跛,它提供了一個DirectByteBuffer對象乏德,可以直接直接訪問系統(tǒng)內(nèi)存,可以避免在Java堆和Native堆中來回切換數(shù)據(jù)。
2. 對象創(chuàng)建過程
-
首先虛擬機會檢查常量池中類的信息喊括,如果沒有胧瓜,需要先加載類信息。檢查通過后郑什,JVM將為新生對象分配內(nèi)存府喳,對象所需的內(nèi)存大小在類加載完之后就可以確定,為對象分配空間的任務其實就是將一塊確定大小的內(nèi)存從Java堆中劃分出來蘑拯。
分配內(nèi)存有兩種方式:
- 指針碰撞:假設堆中的內(nèi)存是絕對規(guī)整的钝满,所有使用過的內(nèi)存都放在一遍,空閑的內(nèi)存放在另一邊申窘,中間放著一個指針作為分界點的指示器弯蚜,那么為新對象分配內(nèi)存時只需要將指針向空閑空間的那邊挪動一段與對象大小相等的距離即可
- 空閑列表:假設堆中的內(nèi)存不是規(guī)整的,已使用的內(nèi)存和空閑的內(nèi)存相互交錯剃法,虛擬機就必須維護一個列表碎捺,記錄哪些內(nèi)存時可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給新的對象贷洲,并更新列表上的記錄
內(nèi)存分配完成后收厨,JVM將分配到的內(nèi)存空間都初始化零值
undefined接下來JVM要對對象進行必要的設置,例如對象是哪個類的實例恩脂,如何才能找到類的元數(shù)據(jù)信息帽氓、對象的哈希碼、對象的GC分代年齡俩块,這些信息將保存在對象的對象頭中
undefined下面將是執(zhí)行init黎休,根據(jù)編寫的代碼對對象進行初始化,對象創(chuàng)建完成
3. Java引用類型
Java中將引用分為了四種類型:強引用(Strong Reference)玉凯,軟引用(Soft Reference)势腮,弱引用(Weak Reference),虛引用(Phantom Reference)漫仆。
- 強引用:指的是類似
Object obj=new Object()
這樣顯示聲明的對象引用捎拯,是最普遍存在的引用,只要強引用還在盲厌,GC永遠不會回收掉被引用的對象署照,即使拋出OutOfMemmoryError,使程序終止吗浩。 - 軟引用:用來描述一些還有用但非必需的對象建芙。對于軟引用關聯(lián)的對象,在系統(tǒng)即將發(fā)生OOM錯誤之前懂扼,將會對這些對象進行回收禁荸,如果這次回收還沒有足夠的內(nèi)存右蒲,才會拋出內(nèi)存溢出異常「鲜欤可以使用SoftReference類來實現(xiàn)軟引用瑰妄。可以使用軟引用來構建緩存映砖。
- 弱引用:用來描述非必須對象间坐,優(yōu)先級比軟引用要低,在垃圾收集器工作時啊央,無論當前內(nèi)存是否足夠眶诈,都會回收掉只被弱引用關聯(lián)的對象
- 虛引用:最弱的一種引用關系,一個對象是否有虛引用的存在瓜饥,不會對其生存時間構成影響,也無法通過虛引用來獲取一個對象實例浴骂。為一個對象設置虛引用關聯(lián)的唯一目的就是能在這個對象被收集器回收時收到一個系統(tǒng)通知乓土。
引用類型 | 被垃圾回收時間 | 用途 | 生存時間 |
---|---|---|---|
強引用 | 從來不會 | 對象的一般狀態(tài) | JVM停止運行時終止 |
軟引用 | 在內(nèi)存不足時 | 對象緩存 | 內(nèi)存不足時終止 |
弱引用 | 在垃圾回收時 | 對象緩存 | gc運行后終止 |
虛引用 | Unknown | Unknown | Unknown |
4. 垃圾檢測
垃圾回收(Garbage Collection)是JVM垃圾回收器提供的一種在空閑時間,不定時回收無任何引用對象占用的內(nèi)存空間的一種機制溯警。那么如何判定一個對象已經(jīng)沒有任何引用了呢趣苏?
4.1 引用計數(shù)法
每個對象都有一個引用計數(shù)器,當一個對象被創(chuàng)建初始化后梯轻,該數(shù)字就為1食磕。每當別的地方引用它時,計數(shù)器就會加1喳挑。當引用失效(如超出作用域彬伦,引用指向新的對象等),計數(shù)器就會減1伊诵。如果對象的引用計數(shù)為0单绑,則就會被GC回收。
引用計數(shù)的優(yōu)點是執(zhí)行簡單曹宴、判定效率高搂橙。缺點是無法解決對象之間的循環(huán)引用問題。
# python簡單演示循環(huán)引用
class C():
def __init__(self):
print('內(nèi)存地址是:%s' % str(hex(id(self))))
def test():
c1 = C()
c2 = C()
c1.t = c2 # &c2的引用計數(shù)加1笛坦,變?yōu)?
c2.t = c1 # &c1的引用計數(shù)加1区转,變?yōu)?
del c1 # c1指向的對象引用計數(shù)減1,變?yōu)?
del c2 # c2指向的對象引用數(shù)減1版扩,變?yōu)?
test()
# 即使是將當前對象的引用刪除废离,由于原來c1和c2對象中還引用著彼此,所以引用計數(shù)都不為0资厉,無法被GC回收
4.2 可達性分析算法
Java通常采用可達性分析(Reachability Analysis)來判定對象是否存活的厅缺。它是從離散數(shù)學中的圖論引入的。
基本思路是:先找到一組對象作為GC Roots(根節(jié)點),然后從根節(jié)點開始遍歷湘捎,遍歷結束后诀豁,如果發(fā)現(xiàn)某個對象與GC Roots沒有任何引用鏈相連(即該對象不可達),就證明該對象就是不可用的垃圾對象窥妇,GC會在接下來清除它們舷胜。
即使是循環(huán)引用的對象,如果與根節(jié)點沒有引用鏈活翩,依然會被GC回收烹骨。
以下對象可以作為GC Roots:
- 虛擬機棧(棧幀中的本地變量表)中的引用的對象
- 方法區(qū)中的類靜態(tài)屬性以及常量引用的對象
- 本地方法棧中Native方法引用的對象
- 存活的線程
在使用可達性分析遍歷對象圖的時候,有幾個關鍵點需要注意:
- GC停頓:在整個分析期間不能出現(xiàn)對象引用關系還在不斷變化的情況材泄,所以在GC進行的時候必須要停頓所有的線程(Stop The World)沮焕,停頓的位置稱為安全點(Safepoint),一般在循環(huán)的末尾拉宗、方法返回前峦树、拋出異常的位置等。如果發(fā)生GC的時候旦事,線程還沒有執(zhí)行到一個安全點魁巩,線程繼續(xù)執(zhí)行,到達下一個安全點的的時候暫停姐浮,然后等待GC谷遂;
- finialize():在可達性分析中不可達的對象,真正宣判它的死亡卖鲤,需要兩次標記過程:
- 如果對象在進行可達性分析過后沒有與GC Roots相連肾扰,那么它會被第一次標記并且進行第一次篩選,篩選的條件是此對象有沒有必要執(zhí)行finalize()方法扫尖。當對象沒有覆蓋finalize方法或者已經(jīng)被JVM調(diào)用過白对,該對象會被視作“沒有必要執(zhí)行”。
- 如果對象被判定為有必要finalize()方法换怖,那么這個對象將會被放置在一個F-Queue隊列中甩恼,并在稍后由一條由虛擬機自動建立的、低優(yōu)先級的Finalizer線程去執(zhí)行finalize()方法沉颂。由于finalize()只會被系統(tǒng)調(diào)用一次条摸,這是對象完成“自我救贖”的最后一次機會哑了。稍后GC將對F-Queue中的對象進行第二次小規(guī)模的標記命迈,如果要在finalize()方法中成功拯救自己梧兼,只要在finalize()方法中讓該對象重新引用鏈上的任何一個對象建立關聯(lián)即可叫惊。而如果對象這時還沒有關聯(lián)到任何鏈上的引用,那它就會被回收掉珍逸。
- 建議盡量不要去使用finalize()方法贷掖。
5. 垃圾回收
5.1 標記-清除(Mark-Sweep)
標記-清除算法是最基礎的收集算法七兜。它分為兩個階段:
- 標記:標記階段的任務就是標記出所有需要被回收的對象
- 清除:回收被標記對象所占用的內(nèi)存空間
優(yōu)點:不需要移動對象,僅需要對不存活的對象進行處理钙蒙,在對象存活率較高的場景下極為高效
缺點:
- 效率問題:標記和清除的效率都不高茵瀑,需要維護一張空閑列表
- 空間問題:標記清除后會產(chǎn)生大量不連續(xù)的內(nèi)存碎片,當分配大對象時躬厌,因為找不到足夠的連續(xù)內(nèi)存空間而不得不提前觸發(fā)另一次GC
5.2 標記-整理(Mark-Compact)
與標記-清除法類似马昨,但標記過后不是對可回收對象進行清理, 而是將所有存活的對象都向一段扛施,然后直接清理掉邊界以外的內(nèi)存鸿捧。
優(yōu)點:經(jīng)過整理過后,新對象的分配只需要指針碰撞即可完成疙渣,而且不會再有碎片問題
缺點:需要將所有的對象都拷貝到一個新的地址匙奴,并且更新引用地址,GC停頓較長
5.3 復制(Copying)
該算法的提出是為了解決句柄開銷和內(nèi)存碎片問題昌阿。它將可用內(nèi)存分為大小相等的兩塊區(qū)域饥脑,每次只使用其中一塊。當一塊中的內(nèi)存用完了懦冰,就將還存活的對象復制到另外一塊區(qū)域上面,然后將使用過的內(nèi)存空間一次性清理掉谣沸。
優(yōu)點:
- 標記和復制階可以同時執(zhí)行
- 每次是對整塊半?yún)^(qū)進行回收刷钢,在對象存活率較低的場景下效率較高
- 分配對象時不用考慮碎片問題
缺點:實際可用內(nèi)存縮小為原來的一半
5.4 分代回收(Generational Collection)
JVM中采用的是分代回收,它根據(jù)對象的存活周期將內(nèi)存區(qū)域分為新生代和老年代乳附。
新生代中:對象生命周期短内地,每次GC時都有大批對象死去,只有少量存活赋除,比較適用于復制方法阱缓。
老年代:對象存活時間極長,比較實用于標記-清理或標記-整理方法举农。
Python中也采用分代回收方法荆针,將對象分為0、1颁糟、2代航背,可參照文章了解。
6. Java中的分代回收
JVM中的堆內(nèi)存按照GC的角度可分為新生代和和老年代棱貌,新生代又可以分為三個部分:Eden和兩個Survivor區(qū)(Survivor0玖媚、Survivor1)。
新生代GC:Minor GC婚脱,非常頻繁今魔,回收速度比較快
老年代GC:Major GC勺像,一般會伴隨著Minor GC,對整個堆內(nèi)存做一次GC错森,所以也稱Full GC吟宦,頻次較低,速度較慢
6.1 新生代
幾乎所有新創(chuàng)建的對象都是放在了年輕代问词。新生代在GC時督函,采用的是復制算法,由于新生代中的對象生命周期大都很短激挪,所以并不需要按照1:1的比例來劃分內(nèi)存空間辰狡,而是將內(nèi)存劃分為較大的Eden區(qū),和兩塊較小的Survivor區(qū)垄分,三者的比例一般是8:1:1宛篇,可以通過-XX:SurvivorRatio設置。
大部分對象是在Eden中生成薄湿。GC時大致的過程如下:
- 當創(chuàng)建新的對象時叫倍,如果Eden空間不足時,會觸發(fā)一次Minor GC豺瘤∵壕耄回收時,先將Eden區(qū)的存活對象復制到S0區(qū)
- 當再次觸發(fā)Minor GC時坐求,會將Eden和S0區(qū)存活的對象復制到S1區(qū)蚕泽,清空Eden和S0區(qū)
- 每次Minor GC時,都會對Eden和其中一個Survivor區(qū)域操作桥嗤,將存活的對象放入到另外一個Survivor區(qū)中须妻,如此反復。
- 如果另外一塊Survior區(qū)沒有足夠空間存放上一次Mionr GC下存活的對象泛领,這些對象將存放到老年代(這種稱之為分配擔保)
- 每當對象在Survivor區(qū)經(jīng)歷一次GC存活下來荒吏,它的年齡將加1,如果年齡達到N(一般是15)歲渊鞋,就會移動到老年代中
6.2 老年代
老年代中存在的一般都是生命周期比較長的對象绰更,它的空間也比新生代大很多(一般是2:1),一般采用的標記-整理方法篓像。需要注意的有以下幾點:
如上一小節(jié)所說动知,在新生代長期存活的對象將會被放入到老年代中
大對象(很長的字符串、長數(shù)組等)直接進入老年代员辩,大對象可能導致內(nèi)存還有不少空間時就提前觸發(fā)Minor GC以獲取足夠的連續(xù)空間盒粮,也可以避免在Eden和Survivor區(qū)之間發(fā)生大量的內(nèi)存復制。大對象的閾值可以通過參數(shù)設置
Survivor空間中奠滑,如果相同年齡的對象大小總和丹皱,大于Survivor空間的一半妒穴,年齡大于等于該年齡的對象可以直接進入老年代
當老年代中的空間不足不足以存放即將升入老年代的對象時,會觸發(fā)一次Full GC摊崭。
-
發(fā)生Minor GC之前讼油,由于可能存在大量對象存活的情況(假如100%存活),虛擬機會檢查老年代中剩余空間是否大于新生代所有對象總空間呢簸,如果這個條件成立矮台,Minor GC可以確保是安全的。如果不成立根时,虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗瘦赫。如果允許,那么會繼續(xù)檢查老年代中最大的可用連續(xù)空間蛤迎,是否大于歷次晉升到老年代對象的平均大小确虱。如果大于,將嘗試Minor GC替裆。如果小于校辩、或者設置中不允許擔保失敗,或者在Minor GC時擔保失敗辆童,則會發(fā)生一次Full GC宜咒。
總結
以上粗淺的介紹了JAVA中的GC機制。由于每次GC都會造成GC停頓把鉴,所以在開發(fā)過程中荧呐,盡可能減少GC的開銷。比如盡可能不要顯式調(diào)用System.gc()纸镊、字符串拼接時盡量使用StringBuffer、能使用基本類型的地方不要使用包裝類等概疆。