Java 虛擬機(jī)在執(zhí)行 Java 程序的過(guò)程中會(huì)把他所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域。Java 虛擬機(jī)規(guī)范將 JVM 所管理的內(nèi)存分為以下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū):程序計(jì)數(shù)器、Java 虛擬機(jī)棧、本地方法棧、Java 堆、方法區(qū)。下面詳細(xì)闡述各數(shù)據(jù)區(qū)所存儲(chǔ)的數(shù)據(jù)類型。
一塊較小的內(nèi)存空間,它是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器,字節(jié)碼解釋器工作時(shí)通過(guò)改變?cè)撚?jì)數(shù)器的值來(lái)選擇下一條需要執(zhí)行的字節(jié)碼指令,分支、跳轉(zhuǎn)、循環(huán)等基礎(chǔ)功能都要依賴它來(lái)實(shí)現(xiàn)。每條線程都有一個(gè)獨(dú)立的的程序計(jì)數(shù)器,各線程間的計(jì)數(shù)器互不影響,因此該區(qū)域是線程私有的。
當(dāng)線程在執(zhí)行一個(gè) Java 方法時(shí),該計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址,當(dāng)線程在執(zhí)行的是 Native 方法(調(diào)用本地操作系統(tǒng)方法)時(shí),該計(jì)數(shù)器的值為空。另外,該內(nèi)存區(qū)域是唯一一個(gè)在 Java 虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何 OOM(內(nèi)存溢出:OutOfMemoryError)情況的區(qū)域。
該區(qū)域也是線程私有的,它的生命周期也與線程相同。虛擬機(jī)棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個(gè)方法被執(zhí)行的時(shí)候都會(huì)同時(shí)創(chuàng)建一個(gè)棧幀,棧它是用于支持續(xù)虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。對(duì)于執(zhí)行引擎來(lái)講,活動(dòng)線程中,只有棧頂?shù)臈怯行У?,稱為當(dāng)前棧幀,這個(gè)棧幀所關(guān)聯(lián)的方法稱為當(dāng)前方法,執(zhí)行引擎所運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作。棧幀用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法返回地址和一些額外的附加信息。在編譯程序代碼時(shí),棧幀中需要多大的局部變量表、多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫(xiě)入了方法表的 Code 屬性之中。因此,一個(gè)棧幀需要分配多少內(nèi)存,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)。
在 Java 虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常情況:
這兩種情況存在著一些互相重疊的地方:當(dāng)棧空間無(wú)法繼續(xù)分配時(shí),到底是內(nèi)存太小,還是已使用的棧空間太大,其本質(zhì)上只是對(duì)同一件事情的兩種描述而已。在單線程的操作中,無(wú)論是由于棧幀太大,還是虛擬機(jī)??臻g太小,當(dāng)??臻g無(wú)法分配時(shí),虛擬機(jī)拋出的都是 StackOverflowError 異常,而不會(huì)得到 OutOfMemoryError 異常。而在多線程環(huán)境下,則會(huì)拋出 OutOfMemoryError 異常。
下面詳細(xì)說(shuō)明棧幀中所存放的各部分信息的作用和數(shù)據(jù)結(jié)構(gòu)。
1、局部變量表
局部變量表是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量,其中存放的數(shù)據(jù)的類型是編譯期可知的各種基本數(shù)據(jù)類型、對(duì)象引用(reference)和 returnAddress 類型(它指向了一條字節(jié)碼指令的地址)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,即在 Java 程序被編譯成 Class 文件時(shí),就確定了所需分配的最大局部變量表的容量。當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在棧中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。
局部變量表的容量以變量槽(Slot)為最小單位。在虛擬機(jī)規(guī)范中并沒(méi)有明確指明一個(gè) Slot 應(yīng)占用的內(nèi)存空間大?。ㄔ试S其隨著處理器、操作系統(tǒng)或虛擬機(jī)的不同而發(fā)生變化),一個(gè) Slot 可以存放一個(gè)32位以內(nèi)的數(shù)據(jù)類型:boolean、byte、char、short、int、float、reference 和 returnAddresss。reference 是對(duì)象的引用類型,returnAddress 是為字節(jié)指令服務(wù)的,它執(zhí)行了一條字節(jié)碼指令的地址。對(duì)于 64 位的數(shù)據(jù)類型(long和double),虛擬機(jī)會(huì)以高位在前的方式為其分配兩個(gè)連續(xù)的 Slot 空間。
虛擬機(jī)通過(guò)索引定位的方式使用局部變量表,索引值的范圍是從 0 開(kāi)始到局部變量表最大的 Slot 數(shù)量,對(duì)于 32 位數(shù)據(jù)類型的變量,索引 n 代表第 n 個(gè) Slot,對(duì)于 64 位的,索引 n 代表第 n 和第 n+1 兩個(gè) Slot。
在方法執(zhí)行時(shí),虛擬機(jī)是使用局部變量表來(lái)完成參數(shù)值到參數(shù)變量列表的傳遞過(guò)程的,如果是實(shí)例方法(非static),則局部變量表中的第 0 位索引的 Slot 默認(rèn)是用于傳遞方法所屬對(duì)象實(shí)例的引用,在方法中可以通過(guò)關(guān)鍵字“this”來(lái)訪問(wèn)這個(gè)隱含的參數(shù)。其余參數(shù)則按照參數(shù)表的順序來(lái)排列,占用從1開(kāi)始的局部變量 Slot,參數(shù)表分配完畢后,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的 Slot。
局部變量表中的 Slot 是可重用的,方法體中定義的變量,作用域并不一定會(huì)覆蓋整個(gè)方法體,如果當(dāng)前字節(jié)碼PC計(jì)數(shù)器的值已經(jīng)超過(guò)了某個(gè)變量的作用域,那么這個(gè)變量對(duì)應(yīng)的 Slot 就可以交給其他變量使用。這樣的設(shè)計(jì)不僅僅是為了節(jié)省空間,在某些情況下 Slot 的復(fù)用會(huì)直接影響到系統(tǒng)的而垃圾收集行為。
2、操作數(shù)棧
操作數(shù)棧又常被稱為操作棧,操作數(shù)棧的最大深度也是在編譯的時(shí)候就確定了。32 位數(shù)據(jù)類型所占的棧容量為 1,64 位數(shù)據(jù)類型所占的棧容量為 2。當(dāng)一個(gè)方法開(kāi)始執(zhí)行時(shí),它的操作棧是空的,在方法的執(zhí)行過(guò)程中,會(huì)有各種字節(jié)碼指令(比如:加操作、賦值元算等)向操作棧中寫(xiě)入和提取內(nèi)容,也就是入棧和出棧操作。
Java 虛擬機(jī)的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“棧”就是操作數(shù)棧。因此我們也稱 Java 虛擬機(jī)是基于棧的,這點(diǎn)不同于 Android 虛擬機(jī),Android 虛擬機(jī)是基于寄存器的。
基于棧的指令集最主要的優(yōu)點(diǎn)是可移植性強(qiáng),主要的缺點(diǎn)是執(zhí)行速度相對(duì)會(huì)慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的優(yōu)點(diǎn)是執(zhí)行速度快,主要的缺點(diǎn)是可移植性差。
3、動(dòng)態(tài)連接
每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池(在方法區(qū)中,后面介紹)中該棧幀所屬方法的引用,持有這個(gè)引用是為了支持方法調(diào)用過(guò)程中的動(dòng)態(tài)連接。Class 文件的常量池中存在有大量的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用為參數(shù)。這些符號(hào)引用,一部分會(huì)在類加載階段或第一次使用的時(shí)候轉(zhuǎn)化為直接引用(如 final、static 域等),稱為靜態(tài)解析,另一部分將在每一次的運(yùn)行期間轉(zhuǎn)化為直接引用,這部分稱為動(dòng)態(tài)連接。
4、方法返回地址
當(dāng)一個(gè)方法被執(zhí)行后,有兩種方式退出該方法:執(zhí)行引擎遇到了任意一個(gè)方法返回的字節(jié)碼指令或遇到了異常,并且該異常沒(méi)有在方法體內(nèi)得到處理。無(wú)論采用何種退出方式,在方法退出之后,都需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行。方法返回時(shí)可能需要在棧幀中保存一些信息,用來(lái)幫助恢復(fù)它的上層方法的執(zhí)行狀態(tài)。一般來(lái)說(shuō),方法正常退出時(shí),調(diào)用者的 PC 計(jì)數(shù)器的值就可以作為返回地址,棧幀中很可能保存了這個(gè)計(jì)數(shù)器值,而方法異常退出時(shí),返回地址是要通過(guò)異常處理器來(lái)確定的,棧幀中一般不會(huì)保存這部分信息。
方法退出的過(guò)程實(shí)際上等同于把當(dāng)前棧幀出站,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,如果有返回值,則把它壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整 PC 計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令。
該區(qū)域與虛擬機(jī)棧所發(fā)揮的作用非常相似,只是虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法服務(wù),而本地方法棧則為使用到的本地操作系統(tǒng)(Native)方法服務(wù)。
Java Heap 是 Java 虛擬機(jī)所管理的內(nèi)存中最大的一塊,它是所有線程共享的一塊內(nèi)存區(qū)域。幾乎所有的對(duì)象實(shí)例和數(shù)組都在這類分配內(nèi)存。Java Heap 是垃圾收集器管理的主要區(qū)域,因此很多時(shí)候也被稱為“GC堆”。
根據(jù) Java 虛擬機(jī)規(guī)范的規(guī)定,Java 堆可以處在物理上不連續(xù)的內(nèi)存空間中,只要邏輯上是連續(xù)的即可。如果在堆中沒(méi)有內(nèi)存可分配時(shí),并且堆也無(wú)法擴(kuò)展時(shí),將會(huì)拋出 OutOfMemoryError 異常。
方法區(qū)也是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已經(jīng)被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。方法區(qū)域又被稱為“永久代”,但這僅僅對(duì)于 Sun HotSpot 來(lái)講,JRockit 和 IBM J9 虛擬機(jī)中并不存在永久代的概念。Java 虛擬機(jī)規(guī)范把方法區(qū)描述為 Java 堆的一個(gè)邏輯部分,而且它和 Java Heap 一樣不需要連續(xù)的內(nèi)存,可以選擇固定大小或可擴(kuò)展,另外,虛擬機(jī)規(guī)范允許該區(qū)域可以選擇不實(shí)現(xiàn)垃圾回收。相對(duì)而言,垃圾收集行為在這個(gè)區(qū)域比較少出現(xiàn)。該區(qū)域的內(nèi)存回收目標(biāo)主要針是對(duì)廢棄常量的和無(wú)用類的回收。運(yùn)行時(shí)常量池是方法區(qū)的一部分,Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池(Class文件常量池),用于存放編譯器生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。運(yùn)行時(shí)常量池相對(duì)于 Class 文件常量池的另一個(gè)重要特征是具備動(dòng)態(tài)性,Java 語(yǔ)言并不要求常量一定只能在編譯期產(chǎn)生,也就是并非預(yù)置入 Class 文件中的常量池的內(nèi)容才能進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池,運(yùn)行期間也可能將新的常量放入池中,這種特性被開(kāi)發(fā)人員利用比較多的是 String 類的 intern()方法。
根據(jù) Java 虛擬機(jī)規(guī)范的規(guī)定,當(dāng)方法區(qū)無(wú)法滿足內(nèi)存分配需求時(shí),將拋出 OutOfMemoryError 異常。
直接內(nèi)存并不是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是 Java 虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,它直接從操作系統(tǒng)中分配,因此不受 Java 堆大小的限制,但是會(huì)受到本機(jī)總內(nèi)存的大小及處理器尋址空間的限制,因此它也可能導(dǎo)致 OutOfMemoryError 異常出現(xiàn)。在 JDK1.4 中新引入了 NIO 機(jī)制,它是一種基于通道與緩沖區(qū)的新 I/O 方式,可以直接從操作系統(tǒng)中分配直接內(nèi)存,即在堆外分配內(nèi)存,這樣能在一些場(chǎng)景中提高性能,因?yàn)楸苊饬嗽?Java 堆和 Native 堆中來(lái)回復(fù)制數(shù)據(jù)。
下面給出個(gè)內(nèi)存區(qū)域內(nèi)存溢出的簡(jiǎn)單測(cè)試方法。
這里有一點(diǎn)要重點(diǎn)說(shuō)明,在多線程情況下,給每個(gè)線程的棧分配的內(nèi)存越大,反而越容易產(chǎn)生內(nèi)存溢出異常。操作系統(tǒng)為每個(gè)進(jìn)程分配的內(nèi)存是有限制的,虛擬機(jī)提供了參數(shù)來(lái)控制 Java 堆和方法區(qū)這兩部分內(nèi)存的最大值,忽略掉程序計(jì)數(shù)器消耗的內(nèi)存(很小),以及進(jìn)程本身消耗的內(nèi)存,剩下的內(nèi)存便給了虛擬機(jī)棧和本地方法棧,每個(gè)線程分配到的棧容量越大,可以建立的線程數(shù)量自然就越少。因此,如果是建立過(guò)多的線程導(dǎo)致的內(nèi)存溢出,在不能減少線程數(shù)的情況下,就只能通過(guò)減少最大堆和每個(gè)線程的棧容量來(lái)?yè)Q取更多的線程。
另外,由于 Java 堆內(nèi)也可能發(fā)生內(nèi)存泄露(Memory Leak),這里簡(jiǎn)要說(shuō)明一下內(nèi)存泄露和內(nèi)存溢出的區(qū)別:
內(nèi)存泄露是指分配出去的內(nèi)存沒(méi)有被回收回來(lái),由于失去了對(duì)該內(nèi)存區(qū)域的控制,因而造成了資源的浪費(fèi)。Java 中一般不會(huì)產(chǎn)生內(nèi)存泄露,因?yàn)橛欣厥掌髯詣?dòng)回收垃圾,但這也不絕對(duì),當(dāng)我們 new 了對(duì)象,并保存了其引用,但是后面一直沒(méi)用它,而垃圾回收器又不會(huì)去回收它,這邊會(huì)造成內(nèi)存泄露,
內(nèi)存溢出是指程序所需要的內(nèi)存超出了系統(tǒng)所能分配的內(nèi)存(包括動(dòng)態(tài)擴(kuò)展)的上限。
對(duì)內(nèi)存分配情況分析最常見(jiàn)的示例便是對(duì)象實(shí)例化:
Object obj = new Object();
這段代碼的執(zhí)行會(huì)涉及 Java 棧、Java 堆、方法區(qū)三個(gè)最重要的內(nèi)存區(qū)域。假設(shè)該語(yǔ)句出現(xiàn)在方法體中,及時(shí)對(duì) JVM 虛擬機(jī)不了解的 Java 使用這,應(yīng)該也知道 obj 會(huì)作為引用類型(reference)的數(shù)據(jù)保存在 Java 棧的本地變量表中,而會(huì)在 Java 堆中保存該引用的實(shí)例化對(duì)象,但可能并不知道,Java 堆中還必須包含能查找到此對(duì)象類型數(shù)據(jù)的地址信息(如對(duì)象類型、父類、實(shí)現(xiàn)的接口、方法等),這些類型數(shù)據(jù)則保存在方法區(qū)中。
另外,由于 reference 類型在 Java 虛擬機(jī)規(guī)范里面只規(guī)定了一個(gè)指向?qū)ο蟮囊?,并沒(méi)有定義這個(gè)引用應(yīng)該通過(guò)哪種方式去定位,以及訪問(wèn)到 Java 堆中的對(duì)象的具體位置,因此不同虛擬機(jī)實(shí)現(xiàn)的對(duì)象訪問(wèn)方式會(huì)有所不同,主流的訪問(wèn)方式有兩種:使用句柄池和直接使用指針。
通過(guò)句柄池訪問(wèn)的方式如下:
通過(guò)直接指針訪問(wèn)的方式如下:
這兩種對(duì)象的訪問(wèn)方式各有優(yōu)勢(shì),使用句柄訪問(wèn)方式的最大好處就是 reference 中存放的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而 reference 本身不需要修改。使用直接指針訪問(wèn)方式的最大好處是速度快,它節(jié)省了一次指針定位的時(shí)間開(kāi)銷。目前 Java 默認(rèn)使用的 HotSpot 虛擬機(jī)采用的便是是第二種方式進(jìn)行對(duì)象訪問(wèn)的。
更多建議: