JVM基礎(chǔ)之內(nèi)存

Java內(nèi)存介紹


Java運(yùn)行時數(shù)據(jù)區(qū)分為下面幾個部分:

  • 程序計數(shù)器;程序計數(shù)器是線程私有的,一塊較小的內(nèi)存空間,它的作用是作為當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器

  • Java虛擬機(jī)棧;虛擬機(jī)棧是線程私有的,它描述的是Java方法在執(zhí)行時方法的內(nèi)存模型
    虛擬機(jī)棧相關(guān)的兩個異常:

    • OutOfMemoryError;如果在動態(tài)擴(kuò)展時,線程無法申請到足夠的內(nèi)存,或無法創(chuàng)建新的線程,則會拋出該異常
    • StackOverflowError;如果方法請求的棧容量超過了JVM允許的最大容量(可通過-Xss參數(shù)進(jìn)行設(shè)置),則會拋出該異常

    棧里最重要的概念就是棧幀,棧幀是一種用于存放方法相關(guān)數(shù)據(jù)和過程數(shù)據(jù)結(jié)果的數(shù)據(jù)結(jié)構(gòu),棧幀存儲了方法的局部變量表,操作數(shù)棧,動態(tài)連接和方法返回地址等信息,這四個部分作用如下:

    • 局部變量表,是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量;需要注意的是相對于類變量兩次賦值過程(一次在準(zhǔn)備階段,賦予系統(tǒng)默認(rèn)初始值,另外一次在初始化階段,賦予程序邏輯定義的初始值),局部變量在定義的時候必須賦值,若未賦值則不能使用,所以在一個方法內(nèi)部定義變量時必須賦值,否則在調(diào)用處將編譯不通過
    • 操作數(shù)棧,它是一個后入先出棧,在方法剛開始執(zhí)行的時候,這個方法的操作數(shù)棧是空的,在方法的執(zhí)行過程中,會有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容,也就是入/出棧操作,例如做算數(shù)運(yùn)算的時候就是通過操作數(shù)棧來進(jìn)行
    • 動態(tài)鏈接,動態(tài)鏈接指的是指向運(yùn)行時常量池中該棧幀所屬方法的引用,持有這個引用是為了方法調(diào)用過程中的動態(tài)連接
    • 方法返回地址,這個比較好理解,就是為了幫助方法調(diào)用完成后回到其調(diào)用處,當(dāng)然異常退出的話是另外的處理機(jī)制

    棧幀的大小在編譯期就已經(jīng)確定,不會在運(yùn)行期改變,而棧幀中兩個最重要的概念就是局部變量表和操作數(shù)棧,局部變量表用于存儲方法參數(shù)以及方法執(zhí)行過程中的局部變量(基本類型,對象引用);操作數(shù)棧用于存儲方法執(zhí)行過程中的參數(shù)和計算結(jié)果

  • 本地方法棧;本地方法棧與Java虛擬機(jī)棧很相似,主要的不同點(diǎn)在于本地方法棧是給native方法使用的,另外很多虛擬機(jī)已經(jīng)將Java虛擬機(jī)棧和本地方法棧合二為一,如最常用的HotSpot虛擬機(jī)

  • Java堆
    Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,他的作用就是存儲Java對象;一般來說不同的線程會在堆中被分配不同的線程緩沖區(qū),以避免多個線程使用同一塊內(nèi)存區(qū)域?qū)е碌母偁?,這一類緩沖區(qū)被稱為TLAB(Thread Local Allocation Buffer),當(dāng)TLAB的空間不夠時,會加鎖并向堆申請新的內(nèi)存空間;Java堆的大小可以設(shè)計為固定大小,也可以設(shè)計為動態(tài)擴(kuò)展;如果Java堆的內(nèi)存已不足且沒有辦法申請更多的內(nèi)存,則會拋出OutOfMemoryError異常;對象的存儲模型一般有兩種形式,第一種是先通過棧的 reference指向堆里的對象實例,而這個對象實例數(shù)據(jù)又有一個指針指向方法區(qū)的對象類型數(shù)據(jù):

heap_1.png

第二種是先通過棧的 reference指向堆里對象實例句柄池,這個句柄池里包含了指向?qū)嵗氐膶ο髮嵗龜?shù)據(jù)和指向方法區(qū)的對象類型數(shù)據(jù);這兩種方式各有優(yōu)點(diǎn),第一種reference直接就指向了對象,減少了一次指針指向,而第二種reference指向的句柄池指針不會隨著對象的移動而改變(Java垃圾回收時會移動對象的位置),而第二種就需要改變reference的指向

heap_2.png

可以通過-Xms-Xmx來設(shè)置堆內(nèi)存的大小,可以通過設(shè)置-XX:+HeapDumpOnOutOfMemoryError實現(xiàn)出現(xiàn)OutOfMemoryError異常時將異常信息保存至磁盤中

  • 方法區(qū)
    方法區(qū)也稱”永久代” 、“非堆”, 它用于存儲類相關(guān)的信息(版本,屬性,方法,接口,final常量,靜態(tài)變量),是各個線程共享的內(nèi)存區(qū)域。默認(rèn)最小值為16MB,最大值為64MB,可以通過-XX:PermSize-XX:MaxPermSize 參數(shù)限制方法區(qū)的大小;在方法區(qū)中,專門劃出了一部分空間作為運(yùn)行時常量池,它是方法區(qū)的一部分,Class文件中除了有類的版本、屬性、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯器生成的各種符號引用,這部分內(nèi)容將在類加載后放到方法區(qū)的運(yùn)行時常量池中
