前言
本文已經(jīng)收錄到我的Github個人博客,歡迎大佬們光臨寒舍:
學(xué)習(xí)導(dǎo)圖:
一.為什么要學(xué)習(xí)內(nèi)存管理刃麸?
Java
與C++
之間有一堵由內(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í)行
在上述過程中,JVM
會用一段空間來存儲執(zhí)行程序期間需要用到的數(shù)據(jù)和相關(guān)信息丧鸯,這段空間就是運(yùn)行時數(shù)據(jù)區(qū)蛤铜,也就是常說的JVM
內(nèi)存
JVM
會將它所管理的內(nèi)存劃分為若干個不同的數(shù)據(jù)區(qū)域,劃分結(jié)果如圖:
可見丛肢,運(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ū)域也會拋出
StackOverflowError
和OutOfMemoryError
異常
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ū)域稱為 “永久代”,它還有個別名叫做
Non-Heap
(非堆)在
JDK7
的HotSpot
中引有,已經(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)存分配的動作進(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ū)域
- 對象頭:包括兩部分信息
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)的動力
本文參考鏈接: