一文洞悉JVM內(nèi)存管理機(jī)制

前言

本文已經(jīng)收錄到我的Github個人博客,歡迎大佬們光臨寒舍:

我的GIthub博客

學(xué)習(xí)導(dǎo)圖:

學(xué)習(xí)導(dǎo)圖

一.為什么要學(xué)習(xí)內(nèi)存管理刃麸?

JavaC++之間有一堵由內(nèi)存動態(tài)分配垃圾回收機(jī)制所圍成的高墻凶硅,墻外面的人想進(jìn)去跺讯,墻里面的人出不來

對于Java程序員來說震庭,JVM給我們提供了自動內(nèi)存管理機(jī)制鼻由,不需要既當(dāng)“皇帝”担钮,又當(dāng)“人民”橱赠,不需要人為地給每一個new操作寫配對的delete/free代碼,不容易出現(xiàn)內(nèi)存泄漏和內(nèi)存溢出問題箫津。然而一旦出現(xiàn)內(nèi)存泄漏和溢出方面的問題狭姨,如果不清楚JVM內(nèi)存的內(nèi)存管理機(jī)制,那么將很難定位與解決問題苏遥。而且饼拍,JVM的內(nèi)存管理機(jī)制在面試中也是非常重要的考點(diǎn)之一。

綜上田炭,想要更加深入了解JVM的奧秘师抄,探究JVM內(nèi)存管理機(jī)制是必不可少的!=塘颉叨吮!

二.核心知識點(diǎn)歸納

2.1 JVM運(yùn)行時數(shù)據(jù)區(qū)域

JVM 執(zhí)行 Java 程序的過程:Java 源代碼文件 (.java) 會被 Java 編譯器編譯為字節(jié)碼文件(.class),然后由 JVM 中的類加載器加載各個類的字節(jié)碼文件栋豫,加載完畢之后挤安,交由 JVM 執(zhí)行引擎執(zhí)行

執(zhí)行Java程序的過程

在上述過程中,JVM會用一段空間來存儲執(zhí)行程序期間需要用到的數(shù)據(jù)和相關(guān)信息丧鸯,這段空間就是運(yùn)行時數(shù)據(jù)區(qū)蛤铜,也就是常說的JVM內(nèi)存

JVM會將它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域,劃分結(jié)果如圖:

JVM運(yùn)行時數(shù)據(jù)區(qū)