memory.png

除了上面的5個部分以外,還有一類JVM相關(guān)的內(nèi)存空間稱之為:直接內(nèi)存;直接內(nèi)存并不是虛擬機(jī)內(nèi)存的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。如JDK1.4中新加入的NIO,引入了通道與緩沖區(qū)的IO方式,它可以調(diào)用Native方法直接分配堆外內(nèi)存,這個堆外內(nèi)存就屬于直接內(nèi)存,直接內(nèi)存的分配不會影響堆內(nèi)存的大小

Java內(nèi)存管理


這里說的Java內(nèi)存分配主要指代的是對象的內(nèi)存分配,也就是堆的運(yùn)作機(jī)制;在JVM中,一般從對象存活時間角度上將存儲空間分為

  • 新生代,新生代由一塊Eden space和一塊Survivor space組成;其中Survivor space又被劃分為兩塊:From space和To space或Survivor 0 和Survivor 1;新創(chuàng)建的對象都會存儲在新生代中(除了一些比較大的對象會直接創(chuàng)建至老年代);與新生代對應(yīng)的回收稱之為 minor garbage collection (minor GC),當(dāng)Eden space不夠用時,JVM就會啟動minor GC并將依舊存活的對象復(fù)制至Survivor space,經(jīng)過特定的GC次數(shù)后(可以通過-XX:MaxTenuringThreshold設(shè)置),如果對象依然存活,則復(fù)制至老年代。
    另外說說這里為什么會把Survivor space分為兩個區(qū)(當(dāng)然也有的JVM模型是不分的),這其實和minor GC(回收新生代對象)的復(fù)制-清除算法有關(guān)(該算法可參考本文"垃圾收集算法"章節(jié)),首先,eden區(qū)滿了,觸發(fā)GC,將存活對象復(fù)制到Survivor區(qū)等待其老去,待到eden區(qū)再一次滿以后,GC會再次清理eden區(qū)和Survivor區(qū),并將eden中存活的對象復(fù)制到Survivor區(qū),若Survivor區(qū)只有一個,那么在復(fù)制前(或者后)需要對Survivor區(qū)進(jìn)行碎片處理,而由于碎片處理效率不高,所以索性將Survivor區(qū)分成兩個,每次都直接進(jìn)行復(fù)制(eden和Survivor 0復(fù)制到Survivor 1,Survivor 0清空,下一次就是eden和Survivor 1復(fù)制到Survivor 0,Survivor 1清空),簡單方便效率高
  • 老年代,也稱Tenured space,用于存放經(jīng)過多次minor garbage collection任然存活的對象,例如緩存對象,新建的對象也有可能直接進(jìn)入老年代,主要有兩種情況:①.大對象,可通過啟動參數(shù)設(shè)置-XX:PretenureSizeThreshold=1024(單位為字節(jié),默認(rèn)為0)來代表超過多大時就不在新生代分配,而是直接在老年代分配。②.大的數(shù)組對象,切數(shù)組中無引用外部對象。 老年代所占的內(nèi)存大小為-Xmx對應(yīng)的值減去-Xmn對應(yīng)的值;與老年代對應(yīng)的回收稱之為major garbage collection(full GC),當(dāng)老年代的空間不夠用時,就會執(zhí)行full GC;需要注意的是full GC比minor GC所需的代價要大得多
  • 永久代,即方法區(qū),永久代這個概念其實屬于HotSpot發(fā)明的,其他的虛擬機(jī)大都沒這個概念,另外HotSpot自身也計劃在后續(xù)的發(fā)展中拋棄永久代這個概念,在Java 8中已經(jīng)通過Metaspace來代替永久代
    其中新生代與老年代分別處于堆中,而永久代處于方法區(qū)中
all.png

對象的生存或死亡

與內(nèi)存分配對應(yīng)的是內(nèi)存的回收,Java的內(nèi)存回收依靠的是垃圾收集器,它主要負(fù)責(zé)將已經(jīng)沒有存在意義的內(nèi)存占用進(jìn)行回收,以保證后續(xù)其他的內(nèi)存使用,這里涉及到兩點(diǎn)知識:

  1. 如何判斷對象已經(jīng)沒有存在意義
  2. 如何進(jìn)行內(nèi)存回收

