在回答這個(gè)問(wèn)題之前先了解一下Java的一些基礎(chǔ)知識(shí)。
我們知道Java程序運(yùn)行在虛擬機(jī)環(huán)境里,那我們先看一下虛擬機(jī)的大致內(nèi)存結(jié)構(gòu)。如下圖所示,虛線框?yàn)檎麄€(gè)虛擬機(jī)內(nèi)存區(qū)域,其中有顏色的區(qū)域?yàn)镴ava程序所占的內(nèi)存區(qū)域。

圖中可見Java程序所占的內(nèi)存區(qū)域可劃分成5個(gè)部分:程序計(jì)數(shù)器、虛擬機(jī)棧(線程棧)、本地方法棧、堆(heap)和方法區(qū)(內(nèi)含常量池)。其中方法區(qū)和堆由所有線程共享。
這5個(gè)區(qū)域作用和功能分別如下:
程序計(jì)數(shù)器:
它類似CPU寄存器中的PC寄存器,用于存放指令地址。因?yàn)镴ava虛擬機(jī)是多線程的,所以每一個(gè)線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器結(jié)構(gòu),它與線程共存亡。不過(guò)Java虛擬機(jī)中的程序計(jì)數(shù)器指向的是正在執(zhí)行的字節(jié)碼地址,而CPU的PC寄存器指向的是下一條指令的地址。當(dāng)線程去執(zhí)行Native方法時(shí),程序計(jì)數(shù)器則為Undefined。
虛擬機(jī)棧(線程棧):
一個(gè)線程一個(gè)棧,并且生命周期與線程相同。它內(nèi)部由一個(gè)個(gè)棧幀構(gòu)成,一個(gè)棧幀代表一個(gè)調(diào)用的方法,線程在每次方法調(diào)用執(zhí)行時(shí)創(chuàng)建一個(gè)棧幀然后壓棧,棧幀用于存放局部變量、操作數(shù)、動(dòng)態(tài)鏈接、方法出口等信息。方法執(zhí)行完成后對(duì)應(yīng)的棧幀出棧。我們平時(shí)說(shuō)的棧內(nèi)存就是指這個(gè)棧。
一個(gè)線程中的方法可能還會(huì)調(diào)用其他方法,這樣就會(huì)構(gòu)成方法調(diào)用鏈,而且這個(gè)鏈可能會(huì)很長(zhǎng),而且每個(gè)線程都有方法處于執(zhí)行狀態(tài)。對(duì)于執(zhí)行引擎來(lái)說(shuō),只有活動(dòng)線程棧頂?shù)臈攀怯行У?,稱為當(dāng)前棧幀(Current?Stack?Frame),這個(gè)棧幀關(guān)聯(lián)的方法稱為當(dāng)前方法(Current?Method)。
棧幀的大致結(jié)構(gòu)如下圖所示:

每一個(gè)棧幀的結(jié)構(gòu)都包括了局部變量表、操作數(shù)棧、方法返回地址和一些額外的附加信息。某個(gè)方法的棧幀需要多大的局部變量表、多深的操作數(shù)棧都在編譯程序時(shí)完全確定了,并且寫入到類方法表的相應(yīng)屬性中了,因此某個(gè)方法的棧幀需要分配多少內(nèi)存,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)變化的影響,而僅僅取決于具體虛擬機(jī)的實(shí)現(xiàn)。
棧幀結(jié)構(gòu)各部分功能:
局部變量區(qū)域:存儲(chǔ)方法的局部變量和參數(shù),存儲(chǔ)單位以slot(4?byte)為最小單位。局部變量存放的數(shù)據(jù)類型有:基本數(shù)據(jù)類型、對(duì)象引用和return address(指向一條字節(jié)碼指令的地址)。其中64位長(zhǎng)度的long和double類型的變量會(huì)占用2個(gè)slot,其它數(shù)據(jù)類型只占用1個(gè)slot。
類的靜態(tài)方法和對(duì)象的實(shí)例方法被調(diào)用時(shí),各自棧幀對(duì)應(yīng)的局部變量結(jié)構(gòu)基本類似。但有以下如圖示區(qū)別:實(shí)例方法中第一個(gè)位置存放的是它所屬對(duì)象的引用。而靜態(tài)方法則沒(méi)有對(duì)象的引用。另外靜態(tài)方法里所操作的靜態(tài)變量存放在方法區(qū)。
void?test(Object?object)
{int?i=0;
Boolean?b=false;
}
static?void?test1(int?i?,Object?object,boolean?b)
{
...
}

