關(guān)于JVM翻越內(nèi)存管理的墻
對(duì)于Java
程序員來說,在虛擬機(jī)自動(dòng)內(nèi)存管理機(jī)制的幫助下,不再需要為每一個(gè)new操作
去寫配對(duì) 的delete/free
代碼釋放內(nèi)存,也由此不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出問題。但凡事都有兩面性,由虛擬機(jī)管理內(nèi)存看起來一切都很美好,但也正是因?yàn)榘芽刂苾?nèi)存的權(quán)力交給了Java虛擬機(jī)
,一旦出現(xiàn)內(nèi)存泄漏和溢出方面的問題,就不得不從Java虛擬機(jī)
角度上去排查問題。因此我們需要了解虛擬機(jī)是怎樣使用內(nèi)存的,才能準(zhǔn)確的定位到錯(cuò)誤,從而正確的解決問題。
主要內(nèi)容:
- JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)域
- JVM垃圾回收機(jī)制
JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)域
Java虛擬機(jī)
在執(zhí)行Java程序
的過程中會(huì)把它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,有的區(qū)域隨著虛擬機(jī)進(jìn)程的啟動(dòng)而一直存在,有些區(qū)域則是依賴用戶線程的啟動(dòng)和結(jié)束而建立和銷毀。
線程私有內(nèi)存:
由于
JVM多線程
是通過線程輪流切換、分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的,在任何一個(gè)確定的時(shí)刻,一個(gè)處理器(對(duì)于多核處理器來說是一個(gè)內(nèi)核)都只會(huì)執(zhí)行一條線程中的指令。因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每條線程都需要有一個(gè)獨(dú)立的程序計(jì)數(shù)器,各條線程之間計(jì)數(shù)器互不影響,獨(dú)立存儲(chǔ),我們稱這類內(nèi)存區(qū)域?yàn)?ldquo;線程私有”的內(nèi)存。
程序計(jì)數(shù)器
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。 它是程序控制流的指示器,分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個(gè)計(jì)數(shù)器來完成。
字節(jié)碼解釋器工作時(shí)就是通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。
如果線程正在執(zhí)行的是一個(gè)Java
方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址。
如果正在執(zhí)行的是本地(Native)方法,這個(gè)計(jì)數(shù)器值則應(yīng)為空。
Java虛擬機(jī)棧
Java虛擬機(jī)棧
描述的是Java方法
執(zhí)行的線程內(nèi)存模型,它也是線程私有內(nèi)存區(qū)域,生命周期和線程一樣。
棧楨
每個(gè)方法被執(zhí)行的時(shí)候,Java虛擬機(jī)
都會(huì)同步創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)連接、方法出口等信息。每一個(gè)方法被調(diào)用直至執(zhí)行完畢的過程,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中從入棧到出棧的過程。
1.局部變量表
局部變量表存放了編譯期可知的:基本數(shù)據(jù)類型、對(duì)象引用、和returnAddress
類型(指向了一條字節(jié)碼指令的地址)
局部變量表中的存儲(chǔ)空間以局部變量槽表示。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在棧幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大?。ㄟ@里說的“大小”是指變量槽的數(shù)量,一個(gè)變量槽多大是由具體虛擬機(jī)實(shí)現(xiàn)的)
2.異常情況
1.StackOverflowError異常:線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度
2.OutOfMemoryError異常:Java虛擬機(jī)棧容量可以動(dòng)態(tài)擴(kuò)展,當(dāng)棧擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存。 在HotSpot
虛擬機(jī)上是不會(huì)由于虛擬機(jī)棧無法擴(kuò)展而導(dǎo)致OutOfMemoryError
異常。只要線程申請(qǐng)棧空間成功了就不會(huì)有OOM
,但是如果申請(qǐng)時(shí)就失敗,仍然是會(huì)出現(xiàn)OOM
異常的。
本地方法棧
與虛擬機(jī)棧所發(fā)揮的作用是非常相似的。本地方法棧則是為虛擬機(jī)使用到的本地(Native)方法服務(wù)。
HotSpot虛擬機(jī)直接就把本地方法棧和虛擬機(jī)棧合二為一
Java堆
Java堆
是虛擬機(jī)所管理的內(nèi)存中最大的一塊,被所有線程共享的一塊內(nèi)存區(qū)域。 Java堆
是垃圾收集器管理的內(nèi)存區(qū)域。所以也經(jīng)常被稱為GC堆
Java堆
會(huì)在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,Java
世界里“幾乎”所有的對(duì)象實(shí)例都在這里分配內(nèi)存。
從回收內(nèi)存的角度看,由于現(xiàn)代垃圾收集器大部分都是基于分代收集理論設(shè)計(jì)的,所以Java堆中經(jīng)常會(huì)出現(xiàn)“新生代”“老年代”“永久代”“Eden空間”“From Survivor空 間”“To Survivor空間”等名詞。
在之前(以G1收集器的出現(xiàn)為分界),作為業(yè)界絕對(duì)主流的HotSpot虛擬機(jī),它內(nèi)部的垃圾收集器全部都基于“經(jīng)典分代” 來設(shè)計(jì),需要新生代、老年代收集器搭配才能工作,在這種背景下,上述說法還算是不會(huì)產(chǎn)生太大歧義。但是到了今天,垃圾收集器技術(shù)與十年前已不可同日而語,HotSpot里面也出現(xiàn)了不采用分代設(shè)計(jì)的新垃圾收集器,再按照上面的提法就有很多需要商榷的地方了。
分配緩沖區(qū)TLAB(Thread Local Allocation Buffer)
如果從分配內(nèi)存的角度看,所有線程共享的Java堆中可以劃分出多個(gè)線程私有的分配緩沖區(qū) (Thread Local Allocation Buffer,TLAB)。
無論如何劃分,都不會(huì)改變Java堆
中存儲(chǔ)內(nèi)容的共性,無論是哪個(gè)區(qū)域,存儲(chǔ)的都只能是對(duì)象的實(shí)例,將Java堆
細(xì)分的目的只是為了更好地回收內(nèi)存,或者更快地分配內(nèi)存。
Java堆的大小設(shè)定
Java堆
既可以被實(shí)現(xiàn)成固定大小的,也可以是可擴(kuò)展的,不過當(dāng)前主流的Java虛擬機(jī)
都是按照可擴(kuò)展來實(shí)現(xiàn)的(通過參數(shù)-Xmx
和-Xms
設(shè)定)。如果在Java堆
中沒有內(nèi)存完成實(shí)例分配,并且堆也無法再擴(kuò)展時(shí),Java虛擬機(jī)
將會(huì)拋出OutOfMemoryError
異常。
方法區(qū)
方法區(qū)別名叫作“非堆”。它用于存儲(chǔ)已被虛擬機(jī)加載的類型信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼緩存等數(shù)據(jù)。是各個(gè)線程共享的內(nèi)存區(qū)域。
很多人都更愿意把方法區(qū)稱呼為“永久代”(PermanentGeneration),或?qū)烧呋鞛橐徽?。本質(zhì)上這兩者并不是等價(jià)的。因?yàn)閮H僅是當(dāng)時(shí)的HotSpot虛擬機(jī)設(shè)計(jì)團(tuán)隊(duì)選擇把收集器的分代設(shè)計(jì)擴(kuò)展至方法區(qū),或者說使用永久代來實(shí)現(xiàn)方法區(qū)而已,這樣使得 HotSpot的垃圾收集器能夠像管理Java堆一樣管理這部分內(nèi)存,省去專門為方法區(qū)編寫內(nèi)存管理代碼的工作。
相對(duì)Java堆
而言,垃圾收集行為在這個(gè)區(qū)域的確是比較少出現(xiàn)的,但并非數(shù)據(jù)進(jìn)入了方法區(qū)就永久”存在了。 這區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載,一般來說這個(gè)區(qū)域的回收效果比較難令人滿意,尤其是類型的卸載,條件相當(dāng)苛刻,但是這部分區(qū)域的回收有時(shí)又確實(shí)是必要的。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池是方法區(qū)的一部分。Class文件
中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池表,用于存放編譯期生成的各種字面量與符號(hào)引用,在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。
運(yùn)行時(shí)常量池相對(duì)于Class文件常量池
的另外一個(gè)重要特征是具備動(dòng)態(tài)性,Java語言
并不要求常量一定只有編譯期才能產(chǎn)生,也就是說,并非預(yù)置入Class文件中常量池
的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可以將新的常量放入池中,這種特性被開發(fā)人員利用得比較多的便是String類的 intern()
方法。
Java中,直接使用雙引號(hào)聲明出來的
String
對(duì)象會(huì)直接存儲(chǔ)在常量池中。不是用雙引號(hào)聲明的String
對(duì)象,可以使用String
提供的intern
方法。
intern
方法:如果字符串常量池中已經(jīng)包含一個(gè)等于此String對(duì)象
的字符串,則返回代表池中這個(gè)字符串的String對(duì)象
的引用;否則,會(huì)將此String對(duì)象
包含的字符串添加 到常量池中,并且返回此String對(duì)象
的引用。
小結(jié)
整理下上面介紹的JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)域:
JVM垃圾回收機(jī)制
上面介紹了程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧都是線程私有區(qū)域,這三個(gè)區(qū)域隨線程而生,隨線程而滅。 在這幾個(gè)區(qū)域內(nèi)就不需要過多考慮如何回收的問題,當(dāng)方法結(jié)束或者線程結(jié)束時(shí),內(nèi)存自然就跟隨著回收了。
比如棧中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作。每一個(gè)棧幀中分配多少內(nèi)存基本上是在類結(jié)構(gòu)確定下來時(shí)就已知的(盡管在運(yùn)行期會(huì)由即時(shí)編譯器進(jìn)行一些優(yōu)化,但在基于概念模型的討論里,大體上可以認(rèn)為是編譯期可知的),因此這幾個(gè)區(qū)域的內(nèi)存分配和回收都具備確定性。
但是Java堆
和方法區(qū)這兩個(gè)區(qū)域則有著很顯著的不確定性:
1.一個(gè)接口的多個(gè)實(shí)現(xiàn)類需要的內(nèi)存可能會(huì)不一樣,一個(gè)方法所執(zhí)行的不同條件分支所需要的內(nèi)存也可能不一樣,只有處于運(yùn)行期間,我們才能知道程序究竟會(huì)創(chuàng)建哪些對(duì)象,創(chuàng)建多少個(gè)對(duì)象,這部分內(nèi)存的分配和回收是動(dòng)態(tài)的。垃圾收集器所關(guān)注的正是這部分內(nèi)存該如何管理。
2.方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:廢棄的常量和不再使用的類型?;厥諒U棄常量與回收 Java堆
中的對(duì)象非常類似。
比如已經(jīng)沒有任何字符串對(duì)象引用常量池中的某常量,且虛擬機(jī)中也沒有其他地方引用這個(gè)字面量。如果在這時(shí)發(fā)生內(nèi)存回收,而且垃圾收集器判斷確有必要的話,該常量就將會(huì)被系統(tǒng)清理出常量池。常量池中其他類(接口)、方法、字段的符號(hào)引用也與此類似。
方法區(qū)垃圾收集的“性價(jià)比”通常也是比較低的:在Java堆中,尤其是在新生代中,對(duì)常規(guī)應(yīng)用進(jìn)行一次垃圾收集通??梢曰厥?0%至99%的內(nèi)存空間,相比之下,方法區(qū)回收囿于苛刻的判定條件,其區(qū)域垃圾收集的回收成果往往遠(yuǎn)低于此。
判斷對(duì)象存活
垃圾回收的是死亡的對(duì)象,所以在回收前要做的事確定這個(gè)對(duì)象是否還存活。判斷對(duì)象存活的方式主流的有兩種算法:引用計(jì)數(shù)算法和可達(dá)性分析算法。
引用計(jì)數(shù)算法
在對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加一;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減一。任何時(shí)刻計(jì)數(shù)器為零的對(duì)象就是不可能再被使用的。
該算法的缺點(diǎn)是:當(dāng)兩個(gè)對(duì)象互相引用,會(huì)導(dǎo)致無法回收;因?yàn)榛ハ嘁弥鴮?duì)方,導(dǎo)致它們的引用計(jì)數(shù)都不為零,引用計(jì)數(shù)算法也就無法回收它們。
引用計(jì)數(shù)算法(Reference Counting)雖然占用了一些額外的內(nèi)存空間來進(jìn)行計(jì)數(shù),但 它的原理簡(jiǎn)單,判定效率也很高,在大多數(shù)情況下它都是一個(gè)不錯(cuò)的算法。也有一些比較著名的應(yīng)用 案例,例如微軟COM(Component Object Model)技術(shù)、使用ActionScript 3的FlashPlayer、Python語言以及在游戲腳本領(lǐng)域得到許多應(yīng)用的Squirrel中都使用了引用計(jì)數(shù)算法進(jìn)行內(nèi)存管理。但是,在Java 領(lǐng)域,至少主流的Java虛擬機(jī)里面都沒有選用引用計(jì)數(shù)算法來管理內(nèi)存。
可達(dá)性分析算法
當(dāng)前主流的商用程序語言(Java、C#,Lisp)的內(nèi)存管理子系統(tǒng),都是通過可達(dá)性分析(Reachability Analysis)算法來判定對(duì)象是否存活的。
該算法的基本思路就是通過一系列稱為“GC Roots”的根對(duì)象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開始,根據(jù)引用關(guān)系向下搜索,搜索過程所走過的路徑稱為“引用鏈”(Reference Chain),如果某個(gè)對(duì)象到GC Roots
間沒有任何引用鏈相連, 或者用圖論的話來說就是從GC Roots
到這個(gè)對(duì)象不可達(dá)時(shí),則證明此對(duì)象是不可能再被使用的。
其中GC Root
的對(duì)象有很多種,常見的有:
- 在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象,譬如各個(gè)線程被調(diào)用的方法堆棧中使用到的參數(shù)、局部變量、臨時(shí)變量等。
- 在方法區(qū)中類靜態(tài)屬性引用的對(duì)象,譬如
Java
類的引用類型靜態(tài)變量。 - 在方法區(qū)中常量引用的對(duì)象,譬如字符串常量池(String Table)里的引用。
- 在本地方法棧中JNI(即通常所說的Native方法)引用的對(duì)象。
- 所有被同步鎖(synchronized)持有的對(duì)象
幾種引用方式
無論是通過引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量,還是通過可達(dá)性分析算法判斷對(duì)象是否引用鏈可達(dá),判定對(duì)象是否存活都和“引用”離不開關(guān)系。
根據(jù)引起的強(qiáng)度從強(qiáng)到弱排序:
- 強(qiáng)引用:強(qiáng)引用是我們最常用的,在程序代碼之中普遍存在的引用賦值,即類似“Object obj=new Object()”這種引用關(guān)系。無論任何情況下,只要強(qiáng)引用關(guān)系還存在,垃圾收集器就永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象。
- 軟引用:描述一些還有用,但非必須的對(duì)象。只被軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi)存溢出異常前,會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收,如果這次回收還沒有足夠的內(nèi)存, 才會(huì)拋出內(nèi)存溢出異常。
- 弱引用:用來描述那些非必須對(duì)象,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象只能生存到下一次垃圾收集發(fā)生為止。當(dāng)垃圾收集器開始工作,無論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象。
- 虛引用:也稱為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無法通過虛引用來取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的只是為了能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。
垃圾回收算法
標(biāo)記清除算法
算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,也可以反過來,標(biāo)記存活的對(duì)象,統(tǒng)一回收所有未被標(biāo)記的對(duì)象。
缺點(diǎn):
- 執(zhí)行效率不穩(wěn)定,如果
Java堆
中包含大量對(duì)象,而且其中大部分是需要被回收的,這時(shí)必須進(jìn)行大量標(biāo)記和清除的動(dòng)作,導(dǎo)致標(biāo)記和清除兩個(gè)過程的執(zhí)行效率都隨對(duì)象數(shù)量增長而降低。 - 內(nèi)存空間的碎片化問題,標(biāo)記、清除之后會(huì)產(chǎn)生大 量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致當(dāng)以后在程序運(yùn)行過程中需要分配較大對(duì)象時(shí)無法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。
標(biāo)記復(fù)制算法
它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對(duì)象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
優(yōu)點(diǎn):
解決標(biāo)記清除法的缺點(diǎn)。每次都是對(duì)整個(gè)半?yún)^(qū)進(jìn)行回收,不用考慮內(nèi)存碎片浪費(fèi)。
缺點(diǎn):
缺陷在于將可用內(nèi)存縮小為了原來的一半,空間浪費(fèi)未免太多了一點(diǎn)。
如果內(nèi)存中多數(shù)對(duì)象都是存活的,這種算法將會(huì)產(chǎn)生大量的內(nèi)存間復(fù)制的開銷。
現(xiàn)在的商用Java虛擬機(jī)大多都優(yōu)先采用了這種收集算法去回收新生代。
新生代中的對(duì)象有98%熬不過第一輪收集。因此并不需要按照1∶1的比例來劃分新生代的內(nèi)存空間。HotSpot虛擬機(jī)的Serial、ParNew等新生代收集器均采用了這種策略來設(shè) 計(jì)新生代的內(nèi)存布局。Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的 Survivor空間,每次分配內(nèi)存只使用Eden和其中一塊Survivor。發(fā)生垃圾搜集時(shí),將Eden和Survivor中仍 然存活的對(duì)象一次性復(fù)制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空 間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8∶1,也即每次新生代中可用內(nèi)存空間為整個(gè)新 生代容量的90%(Eden的80%加上一個(gè)Survivor的10%),只有一個(gè)Survivor空間,即10%的新生代是會(huì) 被“浪費(fèi)”的。
標(biāo)記整理法
該算法讓所有存活的對(duì)象都向內(nèi)存空間一端移動(dòng),然后直接清理掉邊界以外的內(nèi)存。
優(yōu)點(diǎn):不會(huì)存在標(biāo)記整理內(nèi)存浪費(fèi)的問題。
缺點(diǎn):復(fù)制收集算法在對(duì)象存活率高的情況下就會(huì)出現(xiàn)復(fù)制操作,移動(dòng)操作多,效率會(huì)變低。
標(biāo)記清除法和標(biāo)記整理法的選擇是一種權(quán)衡:
標(biāo)記整理法,通過移動(dòng)存活對(duì)象,尤其是在老年代這種每次回收都有大量對(duì)象存活區(qū)域,移動(dòng)存活對(duì)象并更新所有引用這些對(duì)象的地方將會(huì)是一種極為負(fù)重的操作,而且這種對(duì)象移動(dòng)操作必須**全程暫停用戶應(yīng)用程序(Stop The World)**才能進(jìn)行。
如果跟標(biāo)記-清除算法那樣完全不考慮移動(dòng)和整理存活對(duì)象的話,彌散于堆中的存活對(duì)象導(dǎo)致的空間碎片化問題就只能依賴更為復(fù)雜的內(nèi)存分配器和內(nèi)存訪問器來解決。譬如通過“分區(qū)空閑分配鏈表”來解決內(nèi)存分配問題(計(jì)算機(jī)硬盤存儲(chǔ)大文件就不要求物理連續(xù)的磁盤空間,能夠在碎片化的硬盤上存儲(chǔ)和訪問就是通過硬盤分區(qū)表實(shí)現(xiàn)的)。內(nèi)存的訪問是用戶程序最頻繁的操作,假如在這個(gè)環(huán)節(jié)上增加了額外的負(fù)擔(dān),勢(shì)必會(huì)直接影響應(yīng)用程序的吞吐量。
基于以上兩點(diǎn),是否移動(dòng)對(duì)象都存在弊端,移動(dòng)則內(nèi)存回收時(shí)會(huì)更復(fù)雜,不移動(dòng)則內(nèi)存分配時(shí)會(huì)更復(fù)雜。從垃圾收集的停頓時(shí)間來看,不移動(dòng)對(duì)象停頓時(shí)間會(huì)更短,甚至可以不需要停頓,但是從整個(gè)程序的吞吐量來看,移動(dòng)對(duì)象會(huì)更劃算。
即使不移動(dòng)對(duì)象會(huì)使得收集器的效率提升一些,但因內(nèi)存分配和訪問相比垃圾收集頻率要高得多,這部分的耗時(shí)增加,總吞吐量仍然是下降的。
HotSpot虛擬機(jī)里面關(guān)注吞吐量的Parallel Scavenge收集器是基于標(biāo)記-整理算法的,而關(guān)注延遲的CMS收集器則是基于標(biāo)記-清除算法的,
為了平衡二者的弊端,就有一種中和的方式。讓虛擬機(jī)平時(shí)多數(shù)時(shí)間都采用標(biāo)記-清除算法,暫時(shí)容忍內(nèi)存碎片的存在,直到內(nèi)存空間的碎片化程度已經(jīng)大到影響對(duì)象分配時(shí),再采用標(biāo)記-整理算法收集一次,以獲得規(guī)整的內(nèi)存空間。比如基于標(biāo)記-清除算法的CMS收集器面臨空間碎片過多時(shí)采用的就是這種處理辦法。
分代收集算法
當(dāng)前商業(yè)虛擬機(jī)的垃圾收集器,大多數(shù)都遵循了“分代收集”的理論進(jìn)行設(shè)計(jì)。
多款常用的垃圾收集器的一致的設(shè)計(jì)原則:收集器應(yīng)該將Java堆劃分出不同的區(qū)域,然后將回收對(duì)象依據(jù)其年齡(年齡即對(duì)象熬過垃圾收集過程的次數(shù))分配到不同的區(qū)域之中存儲(chǔ)。
這樣做的優(yōu)點(diǎn)是:
如果一個(gè)區(qū)域中大多數(shù)對(duì)象都難以熬過垃圾收集過程的話,那么把它們集中放在一起,每次回收時(shí)只關(guān)注如何保留少量存活而不是去標(biāo)記那些大量將要被回收的對(duì)象,就能以較低代價(jià)回收到大量的空間。
如果剩下的都是難以消亡的對(duì)象,那把它們集中放在一塊, 虛擬機(jī)便可以使用較低的頻率來回收這個(gè)區(qū)域,這就同時(shí)兼顧了垃圾收集的時(shí)間開銷和內(nèi)存的空間有效利用。
在Java堆
劃分出不同的區(qū)域之后,垃圾收集器才可以每次只回收其中某一個(gè)或者某些部分的區(qū)域 。因而才有了Minor GC
,Major GC
,Full GC
這樣的回收類型的劃分。也才能夠針對(duì)不同的區(qū)域安排與里面存儲(chǔ)對(duì)象存亡特征相匹配的垃圾收集算法。
收集概念的區(qū)分:
新生代收集(Minor GC/Young GC):指目標(biāo)只是新生代的垃圾收集
老年代收集(Major GC/Old GC):指目標(biāo)只是老年代的垃圾收集。請(qǐng)注意“Major GC”這個(gè)說法現(xiàn)在有點(diǎn)混淆,在不同資料上常有不同所指, 讀者需按上下文區(qū)分到底是指老年代的收集還是整堆收集。
整堆收集(Full GC):收集整個(gè)Java堆和方法區(qū)的垃圾收集。
Java堆·劃分為新生代和老年代。在新生代中,每次垃圾收集時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,而每次回收后存活的少量對(duì)象,將會(huì)逐步晉升到老年代中存放。
ps: 這些區(qū)域劃分僅僅是一部分垃圾收集器的共同特性或者說設(shè)計(jì)風(fēng)格而已,而非某個(gè)JVM
具體實(shí)現(xiàn)的固有內(nèi)存布局,更不是《Java虛擬機(jī)規(guī)范》里對(duì)Java堆
的進(jìn)一步細(xì)致劃分。作為業(yè)界絕對(duì)主流的HotSpot虛擬機(jī)
,它內(nèi)部的垃圾收集器全部都基于“經(jīng)典分代” 來設(shè)計(jì),需要新生代、老年代收集器搭配才能工作。但到了今天,HotSpot
里面也出現(xiàn)了不采用分代設(shè)計(jì)的新垃圾收集器。
內(nèi)存回收策略
下面介紹的回收策略是基于“經(jīng)典分代” 設(shè)計(jì)的回收過程:
1.新生代的分配和回收
1.大多數(shù)情況下,對(duì)象在新生代Eden區(qū)
中分配。當(dāng)Eden區(qū)
沒有足夠空間進(jìn)行分配時(shí),虛擬機(jī)將發(fā)起一次Minor GC
。把新生代分為一塊較大的Eden空間和兩塊較小的 Survivor空間,每次分配內(nèi)存只使用Eden和其中一塊Survivor。發(fā)生垃圾搜集時(shí),將Eden和Survivor中仍 然存活的對(duì)象一次性復(fù)制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空 間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8∶1
2.大對(duì)象直接進(jìn)入老年代
2.大對(duì)象直接進(jìn)入老年代。大對(duì)象就是指需要大量連續(xù)內(nèi)存空間的Java對(duì)象
,最典型的大對(duì)象便是那種很長的字符串,或者元素?cái)?shù)量很龐大的數(shù)組。
為什么要這么做呢?這樣做的目的就是避免在Eden區(qū)
及兩個(gè)Survivor區(qū)
之間來回復(fù)制,產(chǎn)生大量的內(nèi)存復(fù)制操作。
大對(duì)象對(duì)虛擬機(jī)的內(nèi)存分配來說是一個(gè)壞消息,比遇到一個(gè)大對(duì)象更壞的消息就是遇到一群“朝生夕滅”的短命大對(duì)象。我們寫程序的時(shí)候應(yīng)注意避免大對(duì)象。在Java虛擬機(jī)中要避免大對(duì)象的原因是,在分配空間時(shí),它容易導(dǎo)致內(nèi)存明明還有不少空間時(shí)就提前觸發(fā)垃圾收集,以獲取足夠的連續(xù)空間才能安置好它們,而當(dāng)復(fù) 制對(duì)象時(shí),大對(duì)象就意味著高額的內(nèi)存復(fù)制開銷。
3.長期存活的對(duì)象將進(jìn)入老年代
如果經(jīng)過第一次Minor GC
后仍然存活,并且能被Survivor
容納的話,該對(duì)象會(huì)被移動(dòng)到Survivor
空間中,并且將其對(duì)象年齡設(shè)為1歲。對(duì)象在Survivor區(qū)
中每熬過一次Minor GC
,年齡就增加1歲,當(dāng)它的年齡增加到一定的年齡閾值(默認(rèn)為15),就會(huì)被晉升到老年代中。對(duì)象晉升老年代的年齡閾值,可以通過參數(shù)-XX: MaxTenuringThreshold
設(shè)置。
參考
- 《深入理解Java虛擬機(jī)(第三版)》
- 深入解析String#intern
到此這篇關(guān)于關(guān)于JVM翻越內(nèi)存管理的墻的文章就介紹到這了,更多相關(guān)JVM內(nèi)存管理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot和VUE源碼直接整合打包成jar的踩坑記錄
這篇文章主要介紹了SpringBoot和VUE源碼直接整合打包成jar的踩坑記錄,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03springboot openfeign從JSON文件讀取數(shù)據(jù)問題
今天主要說一下在openfeign里讀取JSON文件的問題,我們將測(cè)試所需要的數(shù)據(jù)存儲(chǔ)到文件里,在修改時(shí)關(guān)注點(diǎn)比較單純2018-06-06idea中MavenWeb項(xiàng)目不能創(chuàng)建Servlet的解決方案
這篇文章主要介紹了idea中MavenWeb項(xiàng)目不能創(chuàng)建Servlet的解決方案,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02一篇文章帶了解如何用SpringBoot在RequestBody中優(yōu)雅的使用枚舉參數(shù)
這篇文章主要介紹了SpringBoot中RequestBodyAdvice使用枚舉參數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08Java-web中利用RSA進(jìn)行加密解密操作的方法示例
這篇文章主要給大家介紹了關(guān)于在Java-web中利用RSA進(jìn)行加密解密操作的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-08-08解決Java Redis刪除HashMap中的key踩到的坑
這篇文章主要介紹了解決Java Redis刪除HashMap中的key踩到的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-02-02