可見丛肢,運(yùn)行時數(shù)據(jù)區(qū)被分為線程私有數(shù)據(jù)區(qū)線程共享數(shù)據(jù)區(qū)兩大類:

  • 線程私有數(shù)據(jù)區(qū)包含:程序計數(shù)器围肥、虛擬機(jī)棧、本地方法棧
  • 線程共享數(shù)據(jù)區(qū)包含:Java堆蜂怎、方法區(qū)(內(nèi)部包含運(yùn)行時常量池

下面將為您詳細(xì)介紹各個數(shù)據(jù)區(qū)的內(nèi)容

2.1.1 程序計數(shù)器

  • 定義:當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器
  • 如果線程正在執(zhí)行的是一個 Java 方法穆刻,那么計數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址
  • 如果線程正在執(zhí)行的是一個 Native 方法,那么計數(shù)器的值則為

字節(jié)碼解釋器工作時杠步,就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令氢伟,分支榜轿、循環(huán)、跳轉(zhuǎn)朵锣、異常處理谬盐、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器來完成。

  • 為什么必須是私有:為了線程切換后能恢復(fù)到正確的執(zhí)行位置诚些,每條線程都需要有一個獨(dú)立的程序計數(shù)器飞傀,各條線程之間計數(shù)器互不影響,獨(dú)立存儲诬烹,因此它是線程私有的內(nèi)存
  • 在《 Java 虛擬機(jī)規(guī)范》中砸烦,是唯一一個沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域

2.1.2 Java 虛擬機(jī)棧

想更加詳細(xì)了解 JVM 棧的讀者,可以看下筆者寫的這篇文章:運(yùn)行時棧幀結(jié)構(gòu)

  • 定義: Java 方法執(zhí)行的內(nèi)存模型
  • 每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀绞吁,用于存儲局部變量表幢痘、操作數(shù)棧、動態(tài)鏈接家破、方法出口等方法信息

  • 每個方法從調(diào)用直至執(zhí)行完成的過程雪隧,就對應(yīng)著一個棧幀在虛擬機(jī)棧中入棧到出棧的過程

局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型、對象引用類型和 returnAddress 類型员舵,它所需的內(nèi)存空間在編譯期間完成分配

  • 線程私有的內(nèi)存,與線程生命周期相同
  • 一般把 Java 內(nèi)存區(qū)分為堆內(nèi)存(Heap)和棧內(nèi)存(Stack)藕畔,其中『椔砥В』指的是虛擬機(jī)棧,『堆』指的是 Java
  • Java 虛擬機(jī)規(guī)范中注服,對這個區(qū)域規(guī)定了兩種異常狀況:
  • 如果線程請求的棧深度大于虛擬機(jī)所允許的深度韭邓,將拋出 StackOverflowError 異常
  • 如果虛擬機(jī)棧可動態(tài)擴(kuò)展且擴(kuò)展時無法申請到足夠的內(nèi)存溶弟,將拋出 OutOfMemoryError 異常

2.1.3 本地方法棧

  • 定義:虛擬機(jī)使用到的 Native 方法服務(wù)

想要了解Native方法的讀者女淑,可以看下這篇文章:Java中native方法

  • 在虛擬機(jī)規(guī)范中,對這個區(qū)域無強(qiáng)制規(guī)定辜御,由具體的虛擬機(jī)自由實(shí)現(xiàn)鸭你。與虛擬機(jī)棧一樣,本地方法棧區(qū)域也會拋出 StackOverflowErrorOutOfMemoryError 異常

2.1.4 Java堆

  • 定義:被所有線程共享的一塊內(nèi)存區(qū)域擒权,在虛擬機(jī)啟動時創(chuàng)建
  • 作用:用于存放幾乎所有的對象實(shí)例和數(shù)組

Java 堆中袱巨,可能劃分出多個線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)碳抄,但無論哪個區(qū)域愉老,存儲的都仍然是對象實(shí)例,進(jìn)一步劃分的目的是為了更好地回收內(nèi)存剖效,或者更快地分配內(nèi)存

  • 是垃圾收集器管理的主要區(qū)域嫉入,也被稱做 “ GC 堆”(可別叫做垃圾堆orz)
  • JVM 所管理的內(nèi)存中最大的一塊
  • 可處于物理上不連續(xù)的內(nèi)存空間中焰盗,只要邏輯上是連續(xù)的即可
  • Java 虛擬機(jī)規(guī)范中,如果在堆中沒有內(nèi)存完成實(shí)例分配咒林,且堆也無法再擴(kuò)展時熬拒,將會拋出 OutOfMemoryError 異常

2.1.5 方法區(qū)

注意:方法區(qū)必須和虛擬機(jī)棧區(qū)分開,方法區(qū)不存方法映九,虛擬機(jī)棧存 Java 方法

  • 定義:與 Java 堆一樣梦湘,是各個線程共享的內(nèi)存區(qū)域

  • 作用:用于存儲已被虛擬機(jī)加載的類信息、常量件甥、靜態(tài)變量捌议、即時編譯器編譯后的代碼等數(shù)據(jù)

方法區(qū)裝了啥
  • 人們更愿意把這個區(qū)域稱為 “永久代”,它還有個別名叫做 Non-Heap(非堆)

    JDK7HotSpot 中引有,已經(jīng)把原本放在永久代的字符串常量池瓣颅,靜態(tài)變量移出;

    JDK8中譬正,廢棄永久代的概念宫补,改用元空間

  • 對用元空間替換永久代的原因感興趣的話曾我,可以看下這篇文章:一文讀懂 - 元空間和永久代