關(guān)于局部變量,還有一點(diǎn)需要強(qiáng)調(diào),就是局部變量不像類的實(shí)例變量那樣會(huì)有默認(rèn)初始化值。所以局部變量需要手工初始化,如果一個(gè)局部變量定義了但沒(méi)有賦初始值是不能使用的。
操作數(shù)棧?所謂操作數(shù)是指那些被指令操作的數(shù)據(jù)。當(dāng)需要對(duì)參數(shù)操作時(shí)如c=a+b,就將即將被操作的參數(shù)數(shù)據(jù)壓棧,如將a 和b 壓棧,然后由操作指令將它們彈出,并執(zhí)行操作。虛擬機(jī)將操作數(shù)棧作為工作區(qū)。Java虛擬機(jī)沒(méi)有寄存器,所有參數(shù)傳遞、值返回都是使用操作數(shù)棧來(lái)完成的。
Java虛擬機(jī)的解釋執(zhí)行引擎稱為“基于棧的執(zhí)行引擎”,其中所指的“?!本褪遣僮鲾?shù)棧。
例如下面這段代碼:
public?static?int?add(int?a,int?b){
int?c=0;
c=a+b;
return?c;
}
add(25,23);
主要步驟如圖:

壓棧的步驟如下:
0:??....
1:???iload_0??//?把局部變量0壓棧,int?a;
2:???iload_1?//?局部變量1壓棧,int?b;
3:???iadd??????//彈出2個(gè)變量,求和,結(jié)果壓棧48
4:???istore_2?//彈出結(jié)果,放于局部變量2;int?c;
5:?...
動(dòng)態(tài)連接,它是個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用。這個(gè)引用是為了支持方法調(diào)用過(guò)程中能進(jìn)行動(dòng)態(tài)連接。我們知道Class文件的常量池存有方法的符號(hào)引用,字節(jié)碼中的方法調(diào)用指令就以指向常量池中方法的符號(hào)引用為參數(shù)。這些符號(hào)引用一部分會(huì)在類加載階段或第一次使用的時(shí)候轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。余下部分將在每一次運(yùn)行期間轉(zhuǎn)化為直接引用,這部分稱為動(dòng)態(tài)連接。
方法返回地址
正常退出,執(zhí)行引擎遇到方法返回的字節(jié)碼,將返回值傳遞給調(diào)用者
異常退出,遇到Exception,并且方法未捕捉異常,返回地址由異常處理器來(lái)確定,并且不會(huì)有任何返回值。
方法退出的過(guò)程實(shí)際上等同于把當(dāng)前棧幀出棧,因此退出時(shí)可能執(zhí)行的操作有:恢復(fù)上層方法的局部變量表和操作數(shù)棧,把返回值(如果有的話)壓入調(diào)用者棧幀的操作數(shù)棧中,調(diào)整PC計(jì)數(shù)器的值以指向方法調(diào)用指令后面的一條指令等。
額外附加信息,虛擬機(jī)規(guī)范沒(méi)有明確規(guī)定,由具體虛擬機(jī)實(shí)現(xiàn)。
Java虛擬機(jī)規(guī)范規(guī)定該區(qū)域有兩種異常:
StackOverFlowError:當(dāng)線程請(qǐng)求棧深度超出虛擬機(jī)棧所允許的深度時(shí)拋出
OutOfMemoryError:當(dāng)Java虛擬機(jī)動(dòng)態(tài)擴(kuò)展到無(wú)法申請(qǐng)足夠內(nèi)存時(shí)拋出
另外需要提醒一下,在規(guī)范模型中,棧幀相互之間是完全獨(dú)立的。但在大多數(shù)虛擬機(jī)的實(shí)現(xiàn)里都會(huì)做一些優(yōu)化處理,這樣兩個(gè)棧幀可能會(huì)出現(xiàn)一部分重疊。這樣在下面的棧幀會(huì)有部分操作數(shù)棧與上面棧幀的部分局部變量表重疊在一起,這樣在進(jìn)行方法調(diào)用時(shí)就可以有部分?jǐn)?shù)據(jù)共享,而無(wú)須進(jìn)行額外的參數(shù)復(fù)制傳遞了。具體情形如下圖所示:

