Java程序是交由JVM執(zhí)行的汰蜘,所以我們在談Java內(nèi)存區(qū)域劃分的時候事實上是指JVM內(nèi)存區(qū)域劃分雀扶。在討論JVM內(nèi)存區(qū)域劃分之前残揉,先來看一下Java程序具體執(zhí)行的過程:
首先Java源代碼文件(.java后綴)會被Java編譯器編譯為字節(jié)碼文件(.class后綴)逸月,然后由JVM中的類加載器加載各個類的字節(jié)碼文件矩屁,加載完畢之后,交由JVM執(zhí)行引擎執(zhí)行舔琅。在整個程序執(zhí)行過程中等恐,JVM會用一段空間來存儲程序執(zhí)行期間需要用到的數(shù)據(jù)和相關(guān)信息,這段空間一般被稱作為Runtime Data Area(運(yùn)行時數(shù)據(jù)區(qū))备蚓,也就是我們常說的JVM內(nèi)存课蔬。因此,在Java中我們常常說到的內(nèi)存管理就是針對這段空間進(jìn)行管理(如何分配和回收內(nèi)存空間)
運(yùn)行時數(shù)據(jù)區(qū)包括:
我們常說的Java內(nèi)存管理就是指這塊區(qū)域的內(nèi)存分配和回收郊尝,那么二跋,這塊兒區(qū)域具體是怎么劃分的呢?
根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定流昏,運(yùn)行時數(shù)據(jù)區(qū)通常包括這幾個部分:
程序計數(shù)器(ProgramCounter Register)
Java棧(VM Stack)
本地方法棧(Native MethodStack)
方法區(qū)(Method Area)
堆(Heap)
根據(jù)《Java虛擬機(jī)規(guī)范》的規(guī)定扎即,運(yùn)行時數(shù)據(jù)區(qū)通常包括這幾個部分:程序計數(shù)器(Program Counter Register)、Java棧(VM Stack)况凉、本地方法棧(Native Method Stack)谚鄙、方法區(qū)(Method Area)、堆(Heap)刁绒。
1.程序計數(shù)器
程序計數(shù)器(Program Counter Register)闷营,也有稱作為PC寄存器。想必學(xué)過匯編語言的朋友對程序計數(shù)器這個概念并不陌生,在匯編語言中傻盟,程序計數(shù)器是指CPU中的寄存器速蕊,它保存的是程序當(dāng)前執(zhí)行的指令的地址(也可以說保存下一條指令的所在存儲單元的地址,一塊較小的內(nèi)存空間)娘赴,當(dāng)CPU需要執(zhí)行指令時规哲,需要從程序計數(shù)器中得到當(dāng)前需要執(zhí)行的指令所在存儲單元的地址,然后根據(jù)得到的地址獲取到指令诽表,在得到指令之后媳叨,程序計數(shù)器便自動加1或者根據(jù)轉(zhuǎn)移指針得到下一條指令的地址,如此循環(huán)关顷,直至執(zhí)行完所有的指令。
雖然JVM中的程序計數(shù)器并不像匯編語言中的程序計數(shù)器一樣是物理概念上的CPU寄存器武福,但是JVM中的程序計數(shù)器的功能跟匯編語言中的程序計數(shù)器的功能在邏輯上是等同的议双,也就是說是用來指示 執(zhí)行哪條指令的。
由于在JVM中捉片,多線程是通過線程輪流切換來獲得CPU執(zhí)行時間的平痰,因此,在任一具體時刻伍纫,一個CPU的內(nèi)核只會執(zhí)行一條線程中的指令宗雇,因此,為了能夠使得每個線程都在線程切換后能夠恢復(fù)在切換之前的程序執(zhí)行位置莹规,每個線程都需要有自己獨(dú)立的程序計數(shù)器赔蒲,并且不能互相被干擾,否則就會影響到程序的正常執(zhí)行次序良漱。因此舞虱,可以這么說,程序計數(shù)器是每個線程所私有的母市。
在JVM規(guī)范中規(guī)定矾兜,如果線程執(zhí)行的是非native方法,則程序計數(shù)器中保存的是當(dāng)前需要執(zhí)行的指令的地址患久;如果線程執(zhí)行的是native方法椅寺,則程序計數(shù)器中的值是undefined。
由于程序計數(shù)器中存儲的數(shù)據(jù)所占空間的大小不會隨程序的執(zhí)行而發(fā)生改變蒋失,因此返帕,對于程序計數(shù)器是不會發(fā)生內(nèi)存溢出現(xiàn)象(OutOfMemory)的。
2.Java棧
Java棧也稱作虛擬機(jī)棧(Java Vitual Machine Stack)高镐,該區(qū)域也是線程私有的溉旋,它的生命周期也與線程相同。也就是我們常常所說的棧嫉髓,跟C語言的數(shù)據(jù)段中的棧類似观腊。事實上邑闲,Java棧是Java方法執(zhí)行的內(nèi)存模型。為什么這么說呢梧油?下面就來解釋一下其中的原因苫耸。
Java棧中存放的是一個個的棧幀,每個棧幀對應(yīng)一個被調(diào)用的方法儡陨,在棧幀中包括局部變量表(Local Variables)褪子、操作數(shù)棧(Operand Stack)、指向當(dāng)前方法所屬的類的運(yùn)行時常量池(運(yùn)行時常量池的概念在方法區(qū)部分會談到)的引用(Reference to runtime constant pool)骗村、方法返回地址(Return Address)和一些額外的附加信息嫌褪。當(dāng)線程執(zhí)行一個方法時,就會隨之創(chuàng)建一個對應(yīng)的棧幀胚股,并將建立的棧幀壓棧笼痛。當(dāng)方法執(zhí)行完畢之后,便會將棧幀出棧琅拌。因此可知缨伊,線程當(dāng)前執(zhí)行的方法所對應(yīng)的棧幀必定位于Java棧的頂部。講到這里进宝,大家就應(yīng)該會明白為什么 在 使用 遞歸方法的時候容易導(dǎo)致棧內(nèi)存溢出的現(xiàn)象了以及為什么棧區(qū)的空間不用程序員去管理了(當(dāng)然在Java中刻坊,程序員基本不用關(guān)心內(nèi)存分配和釋放的事情,因為Java有自己的垃圾回收機(jī)制)党晋,這部分空間的分配和釋放都是由系統(tǒng)自動實施的谭胚。對于所有的程序設(shè)計語言來說,棧這部分空間對程序員來說是不透明的未玻。
下圖表示了一個Java棧的模型:
在 Java 虛擬機(jī)規(guī)范中漏益,對這個區(qū)域規(guī)定了兩種異常情況:
- 如果線程請求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常深胳。
- 如果虛擬機(jī)在動態(tài)擴(kuò)展棧時無法申請到足夠的內(nèi)存空間绰疤,則拋出OutOfMemoryError異常。
這兩種情況存在著一些互相重疊的地方:當(dāng)椢柚眨空間無法繼續(xù)分配時轻庆,到底是內(nèi)存太小,還是已使用的椓踩埃空間太大余爆,其本質(zhì)上只是對同一件事情的兩種描述而已。在單線程的操作中夸盟,無論是由于棧幀太大蛾方,還是虛擬機(jī)棧空間太小,當(dāng)椬椋空間無法分配時拓春,虛擬機(jī)拋出的都是 StackOverflowError 異常,而不會得到 OutOfMemoryError 異常亚隅。而在多線程環(huán)境下硼莽,則會拋出 OutOfMemoryError 異常。
下面詳細(xì)說明棧幀中所存放的各部分信息的作用和數(shù)據(jù)結(jié)構(gòu)煮纵。
局部變量表是一組變量值存儲空間懂鸵,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量,其中存放的數(shù)據(jù)的類型是編譯期可知的各種基本數(shù)據(jù)類型行疏、對象引用(reference)和returnAddress類型(它指向了一條字節(jié)碼指令的地址)匆光。局部變量表所需的內(nèi)存空間在編譯期間完成分配,即在Java程序被編譯成Class文件時酿联,就確定了所需分配的最大局部變量表的容量殴穴。當(dāng)進(jìn)入一個方法時,這個方法需要在棧中分配多大的局部變量空間是完全確定的货葬,在方法運(yùn)行期間不會改變局部變量表的大小。
局部變量表的容量以變量槽(Slot)為最小單位劲够。在虛擬機(jī)規(guī)范中并沒有明確指明一個Slot應(yīng)占用的內(nèi)存空間大姓鹜啊(允許其隨著處理器、操作系統(tǒng)或虛擬機(jī)的不同而發(fā)生變化)征绎,一個Slot可以存放一個32位以內(nèi)的數(shù)據(jù)類型:boolean蹲姐、byte、char人柿、short柴墩、int、float凫岖、reference和returnAddresss江咳。reference是對象的引用類型,returnAddress是為字節(jié)指令服務(wù)的哥放,它執(zhí)行了一條字節(jié)碼指令的地址歼指。對于64位的數(shù)據(jù)類型(long和double),虛擬機(jī)會以高位在前的方式為其分配兩個連續(xù)的Slot空間甥雕。
虛擬機(jī)通過索引定位的方式使用局部變量表踩身,索引值的范圍是從0開始到局部變量表最大的Slot數(shù)量,對于32位數(shù)據(jù)類型的變量社露,索引n代表第n個Slot挟阻,對于64位的,索引n代表第n和第n+1兩個Slot。
在方法執(zhí)行時附鸽,虛擬機(jī)是使用局部變量表來完成參數(shù)值到參數(shù)變量列表的傳遞過程的脱拼,如果是實例方法(非static),則局部變量表中的第0位索引的Slot默認(rèn)是用于傳遞方法所屬對象實例的引用拒炎,在方法中可以通過關(guān)鍵字“this”來訪問這個隱含的參數(shù)挪拟。其余參數(shù)則按照參數(shù)表的順序來排列,占用從1開始的局部變量Slot击你,參數(shù)表分配完畢后玉组,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的Slot。
局部變量表中的Slot是可重用的丁侄,方法體中定義的變量惯雳,作用域并不一定會覆蓋整個方法體,如果當(dāng)前字節(jié)碼PC計數(shù)器的值已經(jīng)超過了某個變量的作用域鸿摇,那么這個變量對應(yīng)的Slot就可以交給其他變量使用石景。這樣的設(shè)計不僅僅是為了節(jié)省空間,在某些情況下Slot的復(fù)用會直接影響到系統(tǒng)的而垃圾收集行為拙吉。操作數(shù)棧潮孽,棧最典型的一個應(yīng)用就是用來對表達(dá)式求值。想想一個線程執(zhí)行方法的過程中筷黔,實際上就是不斷執(zhí)行語句的過程往史,而歸根到底就是進(jìn)行計算的過程。因此可以這么說佛舱,程序中的所有計算過程都是在借助于操作數(shù)棧來完成的椎例。 操作數(shù)棧又常被稱為操作棧,操作數(shù)棧的最大深度也是在編譯的時候就確定了请祖。32位數(shù)據(jù)類型所占的棧容量為1,64為數(shù)據(jù)類型所占的棧容量為2订歪。當(dāng)一個方法開始執(zhí)行時,它的操作棧是空的肆捕,在方法的執(zhí)行過程中刷晋,會有各種字節(jié)碼指令(比如:加操作、賦值元算等)向操作棧中寫入和提取內(nèi)容慎陵,也就是入棧和出棧操作掏秩。
Java 虛擬機(jī)的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“椌D罚”就是操作數(shù)棧蒙幻。因此我們也稱 Java 虛擬機(jī)是基于棧的,這點(diǎn)不同于 Android 虛擬機(jī)胆筒,Android 虛擬機(jī)是基于寄存器的邮破。
基于棧的指令集最主要的優(yōu)點(diǎn)是可移植性強(qiáng)诈豌,主要的缺點(diǎn)是執(zhí)行速度相對會慢些;而由于寄存器由硬件直接提供抒和,所以基于寄存器指令集最主要的優(yōu)點(diǎn)是執(zhí)行速度快矫渔,主要的缺點(diǎn)是可移植性差。指向運(yùn)行時常量池的引用(動態(tài)連接)摧莽,因為在方法執(zhí)行的過程中有可能需要用到類中的常量庙洼,所以必須要有一個引用指向運(yùn)行時常量。每個棧幀都包含一個指向運(yùn)行時常量池(在方法區(qū)中镊辕,后面介紹)中該棧幀所屬方法的引用油够,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接。Class文件的常量池中存在有大量的符號引用征懈,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號引用為參數(shù)石咬。這些符號引用,一部分會在類加載階段或第一次使用的時候轉(zhuǎn)化為直接引用(如final卖哎、static域等)鬼悠,稱為靜態(tài)解析,另一部分將在每一次的運(yùn)行期間轉(zhuǎn)化為直接引用亏娜,這部分稱為動態(tài)連接焕窝。
方法返回地址,當(dāng)一個方法被執(zhí)行后维贺,有兩種方式退出該方法:執(zhí)行引擎遇到了任意一個方法返回的字節(jié)碼指令或遇到了異常它掂,并且該異常沒有在方法體內(nèi)得到處理。無論采用何種退出方式幸缕,在方法退出之后,都需要返回到方法被調(diào)用的位置晰韵,程序才能繼續(xù)執(zhí)行发乔。方法返回時可能需要在棧幀中保存一些信息,用來幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)雪猪。一般來說栏尚,方法正常退出時,調(diào)用者的 PC 計數(shù)器的值就可以作為返回地址只恨,棧幀中很可能保存了這個計數(shù)器值译仗,而方法異常退出時,返回地址是要通過異常處理器來確定的官觅,棧幀中一般不會保存這部分信息纵菌。
方法退出的過程實際上等同于把當(dāng)前棧幀出站,因此退出時可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧休涤,如果有返回值咱圆,則把它壓入調(diào)用者棧幀的操作數(shù)棧中笛辟,調(diào)整 PC 計數(shù)器的值以指向方法調(diào)用指令后面的一條指令。
3.本地方法棧
本地方法棧與Java棧的作用和原理非常相似序苏。區(qū)別只不過是Java棧是為執(zhí)行Java方法服務(wù)的手幢,而本地方法棧則是為執(zhí)行本地方法(Native Method)服務(wù)的。在JVM規(guī)范中忱详,并沒有對本地方發(fā)展的具體實現(xiàn)方法以及數(shù)據(jù)結(jié)構(gòu)作強(qiáng)制規(guī)定围来,虛擬機(jī)可以自由實現(xiàn)它。在HotSopt虛擬機(jī)中直接就把本地方法棧和Java棧合二為一匈睁。
4.堆
在C語言中监透,堆這部分空間是唯一一個程序員可以管理的內(nèi)存區(qū)域。程序員可以通過malloc函數(shù)和free函數(shù)在堆上申請和釋放空間软舌。那么在Java中是怎么樣的呢才漆?
Java中的堆是 Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊,用來存儲對象本身以及數(shù)組(當(dāng)然佛点,數(shù)組引用是存放在Java棧中的)醇滥。只不過和C語言中的不同,在Java中超营,程序員基本不用去關(guān)心空間釋放的問題鸳玩,Java的垃圾回收機(jī)制會自動進(jìn)行處理。因此這部分空間也是Java垃圾收集器管理的主要區(qū)域演闭,也被稱為“GC堆”不跟。另外,堆是被所有線程共享的米碰,在JVM中只有一個堆窝革。
根據(jù) Java 虛擬機(jī)規(guī)范的規(guī)定,Java 堆可以處在物理上不連續(xù)的內(nèi)存空間中吕座,只要邏輯上是連續(xù)的即可虐译。如果在堆中沒有內(nèi)存可分配時,并且堆也無法擴(kuò)展時吴趴,將會拋出 OutOfMemoryError 異常漆诽。
Java 堆從 GC 的角度還可以細(xì)分為: 新生代(Eden 區(qū)、From Survivor 區(qū)和 To Survivor 區(qū))和老年
代锣枝。
4.1. 新生代
是用來存放新生的對象厢拭。一般占據(jù)堆的 1/3 空間。由于頻繁創(chuàng)建對象撇叁,所以新生代會頻繁觸發(fā)
MinorGC 進(jìn)行垃圾回收供鸠。新生代又分為 Eden 區(qū)、ServivorFrom陨闹、ServivorTo 三個區(qū)回季。
4.1.1. Eden 區(qū)
Java 新對象的出生地(如果新創(chuàng)建的對象占用內(nèi)存很大家制,則直接分配到老
年代)。當(dāng) Eden 區(qū)內(nèi)存不夠的時候就會觸發(fā) MinorGC泡一,對新生代區(qū)進(jìn)行
一次垃圾回收颤殴。
4.1.2. ServivorFrom
上一次 GC 的幸存者,作為這一次 GC 的被掃描者鼻忠。
4.1.3. ServivorTo
保留了一次 MinorGC 過程中的幸存者涵但。
4.1.4. MinorGC 的過程(復(fù)制->清空->互換)
MinorGC 采用復(fù)制算法。
1:eden帖蔓、servicorFrom 復(fù)制到 ServicorTo矮瘟,年齡+1
首先,把 Eden 和 ServivorFrom 區(qū)域中存活的對象復(fù)制到 ServicorTo 區(qū)域(如果有對象的年
齡以及達(dá)到了老年的標(biāo)準(zhǔn)塑娇,則賦值到老年代區(qū))澈侠,同時把這些對象的年齡+1(如果 ServicorTo 不
夠位置了就放到老年區(qū));
2:清空 eden埋酬、servicorFrom
然后哨啃,清空 Eden 和 ServicorFrom 中的對象;
3:ServicorTo 和 ServicorFrom 互換
最后写妥,ServicorTo 和 ServicorFrom 互換拳球,原 ServicorTo 成為下一次 GC 時的 ServicorFrom
區(qū)。
4.2. 老年代
主要存放應(yīng)用程序中生命周期長的內(nèi)存對象珍特。
老年代的對象比較穩(wěn)定祝峻,所以 MajorGC 不會頻繁執(zhí)行。在進(jìn)行 MajorGC 前一般都先進(jìn)行
了一次 MinorGC扎筒,使得有新生代的對象晉身入老年代莱找,導(dǎo)致空間不夠用時才觸發(fā)。當(dāng)無法找到足
夠大的連續(xù)空間分配給新創(chuàng)建的較大對象時也會提前觸發(fā)一次 MajorGC 進(jìn)行垃圾回收騰出空間嗜桌。
MajorGC 采用標(biāo)記清除算法:首先掃描一次所有老年代奥溺,標(biāo)記出存活的對象衰腌,然后回收沒
有標(biāo)記的對象莉掂。MajorGC 的耗時比較長,因為要掃描再回收。MajorGC 會產(chǎn)生內(nèi)存碎片诱篷,為了減
少內(nèi)存損耗,我們一般需要進(jìn)行合并或者標(biāo)記出來方便下次直接分配雳灵。當(dāng)老年代也滿了裝不下的
時候棕所,就會拋出 OOM(Out of Memory)異常。
4.3. 永久代
指內(nèi)存的永久保存區(qū)域悯辙,主要存放 Class 和 Meta(元數(shù)據(jù))的信息,Class 在被加載的時候被
放入永久區(qū)域琳省,它和和存放實例的區(qū)域不同,GC 不會在主程序運(yùn)行期對永久區(qū)域進(jìn)行清理迎吵。所以這
也導(dǎo)致了永久代的區(qū)域會隨著加載的 Class 的增多而脹滿,最終拋出 OOM 異常针贬。
JAVA8 與元數(shù)據(jù)
在 Java8 中击费,永久代已經(jīng)被移除,被一個稱為“元數(shù)據(jù)區(qū)”(元空間)的區(qū)域所取代桦他。元空間
的本質(zhì)和永久代類似蔫巩,元空間與永久代之間最大的區(qū)別在于:元空間并不在虛擬機(jī)中,而是使用
本地內(nèi)存快压。因此圆仔,默認(rèn)情況下,元空間的大小僅受本地內(nèi)存限制蔫劣。類的元數(shù)據(jù)放入 native
memory, 字符串池和類的靜態(tài)變量放入 java 堆中坪郭,這樣可以加載多少類的元數(shù)據(jù)就不再由
MaxPermSize 控制, 而由系統(tǒng)的實際可用空間來控制。
5.方法區(qū)
方法區(qū)在JVM中也是一個非常重要的區(qū)域脉幢,它與堆一樣歪沃,是被線程共享的區(qū)域。在方法區(qū)中鸵隧,存儲了每個類的信息(包括類的名稱绸罗、方法信息、字段信息)豆瘫、靜態(tài)變量珊蟀、常量以及編譯器編譯后的代碼等。
在Class文件中除了類的字段外驱、方法育灸、接口等描述信息外,還有一項信息是常量池昵宇,用來存儲編譯期間生成的字面量和符號引用磅崭。
在方法區(qū)中有一個非常重要的部分就是運(yùn)行時常量池,它是每一個類或接口的常量池的運(yùn)行時表示形式瓦哎,在類和接口被加載到JVM后砸喻,對應(yīng)的運(yùn)行時常量池就被創(chuàng)建出來。當(dāng)然并非Class文件常量池中的內(nèi)容才能進(jìn)入運(yùn)行時常量池蒋譬,在運(yùn)行期間也可將新的常量放入運(yùn)行時常量池中割岛,比如String的intern方法。
(intern()方法介紹:https://blog.csdn.net/q5706503/article/details/84586219)
【運(yùn)行時常量池】關(guān)于這個東西要明白三個概念:
常量池(Constant Pool):常量池數(shù)據(jù)編譯期被確定犯助,是Class文件中的一部分癣漆。存儲了類、方法剂买、接口等中的常量惠爽,當(dāng)然也包括字符串常量癌蓖。
字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存儲編譯期類中產(chǎn)生的字符串類型數(shù)據(jù)婚肆。
運(yùn)行時常量池(Runtime Constant Pool):方法區(qū)的一部分租副,所有線程共享。虛擬機(jī)加載Class后把常量池中的數(shù)據(jù)放入到運(yùn)行時常量池较性。
在JVM規(guī)范中附井,沒有強(qiáng)制要求方法區(qū)必須實現(xiàn)垃圾回收。很多人習(xí)慣將方法區(qū)稱為“永久代”两残,是因為HotSpot虛擬機(jī)以永久代來實現(xiàn)方法區(qū)永毅,從而JVM的垃圾收集器可以像管理堆區(qū)一樣管理這部分區(qū)域,從而不需要專門為這部分設(shè)計垃圾回收機(jī)制人弓。不過自從JDK7之后沼死,Hotspot虛擬機(jī)便將運(yùn)行時常量池從永久代移除了。
6崔赌、Java中哪些組件用到內(nèi)存
Java堆
Java堆用于存儲Java對象意蛀,在JVM啟動時就一次性申請到固定大小的空間,所以健芭,一旦分配县钥,大小不變。
內(nèi)存空間管理:JVM
對象創(chuàng)建:Java應(yīng)用程序
對象所占空間釋放:垃圾收集器
線程
JVM運(yùn)行實際程序的實體就是線程慈迈,每個線程創(chuàng)建的時候JVM都為它創(chuàng)建了私有的堆棧和程序計數(shù)器(或者叫做PC寄存器)若贮;很多應(yīng)用程序是根據(jù)CPU的核數(shù)來分配創(chuàng)建的線程數(shù)。
類和類加載器
Java中的類和類加載器同樣需要存儲空間痒留,被存儲在永久代(PermGen區(qū))當(dāng)中谴麦。
JVM加載類方式:按需加載,只加載那些你在程序中明確使用到的類伸头,通常只加載一次匾效,如果一直重復(fù)加載,可能會導(dǎo)致內(nèi)存泄露恤磷,所以也要注意對PernGen區(qū)失效類的卸載內(nèi)存回收問題面哼。
通常PernGen區(qū)滿足內(nèi)存回收的條件為:
1) 堆中沒有對該類加載器的引用;(java.lang.ClassLoader對象)
2) 堆中沒有對類加載器加載的類的引用扫步;(java.lang.Class對象)
3) 該類加載器加載的類的所有實例化的對象不再存活魔策。
NIO
NIO使用java.nio.ByteBuffer.allocateDirect()方法分配內(nèi)存,每次分配內(nèi)存都會調(diào)用操作系統(tǒng)函數(shù)os::malloc()锌妻,所以代乃,分配的內(nèi)存是本機(jī)的內(nèi)存而不是Java堆上的內(nèi)存旬牲;
另外利用該方法產(chǎn)生的數(shù)據(jù)和網(wǎng)絡(luò)仿粹、磁盤發(fā)生交互的時候都是在內(nèi)核空間發(fā)生的搁吓,不需要復(fù)制到用戶空間Java內(nèi)存中,這種技術(shù)避免了Java堆和本機(jī)堆之間的數(shù)據(jù)復(fù)制吭历;但是利用該方法生成的數(shù)據(jù)會作為Java堆GC的一部分來自動清理本機(jī)緩沖區(qū)堕仔。
JNI
JNI技術(shù)使本機(jī)代碼可調(diào)用java代碼,Java代碼的運(yùn)行本身也依賴于JNI代碼來實現(xiàn)類庫功能晌区,所以JNI也增加內(nèi)存占用摩骨。
直接內(nèi)存(Direct Memory)
直接內(nèi)存(Direct Memory)也叫堆外內(nèi)存,并不是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分朗若,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域恼五,它直接從操作系統(tǒng)中分配,因此不受Java堆大小的限制哭懈,但是會受到本機(jī)總內(nèi)存的大小及處理器尋址空間的限制灾馒,因此它也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。在JDK1.4中新引入了NIO機(jī)制遣总,它是一種基于通道與緩沖區(qū)的新I/O方式睬罗,可以直接從操作系統(tǒng)中分配直接內(nèi)存,即在堆外分配內(nèi)存旭斥,這樣能在一些場景中提高性能容达,因為避免了在Java堆和Native堆中來回復(fù)制數(shù)據(jù)。
Direct Memory的回收機(jī)制:
Direct Memory是受GC控制的垂券,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024)花盐,這段代碼的執(zhí)行會在堆外占用1k的內(nèi)存,Java堆內(nèi)只會占用一個對象的指針引用的大小菇爪,堆外的這1k的空間只有當(dāng)bb對象被回收時卒暂,才會被回收,這里會發(fā)現(xiàn)一個明顯的不對稱現(xiàn)象娄帖,就是堆外可能占用了很多也祠,而堆內(nèi)沒占用多少,導(dǎo)致還沒觸發(fā)GC近速,那就很容易出現(xiàn)Direct Memory造成物理內(nèi)存耗光诈嘿。
JDK中使用DirectByteBuffer對象來表示堆外內(nèi)存,每個DirectByteBuffer對象在初始化時削葱,都會創(chuàng)建一個對應(yīng)的Cleaner對象奖亚,用于保存堆外內(nèi)存的元信息(開始地址、大小和容量等)析砸,當(dāng)DirectByteBuffer被GC回收后昔字,Cleaner對象被放入ReferenceQueue中,然后由ReferenceHandler守護(hù)線程調(diào)用unsafe.freeMemory(address),回收堆外內(nèi)存作郭。 ( 在Cleaner 內(nèi)部中通過一個列表陨囊,維護(hù)了一個針對每一個 directBuffer 的一個回收堆外內(nèi)存的 線程對象(Runnable),回收操作是發(fā)生在 Cleaner 的 clean() 方法中夹攒。)
Direct ByteBuffer分配出去的內(nèi)存其實也是由GC負(fù)責(zé)回收的蜘醋,而不像Unsafe是完全自行管理的,Hotspot在GC時會掃描Direct ByteBuffer對象是否有引用咏尝,如沒有則同時也會回收其占用的堆外內(nèi)存压语。
主動回收: 對于Sun的JDK,只要從DirectByteBuffer里取出那個sun.misc.Cleaner编检,然后調(diào)用它的clean()就行胎食;
基于 GC 回收:堆內(nèi)的DirectByteBuffer對象被GC時,會調(diào)用cleaner回收其引用的堆外內(nèi)存允懂。問題是YGC只會將將新生代里的不可達(dá)的DirectByteBuffer對象及其堆外內(nèi)存回收斥季,如果有大量的DirectByteBuffer對象移到了old區(qū),但是又一直沒有做CMS GC或者FGC累驮,而只進(jìn)行YGC酣倾,物理內(nèi)存會被慢慢耗光,觸發(fā)OutOfMemoryError(水平有限, 這句話還沒懂) 因此為了避免這種悲劇的發(fā)生谤专,通過-XX:MaxDirectMemorySize來指定最大的堆外內(nèi)存大小躁锡,當(dāng)使用達(dá)到了閾值的時候?qū)⒄{(diào)用System.gc來做一次full gc,以此來回收掉沒有被使用的堆外內(nèi)存置侍。
詳細(xì)些的回收機(jī)制參考下連接:
Direct Memory的好處:
- 可以擴(kuò)展至更大的內(nèi)存空間映之。比如超過1TB甚至比主存還大的空間
- 理論上能減少GC暫停時間(節(jié)約了大量的堆內(nèi)內(nèi)存)
- 可以在進(jìn)程間共享,減少JVM間的對象復(fù)制蜡坊,使得JVM的分割部署更容易實現(xiàn)
- 它的持久化存儲可以支持快速重啟杠输,同時還能夠在測試環(huán)境中重現(xiàn)生產(chǎn)數(shù)據(jù)
-
堆外內(nèi)存能夠提升IO效率
堆內(nèi)內(nèi)存由JVM管理,屬于“用戶態(tài)”秕衙;而堆外內(nèi)存由OS管理蠢甲,屬于“內(nèi)核態(tài)”。
如果從堆內(nèi)向磁盤寫數(shù)據(jù)時据忘,數(shù)據(jù)會被先復(fù)制到堆外內(nèi)存鹦牛,即內(nèi)核緩沖區(qū),然后再由OS寫入磁盤勇吊,使用堆外內(nèi)存避免了數(shù)據(jù)從用戶內(nèi)向內(nèi)核態(tài)的拷貝曼追。
內(nèi)存溢出
下面給出個內(nèi)存區(qū)域內(nèi)存溢出的簡單測試方法。
這里有一點(diǎn)要重點(diǎn)說明汉规,在多線程情況下礼殊,給每個線程的棧分配的內(nèi)存越大,反而越容易產(chǎn)生內(nèi)存溢出異常。操作系統(tǒng)為每個進(jìn)程分配的內(nèi)存是有限制的晶伦,虛擬機(jī)提供了參數(shù)來控制 Java 堆和方法區(qū)這兩部分內(nèi)存的最大值碟狞,忽略掉程序計數(shù)器消耗的內(nèi)存(很小)坝辫,以及進(jìn)程本身消耗的內(nèi)存,剩下的內(nèi)存便給了虛擬機(jī)棧和本地方法棧射亏,每個線程分配到的棧容量越大近忙,可以建立的線程數(shù)量自然就越少。因此智润,如果是建立過多的線程導(dǎo)致的內(nèi)存溢出及舍,在不能減少線程數(shù)的情況下,就只能通過減少最大堆和每個線程的棧容量來換取更多的線程窟绷。
另外锯玛,由于 Java 堆內(nèi)也可能發(fā)生內(nèi)存泄露(Memory Leak),這里簡要說明一下內(nèi)存泄露和內(nèi)存溢出的區(qū)別:
內(nèi)存泄露是指分配出去的內(nèi)存沒有被回收回來兼蜈,由于失去了對該內(nèi)存區(qū)域的控制攘残,因而造成了資源的浪費(fèi)。Java 中一般不會產(chǎn)生內(nèi)存泄露为狸,因為有垃圾回收器自動回收垃圾歼郭,但這也不絕對,當(dāng)我們 new 了對象辐棒,并保存了其引用病曾,但是后面一直沒用它,而垃圾回收器又不會去回收它漾根,這邊會造成內(nèi)存泄露泰涂,
內(nèi)存溢出是指程序所需要的內(nèi)存超出了系統(tǒng)所能分配的內(nèi)存(包括動態(tài)擴(kuò)展)的上限。
對象實例化分析
對內(nèi)存分配情況分析最常見的示例便是對象實例化:
Object obj = new Object();
這段代碼的執(zhí)行會涉及 Java 棧辐怕、Java 堆逼蒙、方法區(qū)三個最重要的內(nèi)存區(qū)域。假設(shè)該語句出現(xiàn)在方法體中寄疏,及時對 JVM 虛擬機(jī)不了解的 Java 使用這其做,應(yīng)該也知道 obj 會作為引用類型(reference)的數(shù)據(jù)保存在 Java 棧的本地變量表中,而會在 Java 堆中保存該引用的實例化對象赁还,但可能并不知道妖泄,Java 堆中還必須包含能查找到此對象類型數(shù)據(jù)的地址信息(如對象類型、父類艘策、實現(xiàn)的接口蹈胡、方法等),這些類型數(shù)據(jù)則保存在方法區(qū)中。
另外罚渐,由于 reference 類型在 Java 虛擬機(jī)規(guī)范里面只規(guī)定了一個指向?qū)ο蟮囊萌春海]有定義這個引用應(yīng)該通過哪種方式去定位,以及訪問到 Java 堆中的對象的具體位置荷并,因此不同虛擬機(jī)實現(xiàn)的對象訪問方式會有所不同合砂,主流的訪問方式有兩種:使用句柄池和直接使用指針。
通過句柄池訪問的方式如下:
通過直接指針訪問的方式如下:
這兩種對象的訪問方式各有優(yōu)勢源织,使用句柄訪問方式的最大好處就是 reference 中存放的是穩(wěn)定的句柄地址翩伪,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時只會改變句柄中的實例數(shù)據(jù)指針,而 reference 本身不需要修改谈息。使用直接指針訪問方式的最大好處是速度快缘屹,它節(jié)省了一次指針定位的時間開銷。目前 Java 默認(rèn)使用的 HotSpot 虛擬機(jī)采用的便是是第二種方式進(jìn)行對象訪問的侠仇。