引言:看了網(wǎng)上一些作品,沒有特別清晰的一個結(jié)構(gòu),所以,這里本人整理一下Java的堆棧相關(guān)知識。Java 中的堆和棧 Java把內(nèi)存劃分成兩種:一種是棧內(nèi)存,一種是堆內(nèi)存。至于“方法區(qū)”(靜態(tài)存儲區(qū)),可以理解為:主要存放靜態(tài)數(shù)據(jù)、全局 static 數(shù)據(jù)和常量。這塊內(nèi)存在程序編譯時就已經(jīng)分配好,并且在程序整個運行期間都存在??偟膩碚f:堆和棧針對非靜態(tài)數(shù)據(jù),而方法區(qū)針對靜態(tài)數(shù)據(jù)。
一、堆內(nèi)存和棧內(nèi)存
棧(stack)與堆(heap)都是Java用來在Ram中存放數(shù)據(jù)的地方。與C++不同,Java自動管理棧和堆,程序員不能直接地設(shè)置?;蚨选?/p>
-
棧:
- 簡單理解:堆棧(stack)是操作系統(tǒng)在建立某個進程或者線程(在支持多線程的操作系統(tǒng)中是線程)為這個線程建立的存儲區(qū)域,該區(qū)域具有先進后出的特性。
- 特點:存取速度比堆要快,僅次于直接位于CPU中的寄存器。棧中的數(shù)據(jù)可以共享(意思是:棧中的數(shù)據(jù)可以被多個變量共同引用)。
- 缺點:存在棧中的數(shù)據(jù)大小與生存期必須是確定的,缺乏靈活性。
- 相關(guān)存放對象:①一些基本類型的變量(,int, short, long, byte, float, double, boolean, char)和對象句柄【例如:在函數(shù)中定義的一些基本類型的變量和對象的引用變量】。②方法的形參 直接在??臻g分配,當(dāng)方法調(diào)用完成后從??臻g回收。
- 特殊:①方法的引用參數(shù),在棧空間分配一個地址空間,并指向堆空間的對象區(qū),當(dāng)方法調(diào)用完成后從棧空間回收。②局部變量new出來之后,在棧控件和堆空間中分配空間,當(dāng)局部變量生命周期結(jié)束后,它的??臻g立刻被回收,它的堆空間等待GC回收。
-
堆:
- 簡單理解:每個Java應(yīng)用都唯一對應(yīng)一個JVM實例,每一個JVM實例唯一對應(yīng)一個堆。應(yīng)用程序在運行中所創(chuàng)建的所有類實例或者數(shù)組都放在這個堆中,并由應(yīng)用所有的線程共享。Java中分配堆內(nèi)存是自動初始化的,Java中所有對象的存儲控件都是在堆中分配的,但這些對象的引用則是在棧中分配,也就是一般在建立一個對象時,堆和棧都會分配內(nèi)存。
- 特點:可以動態(tài)地分配內(nèi)存大小、比較靈活,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的數(shù)據(jù)。在堆中分配的內(nèi)存,由Java虛擬機的自動垃圾回收器來管理。
- 缺點:由于要在運行時動態(tài)分配內(nèi)存,存取速度較慢。
- 主要存放:①由new創(chuàng)建的對象和數(shù)組 ;②this
- 特殊:引用數(shù)據(jù)類型(需要用new來創(chuàng)建),既在??丶峙湟粋€地址空間,又在堆空間分配對象的類變量。
補充: 在堆中產(chǎn)生了一個數(shù)組或?qū)ο蠛?,還可以在棧中定義一個特殊的變量,讓棧中這個變量的取值等于數(shù)組或?qū)ο笤诙褍?nèi)存中的首地址,棧中的這個變量就成了數(shù)組或?qū)ο蟮囊米兞俊?br> 引用變量就相當(dāng)于是為數(shù)組或?qū)ο笃鸬囊粋€名稱,以后就可以在程序中使用棧中的引用變量來訪問堆中的數(shù)組或?qū)ο蟆?br> 引用變量是普通變量,定義時在棧中分配內(nèi)存,引用變量在程序運行到作用域外釋放。而數(shù)組&對象本身在堆中分配,即使程序運行到使用new產(chǎn)生數(shù)組和對象的語句所在地代碼塊之外,數(shù)組和對象本身占用的堆內(nèi)存也不會被釋放,<u>數(shù)組和對象在沒有引用變量指向它的時候,才變成垃圾,不能再被使用,但是仍然占著內(nèi)存,在隨后的一個不確定的時間被垃圾回收器釋放掉。這個也是java比較占內(nèi)存的主要原因</u>。
這里可以理解為:String s1 = new String("abc");這里面:"abc"表示棧中的一個存儲空間中的一個數(shù)據(jù),new String("abc")表示存在于堆中的一個對象,這個對象的值為‘a(chǎn)bc’,String s1則表示棧中定義的一個取了new String("abc")在堆中的首地址的一個特殊變量,也就是:s1成了引用變量,相當(dāng)于一個別名。
二、Java數(shù)據(jù)存儲和JVM內(nèi)存分區(qū)
- <u>在JAVA中,有六個不同的地方可以存儲數(shù)據(jù):</u>
- 寄存器(register)。這是最快的存儲區(qū),因為它位于不同于其他存儲區(qū)的地方——處理器內(nèi)部。但是寄存器的數(shù)量極其有限,所以寄存器由編譯器根據(jù)需求進行分配。你不能直接控制,也不能在程序中感覺到寄存器存在的任何跡象。
- 堆棧(stack)。位于通用RAM中,但通過它的“堆棧指針”可以從處理器哪里獲得支持。堆棧指針若向下移動,則分配新的內(nèi)存;若向上移動,則釋放那些內(nèi)存。這是一種快速有效的分配存儲方法,僅次于寄存器。創(chuàng)建程序時候,JAVA編譯器必須知道存儲在堆棧內(nèi)所有數(shù)據(jù)的確切大小和生命周期,因為它必須生成相應(yīng)的代碼,以便上下移動堆棧指針。這一約束限制了程序的靈活性,所以雖然某些Java數(shù)據(jù)存儲在堆棧中——特別是對象引用,但是JAVA對象不存儲其中。
-
堆(heap)。一種通用性的內(nèi)存池(也存在于RAM中),用于存放所以的JAVA對象。堆不同于堆棧的好處是:編譯器不需要知道要從堆里分配多少存儲區(qū)域,也不必知道存儲的數(shù)據(jù)在堆里存活多長時間。因此,在堆里分配存儲有很大的靈活性。當(dāng)你需要創(chuàng)建一個對象的時候
,只需要new寫一行簡單的代碼,當(dāng)執(zhí)行這行代碼時,會自動在堆里進行存儲分配。當(dāng)然,為這種靈活性必須要付出相應(yīng)的代碼。用堆進行存儲分配比用堆棧進行存儲存儲需要更多的時間。 - 靜態(tài)存儲(static storage)。這里的“靜態(tài)”是指“在固定的位置”。靜態(tài)存儲里存放程序運行時一直存在的數(shù)據(jù)。你可用關(guān)鍵字static來標(biāo)識一個對象的特定元素是靜態(tài)的,但JAVA對象本身從來不會存放在靜態(tài)存儲空間里。
- 常量存儲(constant storage)。常量值通常直接存放在程序代碼內(nèi)部,這樣做是安全的,因為它們永遠不會被改變。有時,在嵌入式系統(tǒng)中,常量本身會和其他部分分割離開,所以在這種情況下,可以選擇將其放在ROM中 。
- 非RAM存儲。如果數(shù)據(jù)完全存活于程序之外,那么它可以不受程序的任何控制,在程序沒有運行時也可以存在。
就速度來說,有如下關(guān)系:
寄存器 < 堆棧 < 堆 < 其他
- <u>JVM的內(nèi)存分區(qū):</u>
JVM的分區(qū)可分為三個:堆(heap)、棧(stack)和方法區(qū)(method)- 堆區(qū):
- 存儲的全是對象,每個對象都包含一個與之對應(yīng)的class信息(我們常說的類類型,
Clazz.getClass()等方式獲?。琧lass目的是得到操作指令。 - JVM只有一個堆區(qū)(heap)被所有線程共享,堆中不存放基本類型和對象引用,只存放對象本身?!具@里的‘對象’,就不包括基本數(shù)據(jù)類型】
- 棧區(qū):
- 每個線程包含自己的一個棧區(qū),棧中只保存基本數(shù)據(jù)類型的對象和自定義對象的引用。
- 每個棧中的數(shù)據(jù)(基本類型和對象引用)都是私有的,其他棧不可訪問。
- 棧 = 基本類型變量區(qū) + 執(zhí)行環(huán)境上下文 + 操作指令區(qū)(存放操作指令)
- 方法區(qū)【這個可能比較陌生】
- 又稱為‘靜態(tài)區(qū)’,和堆一樣,被所有的線程共享。
- 方法區(qū)包含所有的class和static變量。
補充:大家也許聽說過“數(shù)據(jù)區(qū)”或者“運行時數(shù)據(jù)區(qū)”這個名詞,這里,我們說JVM是驅(qū)動Java程序運行的基礎(chǔ),而它有三個分區(qū):堆、棧、方法區(qū),實際上,JVM的三個方法區(qū)就是包含于 JVM的運行時數(shù)據(jù)區(qū)中的三大塊。于是,“數(shù)據(jù)區(qū)”與上述的分區(qū)的關(guān)系就明朗了。
三、Java的兩種數(shù)據(jù)類型:
-
基本類型(primitive types), 共有8種,即int, short, long, byte, float, double, boolean, char(注意,
并沒有string的基本類型)。這種類型的定義是通過諸如int a = 3; long b = 255L;的形式來定義的,稱為自動變量?!咀詣幼兞看娴氖亲置嬷?,不是類的實例(即不是類的引用),這里并沒有類的存在,如int a=3;這里a只是指向int類型(不是類)的引用,指向字面值3,此時,由于這些字面值的數(shù)據(jù)大小可知并且生存期可知(他們在程序內(nèi)某個固定代碼塊中,代碼塊退出,他們就消失),為了追求速度,于是存在棧中】 - 包裝類,如Integer, String, Double等將相應(yīng)的基本數(shù)據(jù)類型包裝起來的類。這些類數(shù)據(jù)全部存在于堆中,Java用new()語句來顯式地告訴編譯器,在運行時才根據(jù)需要動態(tài)創(chuàng)建,因此比較靈活,但缺點是要占用更多的時間。
四、代碼示例說明
用一些例子來理解哪些數(shù)據(jù)屬于棧內(nèi)存,哪些數(shù)據(jù)屬于堆內(nèi)存:
示例一:對于字面值和字面值引用
//.....
int a = 1; //a屬于字面值 1 的引用
//.....
int b = 1;//b屬于字面值 1 的引用
執(zhí)行上面的代碼是這樣的一個過程:
- 編譯器先處理int a = 1;首先它會在棧中創(chuàng)建一個變量為a的引用,然后查找有沒有字面值為 1 的地址,沒找到,就開辟一個存放 1 這個字面值的地址,然后將a指向3的地址。
- 接著處理int b = 1;在創(chuàng)建完b的引用變量后,由于在棧中已經(jīng)有3這個字面值,便將b直接指向3的地址。這樣,就出現(xiàn)了a與b同時均指向3的情況。
注意:上面代碼注釋說了,a和b都是字面值 1 的引用,他們和我們理解的類對象的引用不同:假定兩個類對象的引用同時指向一個對象,如果一個對象引用變量修改了這個
對象的內(nèi)部狀態(tài),那么另一個對象引用變量<u>也即刻反映出這個變化</u>。而通過字面值的引用來修改其值,不會導(dǎo)致另一個指向此字面值的引用的值也跟著改變的情況。如上例,我們定義完a與b的值后,再令a=2;那么,b不會等于2,還是等于1。在編譯器內(nèi)部,遇到a=2時,它就會重新搜索棧中是否有2的字面值,如果沒有,重新開辟地址存放2的值;如果已經(jīng)有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。
示例二:由new String()開始解釋
代碼一:
String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true
代碼二:
String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false
從代碼一和代碼二分析:
String是一個特殊的包裝類數(shù)據(jù)??梢杂茫?br>
①String str = new String("abc");
②String str = "abc";
兩種的形式來創(chuàng)建,第一種是用new()來新建對象的,它會在存放于堆中。每調(diào)用一次就會創(chuàng)建一個新的對象。
而第二種是先在棧中創(chuàng)建一個對String類的對象引用變量str,然后查找棧中有沒有存放"abc",如果沒有,則將"abc"存放進棧,并令str指向”abc”,如果已經(jīng)有”abc” 則直接令str指向“abc”。
代碼三:
String s1 = "ja";
String s2 = "va";
String s3 = "java";
String s4 = s1 + s2;
System.out.println(s3 == s4);//false
System.out.println(s3.equals(s4));//true
從代碼三分析:
比較類里面的數(shù)值是否相等時,用equals()方法;當(dāng)測試兩個包裝類的引用是否指向同一個對象時,用==。
示例三:程序代碼運行過程分析
public class Test {///運行時,JVM把TestB的類信息全部放入方法區(qū)
public static void main(String[] args){//main方法本身是靜態(tài)方法,放入方法區(qū)
///obj1 和 obj2都是對象引用,所以放到棧區(qū),這個‘new Sample("xxx")’是自定義對象應(yīng)該放到堆區(qū)
Obj obj1 = new Obj("A");
Obj obj2 = new Obj("A");
obj1.printName();
obj2.printName();
// 這里,兩個實例中的size成員都是int(基本類型),所以,這個“3”最終存在于棧區(qū)(而不是堆區(qū)),并供obj1和obj2共用。
obj1.size = 3;
obj2.size = 3;
int A = 4;
int B = 4;
System.out.println(obj1.getName()==obj2.getName());
System.out.println(obj1 == obj2);
System.out.println(A == B);
}
}
/**
* 自定義類:Obj
* 運行時,JVM把Obj的類信息全部放入方法區(qū)
*/
class Obj{
private String name;//new出一個Obj實例后,‘name’這個引用放入了棧區(qū),而給‘name’的賦值的是字面值"A"而不是一個newString("A"),則這個"A"會存在棧中,所以,obj1.name和obj2.name共用這個棧中的"A"
public int size;//雖然size是基本數(shù)據(jù)類型的對象,但是它是跟隨這Obj類初始化加載的,所以上面obj1和obj2兩個對象的size指向的地址不同,由于此時賦予給他們的“3”在兩個不同存儲位置。
public Obj(String name) {
this.name = name;
}
public String getName(){
return this.name;
}
public void printName(){///printName方法本身放入方法區(qū)中
System.out.println(this.name);
}
}
整體圖解:

輸出截圖:

看過本人上一篇文章Java面試相關(guān)(一)-- Java類加載全過程的朋友應(yīng)該比較清楚JVM在啟動程序執(zhí)行代碼時對類的加載過程,這里可以簡單看看上述代碼的注釋說明類的加載時機。這里重點配合上述代碼注釋說說過程中的數(shù)據(jù)存儲分區(qū)情況:
-
Obj obj1 = new Obj("A");:JVM首先就在它的堆區(qū)中為一個新的Obj實例分配內(nèi)存,這個Obj實例持有著指向方法區(qū)的Sample類的類型信息的引用(這個‘引用’,實際上指的是Obj類的類型信息在方法區(qū)中的內(nèi)存地址)。obj1一看就知道是main()方法中定義的局部變量,所以,它會被添加到執(zhí)行main()方法的主線程中Java方法調(diào)用棧中。而=將把這個obj1對象指向堆區(qū)的Obj實例,換句話說,obj1對象持有指向Obj實例的引用。 - new出一個Obj實例后,‘name’這個引用放入了棧區(qū),而給‘name’的賦值的是字面值"A"而不是一個newString("A"),則這個"A"會存在棧中,所以,obj1.name和obj2.name共用這個棧中的"A"。
- Obj類中,雖然size是基本數(shù)據(jù)類型的對象,但是它是跟隨這Obj類初始化加載的,所以上面代碼中,obj1和obj2兩個對象的size指向的地址不同,由于此時賦予給他們的“3”在兩個不同存儲位置。
五、擴展:Java內(nèi)存分配策略
按照編譯原理的觀點,程序運行時的內(nèi)存分配有三種策略,分別是靜態(tài)的,棧式的,和堆式的。
?靜態(tài)存儲分配是指在編譯時就能確定每個數(shù)據(jù)目標(biāo)在運行時刻的存儲空間需求,因而在編譯時就可以給他們分配固定的內(nèi)存空間.這種分配策略要求程序代碼中不允許有可變數(shù)據(jù)結(jié)構(gòu)(比如可變數(shù)組)的存在,也不允許有嵌套或者遞歸的結(jié)構(gòu)出現(xiàn),因為它們都會導(dǎo)致編譯程序無法計算準(zhǔn)確的存儲空間需求。
?棧式存儲分配也可稱為動態(tài)存儲分配,是由一個類似于堆棧的運行棧來實現(xiàn)的.和靜態(tài)存儲分配相反,在棧式存儲方案中,程序?qū)?shù)據(jù)區(qū)的需求在編譯時是完全未知的,只有到運行的時候才能夠知道,但是規(guī)定在運行中進入一個程序模塊時,必須知道該程序模塊所需的數(shù)據(jù)區(qū)大小才能夠為其分配內(nèi)存.和我們在數(shù)據(jù)結(jié)構(gòu)所熟知的棧一樣,棧式存儲分配按照先進后出的原則進行分配。
?靜態(tài)存儲分配要求在編譯時能知道所有變量的存儲要求,棧式存儲分配要求在過程的入口處必須知道所有的存儲要求,而堆式存儲分配則專門負責(zé)在編譯時或運行時模塊入口處都無法確定存儲要求的數(shù)據(jù)結(jié)構(gòu)的內(nèi)存分配,比如可變長度串和對象實例.堆由大片的可利用塊或空閑塊組成,堆中的內(nèi)存可以按照任意順序分配和釋放。