背景
任何軟件程序都需要內(nèi)存資源,而內(nèi)存資源總是有限的,需要回收重復(fù)利用。
早期編程語言如C++要依靠程序員手動估算、分配和回收內(nèi)存,這部分工作要求精細(xì)而且極耗精力,實際上影響了對軟件真正的業(yè)務(wù)價值的實現(xiàn),所以Java提出了內(nèi)存托管的概念,就是由JVM來自動負(fù)責(zé)內(nèi)存的分配和回收,讓開發(fā)者可以專注于實現(xiàn)軟件的業(yè)務(wù)價值。
Java的內(nèi)存分配
在Java里,每個應(yīng)用程序?qū)?yīng)一個自己唯一的JVM實例,每個JVM有自己的內(nèi)存區(qū)域,JVM之間互不影響。
JVM為應(yīng)用程序提供的內(nèi)存,常見的有這么幾種:程序計數(shù)器、棧、堆、方法區(qū)。
一、程序計數(shù)器
程序計數(shù)器用來存放下一條指令(JVM需要的是.class字節(jié)碼)的地址,所以空間很小。
二、棧
JVM的數(shù)據(jù)區(qū)里,有Java虛擬機(jī)棧和本地方法棧(為虛擬機(jī)使用的Native方法服務(wù))兩個棧,我們常說的棧指的是Java虛擬機(jī)棧,如果在內(nèi)存里談到棧,就是指Java虛擬機(jī)棧中的局部變量表。
其實,每個線程都有自己私有的Java虛擬機(jī)棧,這個棧是用來處理方法的,棧里對應(yīng)每個方法都有一個棧幀(Stack Frame),棧幀里存放方法需要的局部變量表、方法出口等信息,線程對方法調(diào)用和執(zhí)行時,對應(yīng)的棧幀就會在虛擬機(jī)棧里入棧和出棧。它們的關(guān)系是這樣的:
從內(nèi)存的角度,棧里最重要的是局部變量表,里面存放了方法需要的所有基本類型和對象引用,因為局部變量的長度都是確定的(long和double需要2個slot,其他1個slot),所以,局部變量表在編譯期間就可以確定,它的內(nèi)存空間也是在編譯時完成分配,方法運行期間,局部變量表的長度是不變的。
局部變量其實就是Java里的兩種變量:基本類型和引用類型。二者作為局部變量,都放在局部變量表中,基本類型直接在棧中保存值,引用類型在棧里保存一個指向堆區(qū)的指針(實例),真正的對象在堆里。作為參數(shù)時基本類型就直接傳值,引用類型傳指針。
目前大部分JVM的堆和棧都可以動態(tài)擴(kuò)展,如果線程請求的棧深度超過了虛擬機(jī)允許的棧深度,會報“StackOverflowError”。
其中,關(guān)于引用reference,有兩種訪問定位的方式:
-
直接指針
reference里直接存儲對象在堆中的地址,如果還用到了方法區(qū)中的對象類型數(shù)據(jù)(靜態(tài)變量等),那么堆里還要有指針,即到方法區(qū)中對象類型數(shù)據(jù)的指針。
來自《對象訪問分析》 - 句柄
堆里有一塊專門的句柄池,reference里存儲的是句柄地址,reference不直接指向堆中的對象地址,而是指向句柄,句柄中指向堆中對象池中的對象實例地址,以及方法區(qū)中的對象類型數(shù)據(jù)。
來自《對象訪問分析》
三、堆
Java堆是所有線程共享的內(nèi)存區(qū)域,一般的應(yīng)用中,堆也是最大的一塊兒內(nèi)存。
前面說過,引用類型在棧里保存的是一個指向堆區(qū)的指針(值是對象在堆內(nèi)存中的首地址),這個指針指向的真正的對象,是放在堆中的,Java內(nèi)存回收,其實主要就是針對這個堆中對象的內(nèi)存回收。
Java堆只要保持邏輯連續(xù),可以分配在物理上不連續(xù)的區(qū)域,當(dāng)堆里的內(nèi)存無法滿足對象需求,且堆的大小無法擴(kuò)展時,會報“OutOfMemoryError”。
關(guān)于棧和堆,還有這樣幾個不同: - 棧里實例,堆里對象
實例和對象是不同的,棧里的指針是實例,堆里的數(shù)據(jù)才是對象,所以多個實例可以指向同一個對象,如果用代碼來說的話,Class a= new Class();中,a是一個實例,不是對象。 - 棧做運算,堆存數(shù)據(jù)
Java里面的數(shù)學(xué)運算都是在棧里進(jìn)行的,而堆只是為開發(fā)者提供存放對象的空間。 - 銷毀時機(jī)不同
棧變量和堆對象并不同步銷毀,棧和生命周期在于方法,方法結(jié)束,棧中的局部變量立即銷毀;堆對象首先要確保沒有棧變量指向,然后在JVM執(zhí)行垃圾回收時才會銷毀。 - 類的變量和方法不同
類的成員變量可能有多個,它們可能指向多個對象,也就是對應(yīng)堆中的多塊內(nèi)存;但是類的方法只有一套,為類里的所有對象共享,而且只在執(zhí)行方法時入棧(這里的棧指的是Java虛擬棧,而不是常說的方法棧幀中的局部變量表,也就是內(nèi)存棧),不使用時不占用內(nèi)存。
四、方法區(qū)(非堆)
ClassLoader加載類時,會把.class文件格式的二進(jìn)制字節(jié)流讀取為當(dāng)前虛擬機(jī)需要的數(shù)據(jù)格式,并放進(jìn)虛擬機(jī)的內(nèi)存,其實就是放進(jìn)了方法區(qū)。
方法區(qū)也是所有線程共享的內(nèi)存區(qū)域,這一點和堆很像,也可以是物理上不連續(xù)的內(nèi)存區(qū)域,大小也可以動態(tài)擴(kuò)展,也可能報“OutOfMemoryError”。
但是,方法區(qū)也叫非堆,因為它存放的是編譯后的代碼、類信息、常量、靜態(tài)變量等。
方法區(qū)有時候也叫永久代,因為方法區(qū)主要回收常量池和類的卸載,實際上就是很少回收這個區(qū)域。
方法區(qū)里會存放運行時常量池,包括字面量和符號引用等。
關(guān)于常量池和靜態(tài)域
我們在程序中經(jīng)常使用final常量,如果頻繁地創(chuàng)建和銷毀常量,會影響系統(tǒng)性能,所以JVM里用專門的常量池來管理數(shù)據(jù),可以節(jié)約內(nèi)存(比如字符串常量池會合并字符串常量),節(jié)省時間(比如字符串之間直接用==判斷是否引用,比用equals判斷內(nèi)容快)。
但是,常量池是最繁瑣的數(shù)據(jù),14種常量類型在常量池里都有自己的結(jié)構(gòu),其中字符串常量池還有一些很特殊的特性(后面說)。
常量又可以細(xì)分為字面量和符號引用:
- 字面量
字面量也叫直接常量,包括基本類型、String、數(shù)組等,都是開發(fā)者自己用Java語言定義的,這個好理解。 - 符號引用
符號引用和編譯有關(guān),包括類和接口的全名、方法/字段的名稱和描述。
符號引用是為類的加載服務(wù)的,由于Class文件的方法/字段是動態(tài)分配的內(nèi)存,所以在加載Class文件時沒有方法/字段的真正的內(nèi)存入口,只能先保存方法/字段的名稱和描述,放在常量池中,作為符號引用,在類得到創(chuàng)建和運行時,方法/字段有真正的內(nèi)存了,再解析翻譯這些符號引用,讓它們指向真正的內(nèi)存。這個過程就是虛擬機(jī)加載Class文件時的動態(tài)連接。
關(guān)于運行時常量池
存放類的直接常量和符號引用,運行時常量池允許在運行期間向池里放新的常量(比如String的intern()方法)
字符串常量池比較特殊,JVM在全局只維護(hù)唯一一個字符串常量池,這其實是一個享元模式。
關(guān)于字符串常量池
字符串常量池,專門存儲編譯期間的字符串?dāng)?shù)據(jù),JVM對字符串常量池做了很多特殊的優(yōu)化。
首先,程序中的字符串可能在常量池,也可能在堆里,很簡單,如果在代碼中用雙引號定義了字符串,就是直接常量,那么這個字符串在編譯時就會創(chuàng)建,并保存在常量池中;如果是在運行中new出來,那么就是運行時,會保存在堆中。
然后,常量池會合并字符串,重復(fù)字符串在常量池中和在堆中的數(shù)量是不一樣的:重復(fù)字符串(equals相等)在常量池中只有一個;但是在堆中會有多個:
這里經(jīng)常有String會創(chuàng)建幾個對象的問題,比如String s = new String("abc");類加載時,針對"abc"會在常量池里創(chuàng)建一個對象;類運行時針對new會在堆里創(chuàng)建一個對象,這樣就是兩個對象。
最后,字符串常量池允許在運行期間增加常量,可以用intern()向常量池增加常量,intern()函數(shù)的功能是,在運行期,判斷常量池是否有某個String對象,沒有則加入常量池(保證唯一),然后返回常量池中該對象的引用。
所以,對于以下代碼:
String s1 = "test";
String s2 = new String("test");
String s3 = "te"+"st";
String st = "st";
String s4 = "te"+st;
String s5 = ("te"+st).intern();
s1.intern()==s1(雙引號字符串本來就在常量池)
s1!=s2(new的對象在堆中)
s1==s3(+雙引號字符串,視為常量)
s1!=s4(+變量,不是常量,在堆中創(chuàng)建)
s1==s5(intern會向常量中保存,并使用其中對應(yīng)字符串常量的引用)
引用
《深入理解Java虛擬機(jī)》
Java:對象的強、軟、弱和虛引用
ReferenceQueue的使用
Java 內(nèi)存分配全面淺析
Java虛擬機(jī)-----方法區(qū)和運行時常量池
Java內(nèi)存分配之堆、棧和常量池
Java常量池理解與總結(jié)
Java中幾種常量池的區(qū)分
Class文件中的常量池詳解(上)
對象訪問分析