Java面試相關(guān)(二)--堆和棧

引言:看了網(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>
    1. 寄存器(register)。這是最快的存儲區(qū),因為它位于不同于其他存儲區(qū)的地方——處理器內(nèi)部。但是寄存器的數(shù)量極其有限,所以寄存器由編譯器根據(jù)需求進行分配。你不能直接控制,也不能在程序中感覺到寄存器存在的任何跡象。
    2. 堆棧(stack)。位于通用RAM中,但通過它的“堆棧指針”可以從處理器哪里獲得支持。堆棧指針若向下移動,則分配新的內(nèi)存;若向上移動,則釋放那些內(nèi)存。這是一種快速有效的分配存儲方法,僅次于寄存器。創(chuàng)建程序時候,JAVA編譯器必須知道存儲在堆棧內(nèi)所有數(shù)據(jù)的確切大小和生命周期,因為它必須生成相應(yīng)的代碼,以便上下移動堆棧指針。這一約束限制了程序的靈活性,所以雖然某些Java數(shù)據(jù)存儲在堆棧中——特別是對象引用,但是JAVA對象不存儲其中。
    3. 堆(heap)。一種通用性的內(nèi)存池(也存在于RAM中),用于存放所以的JAVA對象。堆不同于堆棧的好處是:編譯器不需要知道要從堆里分配多少存儲區(qū)域,也不必知道存儲的數(shù)據(jù)在堆里存活多長時間。因此,在堆里分配存儲有很大的靈活性。當(dāng)你需要創(chuàng)建一個對象的時候
      ,只需要new寫一行簡單的代碼,當(dāng)執(zhí)行這行代碼時,會自動在堆里進行存儲分配。當(dāng)然,為這種靈活性必須要付出相應(yīng)的代碼。用堆進行存儲分配比用堆棧進行存儲存儲需要更多的時間。
    4. 靜態(tài)存儲(static storage)。這里的“靜態(tài)”是指“在固定的位置”。靜態(tài)存儲里存放程序運行時一直存在的數(shù)據(jù)。你可用關(guān)鍵字static來標(biāo)識一個對象的特定元素是靜態(tài)的,但JAVA對象本身從來不會存放在靜態(tài)存儲空間里。
    5. 常量存儲(constant storage)。常量值通常直接存放在程序代碼內(nèi)部,這樣做是安全的,因為它們永遠不會被改變。有時,在嵌入式系統(tǒng)中,常量本身會和其他部分分割離開,所以在這種情況下,可以選擇將其放在ROM中 。
    6. 非RAM存儲。如果數(shù)據(jù)完全存活于程序之外,那么它可以不受程序的任何控制,在程序沒有運行時也可以存在。

就速度來說,有如下關(guān)系:
寄存器 < 堆棧 < 堆 < 其他

  • <u>JVM的內(nèi)存分區(qū):</u>
    JVM的分區(qū)可分為三個:堆(heap)、棧(stack)和方法區(qū)(method)
    1. 堆區(qū):
    • 存儲的全是對象,每個對象都包含一個與之對應(yīng)的class信息(我們常說的類類型,Clazz.getClass()等方式獲?。琧lass目的是得到操作指令。
    • JVM只有一個堆區(qū)(heap)被所有線程共享,堆中不存放基本類型和對象引用,只存放對象本身?!具@里的‘對象’,就不包括基本數(shù)據(jù)類型】
    1. 棧區(qū):
    • 每個線程包含自己的一個棧區(qū),棧中只保存基本數(shù)據(jù)類型的對象和自定義對象的引用。
    • 每個棧中的數(shù)據(jù)(基本類型和對象引用)都是私有的,其他棧不可訪問。
    • 棧 = 基本類型變量區(qū) + 執(zhí)行環(huán)境上下文 + 操作指令區(qū)(存放操作指令)
    1. 方法區(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ù)類型:

  1. 基本類型(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)某個固定代碼塊中,代碼塊退出,他們就消失),為了追求速度,于是存在中】
  2. 包裝類,如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í)行上面的代碼是這樣的一個過程:

  1. 編譯器先處理int a = 1;首先它會在棧中創(chuàng)建一個變量為a的引用,然后查找有沒有字面值為 1 的地址,沒找到,就開辟一個存放 1 這個字面值的地址,然后將a指向3的地址。
  2. 接著處理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)存可以按照任意順序分配和釋放。

參考文章


https://github.com/GeniusVJR/LearningNotes/blob/master/Part1/Android/Android%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E6%80%BB%E7%BB%93.md

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 從三月份找實習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,755評論 11 349
  • 在函數(shù)中定義的一些基本類型的變量和對象的引用變量都在函數(shù)的棧內(nèi)存中分配。 當(dāng)在一段代碼塊定義一個變量時,Java就...
    木有魚丸啦閱讀 630評論 0 0
  • 前言 不知道大家有沒有這樣一種感覺,程序員的數(shù)量井噴了。可能是因為互聯(lián)網(wǎng)火了,也可能是各家培訓(xùn)機構(gòu)為我們拉來了大量...
    活這么大就沒飽過閱讀 2,832評論 6 25
  • 題記:兒子在學(xué)校的詩作業(yè),與大家分享。大家也許從中對西方詩歌教育了解一二。
    舒己懷_Frank閱讀 622評論 42 48
  • 我問優(yōu)優(yōu):你喜歡上學(xué)嗎? 她說:喜歡! 我逗她說:你別上學(xué)了,不上學(xué)行嗎? 她說:不行,法律不讓。 我問:法律怎么...
    喜馬拉雅藍閱讀 242評論 0 0

友情鏈接更多精彩內(nèi)容