本地方法棧
Java可以通過(guò)java本地接口JNI(Java Native Interface)來(lái)調(diào)用其它語(yǔ)言編寫(如C)的程序,在Java里面用native修飾符來(lái)描述一個(gè)方法是本地方法。本地方法棧就是虛擬機(jī)線程調(diào)用Native方法執(zhí)行時(shí)的棧,它與虛擬機(jī)棧發(fā)揮類似的作用。但是要注意,虛擬機(jī)規(guī)范中沒(méi)有對(duì)本地方法棧作強(qiáng)制規(guī)定,虛擬機(jī)可以自由實(shí)現(xiàn),所以可以不是字節(jié)碼。如果是以字節(jié)碼實(shí)現(xiàn)的話,虛擬機(jī)棧本地方法棧就可以合二為一,事實(shí)上,OpenJDK和SunJDK所自帶的HotSpot虛擬機(jī)就是直接將虛擬機(jī)棧和本地方法棧合二為一的。
Java虛擬機(jī)規(guī)范規(guī)定該區(qū)域也可拋出StackOverFlowError和OutOfMemoryError。
堆(heap)
這個(gè)區(qū)域用來(lái)放置所有對(duì)象實(shí)例以及數(shù)組。不過(guò)在JIT(Just-in-time)情況下有些時(shí)候也有可能在棧上分配對(duì)象實(shí)例。堆也是java垃圾收集器管理的主要區(qū)域(所以很多時(shí)候會(huì)稱它為GC堆)。
從GC回收的角度看,由于現(xiàn)在GC基本都是采用的分代收集算法,所以堆內(nèi)存結(jié)構(gòu)還可以分塊成:新生代和老年代;再細(xì)一點(diǎn)的有Eden空間、From?Survivor空間、To?Survivor空間等。如下圖:

