JVM--內(nèi)存管理

JVM運行時內(nèi)存數(shù)據(jù)區(qū)域

前言

JVM會在執(zhí)行過程中把它所管理的內(nèi)存花費為若干個不同的數(shù)據(jù)區(qū)域。如下圖所示

jvm內(nèi)存管理模型.jpg

下面分別對這些區(qū)域進行解釋。


1、程序技術(shù)器

  • 概念:程序技術(shù)器是一塊較小的內(nèi)存空間,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。
  • 作用:
    • 字節(jié)碼解釋器工作時通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令。
    • JVM的多線程是通過線程輪流切換并分配處理器執(zhí)行時間的方法實現(xiàn)的。在任意一個時刻,一個內(nèi)核都只會執(zhí)行一條線程中的指令。因此為了線程切換后能夠恢復(fù)到正確的執(zhí)行位置,每條線程都需要一個獨立的程序計數(shù)器,線程之間的技術(shù)器互不影響,獨立存儲。因此這類內(nèi)存區(qū)域是線程私有的。
  • 其他:如果線程執(zhí)行的是一個java方法,這個技術(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令的地址。如果正在執(zhí)行的是Native方法,這個計數(shù)器為空,此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError的地方。

2、Java虛擬機棧

  • 概念:虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型,每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀用于存儲局部變量,操作數(shù)棧,動態(tài)鏈接,方法出口等消息。每一個方法從調(diào)用至執(zhí)行完成的過程,對應(yīng)著一個棧幀在虛擬機棧中入棧到出棧的過程。
  • 生命周期: 與其線程相同。
  • 是否線程私有:是
  • 局部變量表:存放了編譯器可知的基本數(shù)據(jù)類型,對象引用和指向一條字節(jié)碼指令的地址。其中64位長度的long和double占用2個局部變量空間,其余類型只占有一個。
  • 異常:
    • StackOverflowError:線程請求的棧深度大于虛擬機所允許的深度。換言之線程請求的棧容量超過棧允許的最大容量。常見的就是遞歸調(diào)用沒有正確終止,導(dǎo)致棧溢出。
    • OutOfMemoryError:虛擬機棧動態(tài)擴展時如果無法申請到足夠的內(nèi)存空間,就會拋出OutOfMemoryError異常。

3、本地方法棧

  • 概念:本地方法棧與虛擬機棧所發(fā)揮的作用是非常相似的。兩者之間的區(qū)別在于虛擬機棧為虛擬機執(zhí)行java方法服務(wù),而本地方法棧則為虛擬機使用到的Native方法服務(wù)。其他均是一樣的。

4、Java堆

  • 概念:java Heap 是java虛擬機所管理的內(nèi)存中最大的一塊。
  • 是否線程私有: 否,所有線程共享的一塊內(nèi)存區(qū)域。
  • 生命周期:在虛擬機啟動的時候創(chuàng)建。
  • 目的與作用:存放對象實例,幾乎所有的對象實例都是在這里分配內(nèi)存。是垃圾收集器管理的主要區(qū)域。
  • 細(xì)分:從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本都采用分代收集算法,所以Java堆中還可以細(xì)分為新生代,老年代。再細(xì)致可以分為Eden空間,F(xiàn)rom Survivor空間,To Survivor空間等。
堆內(nèi)存.jpg
  • 參數(shù)控制:
-Xms   JVM啟動時申請的最小堆內(nèi)存,默認(rèn)為物理內(nèi)存的1/64但小于1G  

-Xmx   JVM啟動時申請的最大堆內(nèi)存,默認(rèn)為物理內(nèi)存但1/4但小于1G

-XX:MinHeapFreeRadio  默認(rèn)當(dāng)剩余堆內(nèi)存空間小于40%時,JVM會將-Xms會增大到-Xmx的大小,通過該參數(shù)可以指定這個比例

-XX:MaxHeapFreeRadio  默認(rèn)當(dāng)空余堆內(nèi)存大于70%時,JVM會減小堆內(nèi)存至-Xms大小,通過該參數(shù)可以指定這個比例

5、方法區(qū)

  • 概念:方法區(qū)存放了要加載的類的消息,類中的靜態(tài)變量,final定義的常量,類中的field方法信息,對象中的getName,isInterface等方法的所需數(shù)據(jù)均是來源于方法區(qū)。
  • 是否線程私有:否
  • 對于HotSpot虛擬機來說,很多人更愿意將方法區(qū)稱為永久代,這因為GC分代收集擴展至方法區(qū),或者說是使用永久代(Permanent Generation)實現(xiàn)方法區(qū)。這個區(qū)域的內(nèi)存回收目標(biāo)主要是針對常量池的回收(字符常量池已經(jīng)移出永久代)和類型的卸載。永久代在java8中已經(jīng)被完全移除,原先永久代中類的元信息會被放入本地內(nèi)存(元數(shù)據(jù)區(qū),metaspace),將類的靜態(tài)變量和內(nèi)部字符串放入java堆中。
  • Metaspace:默認(rèn)情況下,類元數(shù)據(jù)只受可用的本地內(nèi)存限制,通過參數(shù) -XX:MaxMetaspaceSize可以限制本地內(nèi)存分配給類元數(shù)據(jù)的大小,若沒有指定這個參數(shù),元空間會在運行時根據(jù)需要動態(tài)調(diào)整。對于僵死的類及加載器的垃圾回收將在元數(shù)據(jù)使用達到MaxMetaspaceSize時進行GC。
  • 異常:OutOfMemoryError異常。 當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時會拋出該異常。

6、運行時常量池

  • 運行時常量池是方法區(qū)的一部分。主要用于存放編譯期生成的各種字面量和符號引用。這部分內(nèi)容將會在類加載后進入方法區(qū)的運行時常量池中存放。并不是要求常量一定只用在編譯的時候才能纏手,運行期間也可能將新的常量放入池中,例如String.intern()方法。
  • 異常:當(dāng)常量池?zé)o法再申請到內(nèi)存時會拋出OutOfMemoryError。

7、直接內(nèi)存

直接內(nèi)存并不是虛擬機運行時數(shù)據(jù)區(qū)的一部分,也不是java虛擬機規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也會被頻繁調(diào)用,而且可能導(dǎo)致OutOfMemoryError異常。

在NIO類中引入了基于Channel與Buffer的I/O方式,其可以使用Native函數(shù)庫直接分配堆外內(nèi)存。然后通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊內(nèi)存的引用進行操作,這樣避免了在Java堆與Native堆中來回復(fù)制數(shù)據(jù)。

直接內(nèi)存的分配不會受到Java堆大小的限制,但是內(nèi)存一定會受到本機總內(nèi)存大小以及處理器尋址空間的限制。千萬不要在設(shè)置-Xmx時忽略直接內(nèi)存,從而使得各個內(nèi)存區(qū)域總和大于物理內(nèi)存限制,導(dǎo)致在進行動態(tài)擴展時出現(xiàn)OutOfMemoryError異常


對象探秘

對象的創(chuàng)建

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用是否已經(jīng)被加載、解析和初始化過。若沒有則先執(zhí)行類加載過程。在類加載通過后,虛擬機將為新生對象分配內(nèi)存,相當(dāng)于是從Java堆中劃出來一部分。目前有兩種分配方法。

  • 指針碰撞。要求Java堆內(nèi)存中是絕對規(guī)整的,所用用過的內(nèi)存在一邊,空閑的在另一邊,中獎放著一個指針作為分界點的指示器。分配內(nèi)存就是把指針向空閑空間那邊挪動一段與對象大小相等的距離。
  • 空閑列表。堆內(nèi)存并不是規(guī)整的,使用的內(nèi)存和空閑的內(nèi)存相互交錯,虛擬機維護了一個列表,記錄了哪些內(nèi)存塊是可用的。

Java堆是否規(guī)整,取決于垃圾收集器是否帶有壓縮整理功能決定的。因此在使用Serial,ParNew等帶Compact過程的收集器時,采用的分配算法是指針碰撞,而使用CMS這種基于Mark-Sweep算法的收集器時,通常采用的是空閑列表。

由于修改指針?biāo)赶虻膬?nèi)存地址在并發(fā)情況下,不是線程安全的。目前有兩種方案解決這個問題。

  • 對分配內(nèi)存空間的動作進行同步處理,虛擬機采用CAS配上失敗重試的方法保證更新的原子性。
  • 把內(nèi)存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在java堆中預(yù)先分配一塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer ,TLAB)。哪個線程要分配內(nèi)存,就在哪個線程的TLAB上,只用TLAB用完,分配新的TLAB時,才需要同步鎖定。

內(nèi)存分配完成后,虛擬機將內(nèi)存空間初始化為0,然后對對象進行必要的設(shè)置,設(shè)置信息存放在對象頭。最后調(diào)用init方法得到一個可用的對象。

對象的內(nèi)存布局

對象在內(nèi)存中存儲的布局可以分為3塊區(qū)域:對象頭,實例數(shù)據(jù)和對齊填充。

對象頭

對象頭信息分為兩部分.

  • 第一部分用于存儲對象自身的運行時數(shù)據(jù),如哈希碼,GC分代年齡,鎖狀態(tài)標(biāo)志,線程持有的鎖,偏向線程ID,偏向時間戳等。
存儲內(nèi)容 標(biāo)志位 狀態(tài)
對象哈希碼,對象分代年齡 01 未鎖定
指向鎖記錄的指針 00 輕量級鎖定
指向重量級鎖的指針 10 膨脹(重量級鎖定)
空,不需要記錄信息 11 GC標(biāo)記
偏向線程ID,偏向時間戳,對象分代年齡 01 可偏向
  • 對象頭的另一部分是類型指針,即對象指向它的類元數(shù)據(jù)的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
  • 對齊填充不是必然存在的,也沒有特別的含義。

對象的訪問定位

Java程序需要通過棧上的reference數(shù)據(jù)來操作堆上的具體對象。目前主流的訪問方式有使用句柄和直接指針兩種。

  • 使用句柄訪問,在java堆中會劃分出一塊內(nèi)存用來作為句柄池,reference中存儲的就是對象的句柄地址,而句柄中則包含了對象實例數(shù)據(jù)與類型數(shù)據(jù)各自的具體地址信息。


    句柄訪問
  • 使用地址直接訪問,此時reference中存儲的直接就是對象的地址。


    地址訪問
  • 句柄的優(yōu)勢:reference中存儲的是穩(wěn)定的句柄地址,在對象的位置發(fā)生改變的時候,只會改變句柄中的實例數(shù)據(jù)指針,而reference不會受到影響。

  • 直接指針訪問優(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)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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