java中的變量大體分為:類(靜態)變量、成員變量、局部變量,在class文件被jvm的類加載器加載后,隨后這些變量被分配至內存中。但是,它們何時被分配至內存的何處呢?
jvm把自己運行時管理的內存稱為運行時數據區。主要分為棧、堆、方法區,java變量就存在這3個區中。
下表為棧、堆、方法區內存分配情況:
| 運行時數據區 | 內存分配時機 | 分配內容 | 備注 |
| 棧 | 線程執行方法時 | • 當前線程中局部基本類型的變量(boolean、char、byte、short、int、long、float、double) |
|
| 堆 | new創建對象時 | • 對象實例及其成員變量 |
• 可以認為Java中所有通過new創建的對象的內存都在此分配 |
| 方法區 | 類加載器加載class文件時 | • 類的信息(名稱、修飾符等) |
• 很難被回收,在一定的條件下它也會被GC |
注意:
1.對於引用類型的變量而言,由於引用類型的變量由兩部分組成:引用及引用指向的對象。因此,對於引用類型的變量而言,搞清楚它在內存中的位置需要明白:其引用存儲在哪里,其引用指向的對象存儲在哪里。而對於基本類型的變量,由於它沒有引用(變量名和引用是兩碼事,基本類型的變量雖然有變量名但沒有引用),所以無需考慮基本類型變量的引用存儲在哪里。
2.本文的難點是:搞清楚靜態變量及常量在內存中的存儲位置。因為它們在jdk7時“搬過家”:從方法區的永久代搬家到了堆中(至於搬到了堆中的具體位置筆者也不是很清楚)。由於在《Java虛擬機規范》中我沒有找到這兩個家伙的搬家記錄,下面僅從《深入理解Java虛擬機》中的相關記錄進行說明。需要說明的是:由於常量在類加載器加載后被存儲在運行時常量池中,因此確定了運行時常量池的存儲位置自然就確定了常量的存儲位置。
• 靜態變量的搬家記錄:
| 到了JDK 7的HotSpot,已經把原本放在永久代的字符串常量池、靜態變量等移出——《深入理解Java虛擬機》第三版 2.2運行時數據區域 准備階段是正式為類中定義的變量(即靜態變量,被static修飾的變量)分配內存並設置類變量初始值的階段,從概念上講,這些變量所使用的內存都應當在方法區中進行分配,但必須注意到方法區本身是一個邏輯上的區域,在JDK 7及之前,HotSpot使用永久代來實現方法區時,實現是完全符合這種邏輯概念的;而在JDK 8【筆者注:作者在后面說到JDK1.7的時候靜態變量已從永久代移出,所以這里應該是印刷錯誤或筆誤,正確的寫法應該是“JDK7”】及之后,類變量則會隨着Class對象一起存放在Java堆中,這時候“類變量在方法區”就完全是一種對邏輯概念的表述了,關於這部分內容,筆者已在4.3.1節介紹並且驗證過。——《深入理解Java虛擬機》第三版 7.3類加載的過程——《深入理解Java虛擬機》第三版 7.3類加載的過程 |
• 常量的搬家記錄:
| 在JDK 6或更早之前的HotSpot虛擬機中,常量池都是分配在永久代中,......出現這種變化,是因為自JDK 7起,原本存放在永久代的字符串常量池被移至Java堆之中 ——《深入理解Java虛擬機》第三版 2.4.3 方法區和運行時常量池溢出 |
上文從堆、棧、方法區的角度說明了變量的內存分配,但是,通常情況下,我們很少從這個角度想問題,更多的是,我們會從變量的角度,去思考這個變量存在哪里。下文就從變量的角度再梳理一遍,雖然你可能認為沒這個必要,因為下文的內容主要依據是來自於上文,本來在寫本文時,筆者也考慮了將下文干脆刪去使得本文更加簡潔,但是最后還是保留了,或許這類似於古詩文中的“互文”吧。
一、局部變量
• 局部變量存在棧中。當線程執行某一個方法時,jvm會在棧空間創建一個棧幀,棧幀包含局部變量表、動態鏈接等信息。局部變量表存放了局部變量,包含基本類型變量(boolean、byte、char、short、int、float、long、double)及對象引用(reference類型,可能是一個指向對象起始地址的引用指針,也可能是一個代表對象的句柄或其他與此對象相關的位置)。
• 引用指向的對象實例存儲在堆中。
二、成員變量
• 當new創建一個對象時,成員變量會隨着對象被分配在堆中。如果成員變量是引用類型,引用會隨着對象存儲在一塊堆空間中,引用指向的對象存儲在另一塊堆空間中。
三、類(靜態)變量、常量
• 類(靜態)變量
類加載器在加載class文件的准備階段,即為類變量分配內存並設置初始值的階段。類變量被分配在方法區中,在jdk7之前,HotSpot使用永久代實現方法區時,類變量被分配在永久代中,從jdk7開始,類變量被移至堆中。(靜態常量和靜態變量存儲的位置是一致的)
• 常量
常量也屬於成員變量,按照成員變量的內存分配原則,它也應該隨着創建的對象一起被分配到堆中,然而並非如此,被final修飾的常量被編譯成class文件后,位於常量池表中,在類加載后,常量池表中的內容存放在運行時常量池中,而運行時常量池是方法區的一部分。運行時常量池像類變量一樣,在jdk7也搬家到了堆中。因此,在jdk7之前,常量存儲在方法區中,從jdk7開始,常量存儲在堆中。
總結:
在類中定義的三種變量在內存中的分配位置及分配時機如下圖:

代碼示例
package 內存分配原理;
public class Demo {
/*
1.成員變量
分配時機:new創建對象時
分配位置:堆
*/
// 舉例說明:在創建對象時(C1 c = new C1();)會給對象c的成員變量分配內存,其中基本類型數據i及引用s作為對象c的實例數據存儲在堆中;引用s指向的對象new String("a")存儲在另一塊堆空間
private int i = 1; // 堆
private String s/*引用:堆*/ = new String("a")/*對象實例:另一塊堆空間中*/;
private String s1/*引用:堆*/ = "aa"/*字面量aa:字符串常量池*/; // 字符串常量池是運行時常量池的一部分,在jdk7之前位於方法區,jdk7開始移至堆中
/*
2.類(靜態)變量&常量&靜態常量
分配時機:類加載時
分配位置:方法區(jdk7之前) -> 堆(jdk7及以后)
*/
// 靜態變量
private static int ii = 2; // 方法區(jdk7之前) -> 堆(jdk7及以后)
private static String ss/*引用:方法區(jdk7之前) -> 堆(jdk7及以后)*/ = new String("b")/*對象實例:堆*/;
// 常量
private final int iii = 3;// 方法區(jdk7之前) -> 堆(jdk7及以后)
private final String sss/*引用:方法區(jdk7之前) -> 堆(jdk7及以后)*/ = new String("c")/*對象實例:堆*/;
// 靜態常量
private final static String ssss/*引用:方法區(jdk7之前) -> 堆(jdk7及以后)*/ = new String("d")/*對象實例:堆*/;
/*
3.局部變量
分配時機:線程執行方法時
分配位置:棧
注:對於引用類型,其引用存在棧中,其引用所指向的對象實例存在堆中
*/
void m() {
int i4 = 4; // 棧
Demo d/*引用:棧*/ = new Demo()/*堆*/;
}
}
根據上面示例代碼,我畫了一張圖加以說明:
