前言
在使用c++進(jìn)行編程時(shí)淘衙,我們通過(guò)new創(chuàng)建的每一個(gè)對(duì)象都需要有對(duì)應(yīng)的delete操作去釋放對(duì)象所占用的內(nèi)存横浑,對(duì)內(nèi)存的掌控度比較高剔桨,但是程序員需要知道對(duì)象什么時(shí)候不需要使用了,并需要手動(dòng)釋放內(nèi)存徙融,如果忘記了delete釋放领炫,很容易出現(xiàn)內(nèi)存泄漏(申請(qǐng)內(nèi)存后,沒(méi)有釋放张咳,會(huì)一直占用著)和內(nèi)存溢出(因?yàn)檫^(guò)多的內(nèi)存泄漏導(dǎo)致無(wú)法申請(qǐng)足夠的內(nèi)存帝洪,即out of memory)的問(wèn)題。
相比之下脚猾,java虛擬機(jī)提供了自動(dòng)內(nèi)存管理機(jī)制葱峡,java程序員可以解放雙手,不再需要去寫(xiě)delete等手動(dòng)釋放內(nèi)存的代碼龙助,虛擬機(jī)會(huì)自動(dòng)將內(nèi)存中無(wú)用的對(duì)象占用的內(nèi)存釋放砰奕。
了解jvm的必要
雖然有自動(dòng)內(nèi)存管理機(jī)制的存在蛛芥,但是不代表寫(xiě)的每個(gè)java程序都不存在內(nèi)存泄漏和內(nèi)存溢出問(wèn)題,我們需要對(duì)虛擬機(jī)有足夠的了解军援,才能在發(fā)生內(nèi)存泄露和內(nèi)存溢出的時(shí)候有效地排查問(wèn)題仅淑。
本文將對(duì)jvm虛擬機(jī)運(yùn)行時(shí)內(nèi)存進(jìn)行一個(gè)基本的介紹,后續(xù)的文章也會(huì)講解jvm其他知識(shí)胸哥,大部分都是自己的讀書(shū)總結(jié)加上自己的理解涯竟。希望將自己的所學(xué)進(jìn)行總結(jié)的同時(shí)能惠及他人,如果有什么地方講的不對(duì)空厌,希望各位同學(xué)能夠指出庐船。
內(nèi)存劃分
java虛擬機(jī)將其管理的內(nèi)存劃分為以下幾塊:
- 程序計(jì)數(shù)器 (PC Register)
- 虛擬機(jī)棧 (JVM Stack)
- 本地方法棧 (Native Method Stack)
- 堆 (Heap)
- 方法區(qū) (Method Area)
各個(gè)區(qū)域都有其各自的特點(diǎn)和作用,以及不同的創(chuàng)建和銷(xiāo)毀的時(shí)間
各個(gè)區(qū)域的介紹
程序計(jì)數(shù)器
- 描述
- 程序計(jì)數(shù)器是一個(gè)較小的內(nèi)存區(qū)域
- 作用
- 記錄著當(dāng)前線程所執(zhí)行的字節(jié)碼行號(hào)嘲更。
- 字節(jié)碼解釋器在工作的時(shí)候筐钟,通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條要執(zhí)行的字節(jié)碼指令。
- 分支赋朦,循環(huán)篓冲,跳轉(zhuǎn),異常處理宠哄,線程恢復(fù)等功能都需要使用到這個(gè)程序計(jì)數(shù)器纹因。
- 特點(diǎn)
- 線程私有--每個(gè)線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器。
- 如果當(dāng)前線程正在執(zhí)行一個(gè)java方法琳拨,這程序計(jì)數(shù)器的值為虛擬機(jī)字節(jié)碼指令的地址瞭恰,如果執(zhí)行的是一個(gè)
Native
方法,這個(gè)計(jì)數(shù)器的值則為空狱庇。 - 程序計(jì)數(shù)器是唯一沒(méi)有規(guī)定OutOfMemoryError的內(nèi)存區(qū)域惊畏。
- 創(chuàng)建時(shí)間
- 每個(gè)線程啟動(dòng)的時(shí)候會(huì)創(chuàng)建一個(gè)較小的內(nèi)存區(qū)域作為線程的程序計(jì)數(shù)器
- 銷(xiāo)毀時(shí)間
- 線程結(jié)束時(shí)會(huì)釋放該內(nèi)存區(qū)域
擴(kuò)展問(wèn)題1:為什么需要程序計(jì)數(shù)器?
java虛擬機(jī)的多線程是通過(guò)線程輪轉(zhuǎn)密任,分配CPU時(shí)間片來(lái)執(zhí)行java程序颜启,當(dāng)線程切換時(shí),為了能夠回到原來(lái)的字節(jié)碼執(zhí)行位置繼續(xù)程序的執(zhí)行浪讳,所以每個(gè)線程會(huì)有一個(gè)程序計(jì)數(shù)器缰盏。
擴(kuò)展問(wèn)題2:Native方法是什么?
java程序執(zhí)行的時(shí)候調(diào)用的方法淹遵,有些是用java語(yǔ)言實(shí)現(xiàn)的口猜,有些是用其他語(yǔ)言編寫(xiě)實(shí)現(xiàn)的,用其他語(yǔ)言實(shí)現(xiàn)的方法稱(chēng)為Native方法或本地方法,native方法會(huì)使用native
關(guān)鍵字進(jìn)行標(biāo)注,如Object
類(lèi)的getClass()
方法:
public class Object {
public final native Class<?> getClass();
...
}
由于native方法不是java實(shí)現(xiàn)的透揣,也就沒(méi)有字節(jié)碼行號(hào)之說(shuō)济炎,此時(shí)程序計(jì)數(shù)器的值應(yīng)當(dāng)為空(undefined)。
虛擬機(jī)棧
- 描述
- 虛擬機(jī)棧是描述java方法執(zhí)行過(guò)程的一個(gè)內(nèi)存模型辐真。
- 具體描述:每個(gè)方法在執(zhí)行的時(shí)候都會(huì)創(chuàng)建一個(gè)棧幀须尚,棧幀中存儲(chǔ)的是java方法的局部變量表崖堤、操作數(shù)棧、動(dòng)態(tài)鏈接耐床、方法出口等信息密幔。java程序在執(zhí)行的時(shí)候每調(diào)用一個(gè)java方法都會(huì)對(duì)應(yīng)的創(chuàng)建棧幀并壓入虛擬機(jī)棧中,當(dāng)方法執(zhí)行完畢撩轰,又會(huì)將棧幀從虛擬機(jī)棧中彈出胯甩。虛擬機(jī)棧就是棧幀存放的一個(gè)棧結(jié)構(gòu)的內(nèi)存區(qū)域。
- 作用
- 描述java方法執(zhí)行的過(guò)程钧敞,保存棧幀蜡豹。
- 特點(diǎn)
- 線程私有
- 此區(qū)域可能會(huì)有兩種內(nèi)存異常情況:
- 當(dāng)棧的深度大于虛擬機(jī)所限制的最大深度麸粮,會(huì)拋出StackOverflowError異常溉苛。
- 如果虛擬機(jī)棧動(dòng)態(tài)擴(kuò)展無(wú)法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出OutOfMemoryError異常弄诲。
- 創(chuàng)建時(shí)間
- 線程啟動(dòng)的時(shí)候
- 銷(xiāo)毀時(shí)間
- 線程結(jié)束的時(shí)候
擴(kuò)展1:局部變量表
局部變量表用于存放編譯期可知的各種基本數(shù)據(jù)類(lèi)型愚战、對(duì)象引用、returnAddress類(lèi)型(一條字節(jié)碼指令的地址)。
對(duì)于基本數(shù)據(jù)類(lèi)型,存放的是變量的名和值悄但;
對(duì)于引用類(lèi)型澜薄,存放的是指向?qū)ο笤诙阎械钠鹗嫉刂贰?/p>
ps: 對(duì)于64位的long或double類(lèi)型的局部變量會(huì)占用兩個(gè)局部變量表空間(Slot),其余的數(shù)據(jù)類(lèi)型都是只占用一個(gè)局部變量表空間。
局部變量表所需要的空間在編譯期間已經(jīng)計(jì)算好了佃迄,在一個(gè)方法執(zhí)行時(shí),需要為棧幀分配多少局部變量表空間是完全確定的。
本地方法棧
本地方法棧的特性和虛擬機(jī)棧幾乎一樣断序。
- 本地方法棧與虛擬機(jī)棧的區(qū)別
- 本地方法棧為本地方法服務(wù)
- 本地方法棧可能出現(xiàn)的異常
- 同虛擬機(jī)棧一樣可能拋出
StackOverflowError
和OutOfMemoryError
異常糜烹。
- 同虛擬機(jī)棧一樣可能拋出
堆
- 描述
- 堆內(nèi)存的唯一目的是存放對(duì)象實(shí)例违诗。
- 堆內(nèi)存是垃圾收集的主要區(qū)域,因此也叫GC堆
- 作用
- 存放對(duì)象實(shí)例
- 特點(diǎn)
- 虛擬機(jī)所管理的內(nèi)存中最大的一塊
- 幾乎所有的對(duì)象都在堆區(qū)分配內(nèi)存疮蹦,當(dāng)然也有例外诸迟,JIT編譯器有可能會(huì)進(jìn)行優(yōu)化,直接在棧上分配愕乎,有關(guān)信息可以直接搜索“逃逸分析”了解阵苇,這不在本文的討論范圍內(nèi)。
- 所有線程共享的一塊內(nèi)存區(qū)域
- 堆內(nèi)存在物理上不一定是連續(xù)的感论,保證邏輯連續(xù)即可
- 堆內(nèi)存區(qū)域無(wú)法滿足分配對(duì)象實(shí)例所需內(nèi)存慎玖,可能拋出OutOfMemoryError異常
- 堆內(nèi)存設(shè)置固定大小也可以動(dòng)態(tài)擴(kuò)展,可在啟動(dòng)參數(shù)上指定最小大小及擴(kuò)容的上限笛粘。
- 創(chuàng)建時(shí)間
- 虛擬機(jī)啟動(dòng)的時(shí)候就創(chuàng)建了堆內(nèi)存
擴(kuò)展1: 堆區(qū)細(xì)分
jvm為了垃圾回收的方便趁怔,將堆劃分為新生代
和老年代
,新創(chuàng)建的對(duì)象基本上都放在新生代中湿硝,而存活比較久的對(duì)象則會(huì)移到老年代中。新生代和老年代采用不同的垃圾收集算法润努,可以更高效地回收內(nèi)存关斜。采用復(fù)制算法的新生代還可以細(xì)分為Eden
、From Survivor
和To Survivor
铺浇。具體的詳情是怎樣的痢畜,為了不偏離這篇文章的主旨,這里先打個(gè)問(wèn)號(hào)鳍侣,后序的文章將會(huì)詳細(xì)介紹堆區(qū)的幾個(gè)劃分的用途丁稀。
堆區(qū)雖然是線程共享的,但是如果設(shè)定了啟動(dòng)參數(shù)-XX:+UseTLAB
倚聚,則開(kāi)啟了本地線程分配緩沖(Thread local Allocation Buffer, TLAB)线衫,會(huì)為每個(gè)線程單獨(dú)在堆中劃分出一個(gè)TLAB
,哪個(gè)線程需要分配內(nèi)存惑折,就先在該線程對(duì)應(yīng)的TLAB中分配內(nèi)存授账,當(dāng)TLAB用完,才在堆區(qū)的Eden
中繼續(xù)申請(qǐng)一塊TLAB
惨驶。
方法區(qū)
方法區(qū)是用于存放虛擬機(jī)加載的類(lèi)信息白热、常量、靜態(tài)變量粗卜、編譯后的代碼等數(shù)據(jù)屋确。
方法區(qū)特點(diǎn):
- 線程共享
- 方法區(qū)大小可固定也可以動(dòng)態(tài)擴(kuò)展。
- 與堆區(qū)一樣不需要連續(xù)的物理內(nèi)存续扔,但要求邏輯連續(xù)攻臀。
- 該區(qū)域的垃圾收集目標(biāo)主要是針對(duì)運(yùn)行時(shí)常量池的回收和對(duì)類(lèi)進(jìn)行卸載。
- 可能出現(xiàn)
OutOfMemoryError
異常测砂。
擴(kuò)展1:運(yùn)行時(shí)常量池:
class文件中有個(gè)常量池茵烈,運(yùn)行時(shí)常量池就是class文件中常量池經(jīng)過(guò)類(lèi)加載后存放的內(nèi)存區(qū)域。
常量池主要存放兩類(lèi)常量:字面量和符號(hào)引用砌些。
字面量指字符串呜投,聲明為final的常量值等;而符號(hào)引用是java編譯后生成的各種常量存璃,其包括:
- 類(lèi)和接口的全限定名
- 成員變量的名稱(chēng)和描述符
- 方法的名稱(chēng)和描述符
jdk1.8
之前仑荐,方法區(qū)是用永久代
實(shí)現(xiàn)的,
在jdk1.7
以下的版本纵东,運(yùn)行時(shí)常量池是方法區(qū)的一部分粘招,而jdk1.7
及之后的版本,運(yùn)行時(shí)常量池中的字符串常量池已經(jīng)不在方法區(qū)偎球,而是在java堆中開(kāi)辟了一塊區(qū)域作為字符串常量池洒扎。
在jdk1.8開(kāi)始辑甜,已經(jīng)沒(méi)有永久代的概念,譬如符號(hào)引用(Symbols)轉(zhuǎn)移到了native 堆中的元空間袍冷;字面量也在 java heap磷醋;類(lèi)的靜態(tài)變量(class statics)轉(zhuǎn)移到了java heap
擴(kuò)展2:常量是否只能在編譯期產(chǎn)生?
否胡诗,運(yùn)行期也可能將新的常量放入運(yùn)行時(shí)常量池中邓线,比如String
的intern
方法。在jdk1.7的表現(xiàn)如下:
// 如果運(yùn)行時(shí)常量池中煌恢,存在"10"這個(gè)字符串常量
// 則將常量池中的字符串對(duì)象返回骇陈,
// 如果不存在,則直接在運(yùn)行時(shí)常量池中創(chuàng)建“10"這個(gè)字符串瑰抵,并將其返回你雌。
String s = String.valueOf(10).intern();
直接內(nèi)存
前面講的幾塊都屬于虛擬機(jī)管理的運(yùn)行時(shí)數(shù)據(jù)區(qū)域,java程序中也有可能會(huì)用到不是虛擬機(jī)運(yùn)行時(shí)內(nèi)存區(qū)域的一部分谍憔。這塊內(nèi)存我們通常稱(chēng)為直接內(nèi)存
直接內(nèi)存不受java堆大小的限制匪蝙,但是受本機(jī)物理內(nèi)存的限制主籍。
直接內(nèi)存也可能導(dǎo)致出現(xiàn)OutOfMemoryError異常习贫。
直接內(nèi)存的例子:
jdk 1.4 加入的NIO類(lèi),引入了一種基于通道Channel
和緩沖區(qū)Buffer
的IO方式千元。直接通過(guò)Native方法在java堆外的直接內(nèi)存
中分配內(nèi)存, 通過(guò)存儲(chǔ)在java堆中的DirectByteBuffer對(duì)象作為這塊直接內(nèi)存的引用苫昌。操作DirectByteBuffer即可操作直接內(nèi)存,這樣做的好處是避免了要使用直接內(nèi)存的時(shí)候需要先復(fù)制到j(luò)ava堆中幸海。直接操作直接內(nèi)存更加高效祟身。
點(diǎn)贊是對(duì)我最大的鼓勵(lì)