永久代/元空間 和方法區(qū)的區(qū)別:

  • 永久代/元空間 可看作是方法區(qū)的實(shí)現(xiàn)
  • Java 堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或可擴(kuò)展外粉怕,還可選擇不實(shí)現(xiàn) GC
  • Java 虛擬機(jī)規(guī)范中,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時抒巢,將拋出 OutOfMemoryError 異常

2.1.6 運(yùn)行時常量池

Class 文件中除了有類的版本贫贝、字段、方法蛉谜、接口等描述信息外稚晚,還有一項(xiàng)信息是常量池表,用于存放編譯期生成的各種字面量和符號引用型诚,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時常量池中存放

Q1:字面量是什么

可以理解為字面意思的常量客燕。

int a; //變量
const int b = 10; //b為常量,10為字面量
string str = “hello world狰贯!”; // str 為變量也搓,hello world!為字面量

由上述代碼可知暮现,字面量就是如此樸實(shí)無華

Q2:符號引用是什么

可以是任意類型的字面量还绘。只要能無歧義的定位到目標(biāo)。在編譯期間由于暫時不知道類的直接引用栖袋,因此先使用符號引用代替拍顷。最終還是會轉(zhuǎn)換為直接引用訪問目標(biāo)

比如:java/lang/StringBuilder

Q3:運(yùn)行時常量池是什么

  • 相對于 Class 文件常量池的一個重要特征是具備動態(tài)性,體現(xiàn)在并非只有預(yù)置入 Class 文件中常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時常量池塘幅,運(yùn)行期間也可能將新的常量放入池中
  • 方法區(qū)的一部分昔案,會受到方法區(qū)內(nèi)存的限制
  • Java 虛擬機(jī)規(guī)范中尿贫,當(dāng)常量池?zé)o法再申請到內(nèi)存時會拋出 OutOfMemoryError 異常

2.1.7 直接內(nèi)存

  • 它并不是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分,也不是《Java虛擬機(jī)規(guī)范》中定義的內(nèi)存區(qū)域踏揣,但是這部分內(nèi)存也被頻繁地調(diào)用
  • 作用:避免了在JAVA堆和Native堆中來回復(fù)制數(shù)據(jù)庆亡,因此在一些場景下能顯著提高性能

JDK1.4中新加入了NIO類,引入了基于通道與緩沖區(qū)的IO方式捞稿,可以使用Native函數(shù)庫直接分配直接內(nèi)存(堆外內(nèi)存)又谋,然后通過DirectByteBuffer作為這塊內(nèi)存的引用進(jìn)行操作

2.2 HotSpot 虛擬機(jī)內(nèi)存對象探秘

在熟悉虛擬機(jī)內(nèi)存劃分及其具體內(nèi)容之后,為詳細(xì)了解虛擬機(jī)內(nèi)存中數(shù)據(jù)的其他細(xì)節(jié)娱局,以常用的虛擬機(jī) HotSpot 和常用的內(nèi)存區(qū)域 Java 堆為例彰亥,探討 HotSpot 虛擬機(jī)在 Java 堆中對象分配、布局和訪問的全過程

2.2.1 對象的創(chuàng)建

遇到一個 new 指令后創(chuàng)建過程分三步

1.類加載檢查

檢查 new 指令的參數(shù)是否能在常量池中定位到一個類的符號引用且該符號引用代表的類是否已被加載衰齐、解析和初始化任斋,若沒有則需先執(zhí)行相應(yīng)的類加載,反之下一步

想詳細(xì)了解類加載的知識的話耻涛,可以看下筆者的一篇文章:一夜搞懂 | JVM 類加載機(jī)制

2.分配內(nèi)存

  • Java 堆中的內(nèi)存是否規(guī)整決定如何給新生對象分配可用空間
  • 由堆所采用的垃圾收集器是否帶有空間壓縮整理的能力決定Java 堆中的內(nèi)存是否規(guī)整

PS:想詳細(xì)了解GC或者內(nèi)存分配的話废酷,可以看下筆者的這篇文章:一夜搞懂 | JVM GC&內(nèi)存分配

  • 若規(guī)整,采用 “指針碰撞” 分配方式:
  • 過程:將用過和空閑的內(nèi)存放在兩邊抹缕,中間以一個指針作為分界指示器澈蟆。當(dāng)分配內(nèi)存時,就把指針向空閑一邊挪動與對象大小相等的距離即可
  • 應(yīng)用:Serial卓研、ParNew 等帶 壓縮過程的收集器
  • 若非規(guī)整丰介,采用 “空閑列表” 分配方式:
  • 過程:維護(hù)一個記錄可用內(nèi)存塊的列表。當(dāng)分配內(nèi)存時鉴分,就從列表中找到一塊足夠大的空間劃分給對象實(shí)例并更新記錄
  • 應(yīng)用:基于 Mark-Sweep 算法的 CMS 收集器
分配內(nèi)存

保證內(nèi)存分配是線程安全的解決方案:

  • 對內(nèi)存分配的動作進(jìn)行同步處理
  • 每個線程在 Java 堆中預(yù)先分配一塊內(nèi)存(本地線程分配緩沖 TLAB),在本線程的 TLAB 上進(jìn)行分配带膀,當(dāng) TLAB 用完需要分配新的 TLAB 時再同步鎖定

3.設(shè)置對象頭

將對象的所屬類志珍、找到類的元數(shù)據(jù)信息的方式、對象的哈希碼垛叨、對象的 GC 分代年齡等信息存放在對象的對象頭中

2.2.2 對象的內(nèi)存分布

分為三塊區(qū)域

對象的內(nèi)存分布
  • 對象頭:包括兩部分信息
  • Mark Word:用于存儲對象自身的運(yùn)行時數(shù)據(jù)伦糯,如哈希碼、GC 分代年齡嗽元、鎖狀態(tài)標(biāo)志敛纲、線程持有的鎖、偏向線程 ID剂癌、偏向時間戳等
  • 類型指針:用于確定這個對象的所屬類
  • 實(shí)例數(shù)據(jù):存儲真正的有效信息淤翔,是程序代碼中定義的各種類型的字段內(nèi)容。存儲順序會受虛擬機(jī)分配策略參數(shù)和字段在 Java 源碼中定義順序這兩個因素影響佩谷。
  • 對齊填充:占位符旁壮,幫助補(bǔ)全未對齊的對象實(shí)例數(shù)據(jù)部分(保證是 8 字節(jié)的倍數(shù))监嗜,非必需

2.2.3 對象的訪問定位

兩種主流的訪問方式

  • 通過句柄訪問對象

    Java 中劃分出一塊內(nèi)存來作為句柄池reference 存儲的是對象的句柄地址抡谐,在句柄中包含了對象實(shí)例數(shù)據(jù)與類型數(shù)據(jù)(方法區(qū)中的類信息)各自的具體地址信息

    好處:reference 中存儲的是穩(wěn)定的句柄地址裁奇,在對象被移動時只會改變句柄中的實(shí)例數(shù)據(jù)指針,而 reference 本身不需要修改

    通過句柄訪問對象
  • 通過直接指針訪問對象

    Java 堆對象的布局中考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息麦撵,reference 存儲的直接就是對象地址

    好處:速度更快刽肠,節(jié)省了一次指針定位的時間開銷

    通過直接指針訪問對象

2.3 實(shí)戰(zhàn):OutOfMemoryError 異常

這部分的內(nèi)容可以看下這篇文章:JVM內(nèi)存溢出詳解(棧溢出,堆溢出免胃,持久代溢出音五、無法創(chuàng)建本地線程)

三.課堂小測試

恭喜你!已經(jīng)看完了前面的文章杜秸,相信你對JVM內(nèi)存管理機(jī)制已經(jīng)有一定深度的了解放仗,下面,進(jìn)行一下課堂小測試撬碟,驗(yàn)證一下自己的學(xué)習(xí)成果吧诞挨!