首先來看垃圾收集器是如何判斷對象的生或死的,一般有兩種方式判斷一個對象是否應(yīng)該繼續(xù)存活:

  1. 引用計數(shù)算法,即有地方持有該對象的引用就在該對象的引用計數(shù)器上加1,反之,當(dāng)引用失效時就減1;那么該引用計數(shù)器為0的對象就是不可能再被使用的
  2. 可達(dá)性分析算法,這個算法的基本思路是通過一系列稱為「GC Roots」的對象作為起始點(diǎn),從這些節(jié)點(diǎn)開始往下搜索,搜索所走過的路徑稱為引用鏈,當(dāng)一個對象到「GC Roots」沒有任何引用鏈相連(就是「GC Roots」到這個對象不可達(dá))則證明此對象是不可用的,如下圖,Object 5,6,7皆為不可達(dá)對象


    image.png

    在Java中作為「GC Roots」的有四類對象

  • 虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象
  • 方法區(qū)中類靜態(tài)屬性應(yīng)用的對象
  • 方法區(qū)中常量引用的對象
  • 本地方法棧中JNI(即一般說的Native方法)引用的對象

Java使用的就是可達(dá)性分析算法,其實對于引用計數(shù)算法有一個比價明顯的問題,就是對于兩個或多個對象的循環(huán)引用,它是無能為力的;那么被可達(dá)性算法標(biāo)記為不可達(dá)的對象是否就一定會被回收掉呢?其實也不是,要真正宣告一個對象的死亡,至少要經(jīng)歷兩次標(biāo)記過程:

  1. 如果對象在經(jīng)過可達(dá)性分析發(fā)現(xiàn)沒有與「GC Roots」相連,那么它將被第一次標(biāo)記并進(jìn)行一次篩選,篩選的條件是該對象是否有必要執(zhí)行finalize()方法,如果有必要(如果重寫了就需要執(zhí)行)就執(zhí)行,此時是可以在finalize()中將對象進(jìn)行最后的挽救的(重新對其進(jìn)行引用),但是不建議這樣做,一是因為這種垂死掙扎的方式?jīng)]必要,二是因為Java本身并不保證finalize()何時執(zhí)行或一定能執(zhí)行完成
  2. 經(jīng)過第一步后,會再次對這些對象進(jìn)行標(biāo)記,若此時沒有在finalize()中對對象進(jìn)行重新引用,那么就可以標(biāo)記為「確定死亡」了

最后說說方法區(qū)(或永久代)的回收,方法區(qū)的垃圾收集主要針對兩部分內(nèi)容:廢棄常量和無用的類?;厥諒U棄常量與回收J(rèn)ava堆中的對象很類似,以常量池中的字符串回收為例,如果字符串常量"abc"在當(dāng)前的系統(tǒng)中沒有一個對應(yīng)的String對象對其進(jìn)行引用,那么這個常量將允許被回收;而判斷一個類是否是"無用的類"則較苛刻,它需要滿足幾個條件:

  1. 該類的所有實例都被回收,也就是Java堆中不存在該類的任何實例
  2. 加載該類的ClassLoader已被回收
  3. 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法再任何地方通過反射訪問該類的方法

只有滿足這三個條件,該類才允許被回收,但需要注意的是方法區(qū)的回收不是一定會執(zhí)行的,而且JVM的規(guī)范里也沒有強(qiáng)制性的要求說需要實現(xiàn)這一部分區(qū)域的回收機(jī)制,對于一些運(yùn)用運(yùn)行時代碼生成技術(shù)如反射,動態(tài)代理,CGLib等這種頻繁自定義ClassLoader的場景需要虛擬機(jī)具備類卸載的功能,以保證方法區(qū)不會溢出

垃圾收集算法

垃圾收集算法主要有下列幾種:

  1. 標(biāo)記-清除算法,正如它的名字一樣算法分為"標(biāo)記"和"清除"兩個階段,這個算法是最基礎(chǔ)的收集算法,后續(xù)的收集算法都是基于這種思路并對其不足進(jìn)行改進(jìn),它的效率不高,同時會產(chǎn)生大量不連續(xù)的內(nèi)存碎片


    image.png
  2. 復(fù)制-收集算法,它將可用內(nèi)存分為兩塊,每次使用其中一塊,當(dāng)這塊內(nèi)存用完了,就將還存活的對象復(fù)制到另外一塊上面,然后把已使用的內(nèi)存空間一次性清理掉;它的主要優(yōu)點(diǎn)是不用考慮內(nèi)存碎片的問題,但是也存在著實際使用內(nèi)存變小的不足


    image.png
  3. 標(biāo)記-整理算法,這種算法的過程與"標(biāo)記-清除算法"類似,但后續(xù)步驟不是直接對可回收對象進(jìn)行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存


    image.png
  4. 分代收集算法,這是當(dāng)前主流JVM采用的算法,這種算法沒有什么新思想,只是根據(jù)對象存活周期不同將內(nèi)存劃分為幾塊(前面已有介紹),在新生代中,每次垃圾收集時都發(fā)現(xiàn)有大批對象死去,只有少量存活,那就選用復(fù)制算法,只需要付出少量的復(fù)制成本就可以完成收集,而老年代中因為對象存活率高,沒有額外空間進(jìn)行分配擔(dān)保,就采用"標(biāo)記-清除算法"或"標(biāo)記-整理算法"進(jìn)行回收

最后,推薦兩篇關(guān)于JVM內(nèi)存相關(guān)性能調(diào)優(yōu)的文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容