方法區(qū)
它是虛擬機(jī)在加載類文件時(shí),用于存放加載過(guò)的類信息,常量,靜態(tài)變量,及jit編譯后的代碼(類方法)等數(shù)據(jù)的內(nèi)存區(qū)域。它是線程共享的。
方法區(qū)存放的信息包括:
類的基本信息:
每個(gè)類的全限定名
每個(gè)類的直接超類的全限定名(可約束類型轉(zhuǎn)換)
該類是類還是接口
該類型的訪問(wèn)修飾符
直接超接口的全限定名的有序列表
已裝載類的詳細(xì)信息:
運(yùn)行時(shí)常量池:
類信息除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant?Pool?Table),用于存放編譯期生成的各種字面量、符號(hào)引用,文字字符串、final變量值、類名和方法名常量,這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。它們以數(shù)組形式訪問(wèn),是調(diào)用方法、與類聯(lián)系及類的對(duì)象化的橋梁。
這里再講一下,JDK1.7之前運(yùn)行時(shí)常量池是方法區(qū)的一部分,JDK1.7及之后版本已經(jīng)將運(yùn)行時(shí)常量池從方法區(qū)中移了出來(lái),在堆(Heap)中開辟了一塊區(qū)域存放運(yùn)行時(shí)常量池。
運(yùn)行時(shí)常量池除了存放編譯期產(chǎn)生的Class文件的常量外,還可存放在程序運(yùn)行期間生成的新常量,比較常見增加新常量方法有String類的internd()方法。String.intern()是一個(gè)Native方法,它的作用是:如果運(yùn)行時(shí)常量池中已經(jīng)包含一個(gè)等于此String對(duì)象內(nèi)容的字符串,則返回常量池中該字符串的引用;如果沒(méi)有,則在常量池中創(chuàng)建與此String內(nèi)容相同的字符串,并返回常量池中創(chuàng)建的字符串的引用。不過(guò)JDK7的intern()方法的實(shí)現(xiàn)有所不同,當(dāng)常量池中沒(méi)有該字符串時(shí),不再是在常量池中創(chuàng)建與此String內(nèi)容相同的字符串,而改為在常量池中記錄堆中首次出現(xiàn)的該字符串的引用,并返回該引用。
字段信息:
字段信息存放類中聲明的每一個(gè)字段(實(shí)例變量)的信息,包括字段的名、類型、修飾符。
如privateStringa=“”;則a為字段名,String為描述符,private為修飾符。
方法信息:
類中聲明的每一個(gè)方法的信息,包括方法名、返回值類型、參數(shù)類型、修飾符、異常、方法的字節(jié)碼。(在編譯的時(shí)候,就已經(jīng)將方法的局部變量表、操作數(shù)棧大小等完全確定并存放在字節(jié)碼中,在加載載的時(shí)候,隨著類一起裝入方法區(qū)。)
在運(yùn)行時(shí),虛擬機(jī)線程調(diào)用方法時(shí)從常量池中獲得符號(hào)引用,然后在運(yùn)行時(shí)解析成方法的實(shí)際地址,最后通過(guò)常量池中的全限定名、方法和字段描述符,把當(dāng)前類或接口中的代碼與其它類或接口中的代碼聯(lián)系起來(lái)。
靜態(tài)變量:
就是類變量,被類的所有實(shí)例對(duì)象共享,我們只需知道,在方法區(qū)有個(gè)靜態(tài)區(qū),靜態(tài)區(qū)專門存放靜態(tài)變量和靜態(tài)塊。
到類ClassLoader的引用:到該類的類裝載器的引用。
到類Class的引用:虛擬機(jī)為每一個(gè)被裝載的類型創(chuàng)建一個(gè)Class實(shí)例,用來(lái)代表這個(gè)被裝載的類。
Java虛擬機(jī)規(guī)范規(guī)定該區(qū)域可拋出OutOfMemoryError。
直接內(nèi)存
直接內(nèi)存(Direct?Memory)雖然不是程序運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,但這部分內(nèi)存也被頻繁使用,而且它也可能導(dǎo)致OutOfMemoryError異常出現(xiàn)。
在JDK1.4中新加入了NIO(New?Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O方式,它可以使用Native方法庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java堆里面的DirecByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在某些應(yīng)用場(chǎng)景中顯著提高性能,因?yàn)樗苊饬嗽贘ava堆和Native堆中來(lái)回復(fù)制數(shù)據(jù)。
顯然,本機(jī)直接內(nèi)存的分配不會(huì)受到Java堆大小的限制,但是,還是會(huì)受到本機(jī)總內(nèi)存(包括RAM及SWAP區(qū)或者分頁(yè)文件)的大小及處理器尋址空間的限制,從而導(dǎo)致動(dòng)態(tài)擴(kuò)展時(shí)出現(xiàn)OutOfMemoryError異常。
執(zhí)行引擎
將字節(jié)碼即時(shí)編譯?優(yōu)化?為本地代碼,?然后執(zhí)行。
在了解完這些知識(shí)以后,就可以知道:類和對(duì)象在運(yùn)行時(shí)的內(nèi)存里是怎么樣的?以及各類型變量、方法在運(yùn)行時(shí)是怎么交互的?
在程序運(yùn)行時(shí)類是在方法區(qū),實(shí)例對(duì)象本身在堆里面。
方法字節(jié)碼在方法區(qū)。線程調(diào)用方法執(zhí)行時(shí)創(chuàng)建棧幀并壓棧,方法的參數(shù)和局部變量在棧幀的局部變量表。
對(duì)象的實(shí)例變量和對(duì)象一起在堆里,所以各個(gè)線程都可以共享訪問(wèn)對(duì)象的實(shí)例變量。
靜態(tài)變量在方法區(qū),所有對(duì)象共享。字符串常量等常量在運(yùn)行時(shí)常量池。
各線程調(diào)用的方法,通過(guò)堆內(nèi)的對(duì)象,方法區(qū)的靜態(tài)數(shù)據(jù),可以共享交互信息。
各線程調(diào)用的方法所有參數(shù)傳遞、方法返回值的返回,都是使用棧幀里的操作數(shù)棧來(lái)完成的。