Q1:JVM中,為什么要把堆與棧分離呢蛤?棧不是也可以存儲數(shù)據(jù)嗎惶傻?

  • 軟件設(shè)計的角度看,棧代表了處理邏輯其障,而堆代表了數(shù)據(jù)银室,分工明確,處理邏輯更為清晰體現(xiàn)了“分而治之”以及“隔離”的思想励翼。

  • 堆與棧的分離蜈敢,使得堆中的內(nèi)容可以被多個棧共享(也可以理解為多個線程訪問同一個對象)。這樣共享的方式有很多收益:提供了一種有效的數(shù)據(jù)交互方式(如:共享內(nèi)存)汽抚;堆中的共享常量和緩存可以被所有棧訪問抓狭,節(jié)省了空間。

  • 棧因?yàn)檫\(yùn)行時的需要造烁,比如保存系統(tǒng)運(yùn)行的上下文否过,需要進(jìn)行地址段的劃分。由于棧只能向上增長惭蟋,因此就會限制住棧存儲內(nèi)容的能力苗桂。而堆不同,堆中的對象是可以根據(jù)需要動態(tài)增長的告组,因此棧和堆的拆分煤伟,使得動態(tài)增長成為可能,相應(yīng)棧中只需記錄堆中的一個地址即可。

  • 堆和棧的結(jié)合完美體現(xiàn)了面向?qū)ο蟮脑O(shè)計持偏。當(dāng)我們將對象拆開驼卖,你會發(fā)現(xiàn),對象的屬性即是數(shù)據(jù)鸿秆,存放在堆中酌畜;而對象的行為(方法)即是運(yùn)行邏輯,放在棧中卿叽。因此編寫對象的時候桥胞,其實(shí)即編寫了數(shù)據(jù)結(jié)構(gòu),也編寫的處理數(shù)據(jù)的邏輯考婴。

Q2:為啥說堆和JVM棧是程序運(yùn)行的關(guān)鍵

  • 棧是運(yùn)行時的單位(解決程序的運(yùn)行問題贩虾,即程序如何執(zhí)行,或者說如何處理數(shù)據(jù))沥阱,而堆是存儲的單位(解決的是數(shù)據(jù)存儲的問題缎罢,即數(shù)據(jù)怎么放、放在哪兒)
  • 堆存儲的是對象考杉。棧存儲的是基本數(shù)據(jù)類型和堆中對象的引用策精;(參數(shù)傳遞的值傳遞和引用傳遞)

如果文章對您有一點(diǎn)幫助的話,希望您能點(diǎn)一下贊崇棠,您的點(diǎn)贊咽袜,是我前進(jìn)的動力

本文參考鏈接:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末凹联,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子哆档,更是在濱河造成了極大的恐慌匕垫,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件虐呻,死亡現(xiàn)場離奇詭異,居然都是意外死亡寞秃,警方通過查閱死者的電腦和手機(jī)斟叼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來春寿,“玉大人朗涩,你說我怎么就攤上這事“蟾模” “怎么了谢床?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵兄一,是天一觀的道長。 經(jīng)常有香客問我识腿,道長出革,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任渡讼,我火速辦了婚禮骂束,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘成箫。我一直安慰自己展箱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布蹬昌。 她就那樣靜靜地躺著混驰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪皂贩。 梳的紋絲不亂的頭發(fā)上栖榨,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機(jī)與錄音先紫,去河邊找鬼治泥。 笑死,一個胖子當(dāng)著我的面吹牛遮精,可吹牛的內(nèi)容都是我干的居夹。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼本冲,長吁一口氣:“原來是場噩夢啊……” “哼准脂!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起檬洞,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤狸膏,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后添怔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體湾戳,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年广料,在試婚紗的時候發(fā)現(xiàn)自己被綠了砾脑。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡艾杏,死狀恐怖韧衣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤畅铭,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布氏淑,位于F島的核電站,受9級特大地震影響硕噩,放射性物質(zhì)發(fā)生泄漏假残。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一榴徐、第九天 我趴在偏房一處隱蔽的房頂上張望守问。 院中可真熱鬧,春花似錦坑资、人聲如沸耗帕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仿便。三九已至,卻和暖如春攒巍,著一層夾襖步出監(jiān)牢的瞬間嗽仪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工柒莉, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闻坚,地道東北人。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓兢孝,卻偏偏與公主長得像窿凤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子跨蟹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容