Java內存中中堆和棧的區別

都是Java中常用的存儲結構。都是用來存放數據的。

棧是運行時的單位,而堆是存儲的單位。
棧解決程序的運行問題,即程序如何執行,或者說如何處理數據;堆解決的是數據存儲的問題,即數據怎麼放、放在哪兒。
在Java中一個線程就會相應有一個線程棧與之對應,這點很容易理解,因爲不同的線程執行邏輯有所不同,因此需要一個獨立的線程棧。而堆則是所有線程共享的。棧因爲是運行單位,因此裏面存儲的信息都是跟當前線程(或程序)相關信息的。包括局部變量、程序運行狀態、方法返回值等等;而堆只負責存儲對象信息。

堆:(存儲時的單位)

  • 堆內存主要用來存放new出來的對象和數組。
  • 引用類型的變量。內存分配在堆上或者常量池(字符串常量,基本數據類型常量,在方法區裏面)。(就是new出來的東西或者是final修飾的變量)
  • 就是用來存放對象 可以在運行的時候動態的分配內存,存取速度慢 ,生存週期不需要提前確定

棧:(運行時的單位)

  • 棧內存主要用來存放基本數據類型(byte short int long floot double char
    boolean)和堆中對象的引用。
  • 變量出了作用域就會自動釋放。
  • 主要是用來執行程序,存取速度快,大小和生存週期必須提前確定,缺乏靈活性。

方法區:

  • 又叫靜態區 和堆內存一樣 被所有的線程共享 方法區包含所有的class和static變量
  • 方法區中包含的是整個程序永遠唯一的class和static變量

一個實例:
這裏寫圖片描述

java虛擬機中的運行順序:
系統收到了我們發出的運行指令 就會啓動一個虛擬機進程 這個進程首先從classpath中找到被編譯爲二進制字節碼文件的AppMain.class文件 讀取這個文件 並將文件中的AppMain的類信息加載到方法區,這一個過程稱之爲AppMain的類加載。
然後java虛擬機定位到方法區中的main方法的字節碼 執行命令
因爲第一條爲Sample test1=new Sample(“測試一”)
就是創建一個Sample實例 並生成一個叫test1的引用指向這個實例,那麼java虛擬機中進行的操作就是:

  • 先進入方法區去找Sample這個類的類型信息,可是沒有,因爲這會兒還沒有Sample類,所以Jvm就立馬加載Sample類,把Sample類的類型信息加載到方法區就ok了
  • 找到了類的類型信息,接下來就是在堆內存中新開闢一個內存空間用來存儲Sample實例,這個在堆內存中的實例持有着指向方法區中Sample類的類型信息的引用,引用實際上就是有着指向Sample類的類型信息在方法區中的內存地址。
  • 在Jvm中每個線程都會對應一個棧內存,用來跟蹤線程在運行中的方法的調用過程。棧中的每一個元素稱之爲棧幀,當調用一個方法的時候就會向方法棧中壓入一個新幀,這個幀用來存儲局部變量,方法的參數和運算過程中的臨時數據。在“=”號前面的test1是一個在main()方法中定義的變量,是一個局部變量,就會被加到這個線程的方法調用棧裏面
    這個test1持有着指向對內存中的Sample的實例的引用。

Java中的數據類型有兩種。
  一種是基本類型(primitive types), 共有8種,即int, short, long, byte, float, double, boolean, char(注意,並沒有string的基本類型)。這種類型的定義是通過諸如int a = 3; long b = 255L;的形式來定義的,稱爲自動變量。值得注意的是,自動變量存的是字面值,不是類的實例,即不是類的引用,這裏並沒有類的存在。如int a = 3; 這裏的a是一個指向int類型的引用,指向3這個字面值。這些字面值的數據,由於大小可知,生存期可知(這些字面值固定定義在某個程序塊裏面,程序塊退出後,字段值就消失了),出於追求速度的原因,就存在於棧中。
  另外,棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義
  int a = 3;
  int b = 3;
  編譯器先處理int a = 3;首先它會在棧中創建一個變量爲a的引用,然後查找有沒有字面值爲3的地址,沒找到,就開闢一個存放3這個字面值的地址,然後將a指向3的地址。接着處理int b = 3;在創建完b的引用變量後,由於在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。
  特別注意的是,這種字面值的引用與類對象的引用不同。假定兩個類對象的引用同時指向一個對象,如果一個對象引用變量修改了這個對象的內部狀態,那麼另一個對象引用變量也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟着改變的情況。如上例,我們定義完a與 b的值後,再令a=4;那麼,b不會等於4,還是等於3。在編譯器內部,遇到a=4;時,它就會重新搜索棧中是否有4的字面值,如果沒有,重新開闢地址存放4的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。
  另一種是包裝類數據,如Integer, String, Double等將相應的基本數據類型包裝起來的類。這些類數據全部存在於堆中,Java用new()語句來顯示地告訴編譯器,在運行時才根據需要動態創建,因此比較靈活,但缺點是要佔用更多的時間。

String是一個特殊的包裝類數據。即可以用String str = new String(“abc”);的形式來創建,也可以用String str = “abc”;的形式來創建(作爲對比,在JDK 5.0之前,你從未見過Integer i = 3;的表達式,因爲類與字面值是不能通用的,除了String。而在JDK 5.0中,這種表達式是可以的!因爲編譯器在後臺進行Integer i = new Integer(3)的轉換)。前者是規範的類的創建過程,即在Java中,一切都是對象,而對象是類的實例,全部通過new()的形式來創建。Java 中的有些類,如DateFormat類,可以通過該類的getInstance()方法來返回一個新創建的類,似乎違反了此原則。其實不然。該類運用了單例模式來返回類的實例,只不過這個實例是在該類內部通過new()來創建的,而getInstance()向外部隱藏了此細節。那爲什麼在String str = “abc”;中,並沒有通過new()來創建實例,是不是違反了上述原則?其實沒有。
  關於String str = “abc”的內部工作。Java內部將此語句轉化爲以下幾個步驟:
  (1)先定義一個名爲str的對String類的對象引用變量:String str;
  (2)在棧中查找有沒有存放值爲”abc”的地址,如果沒有,則開闢一個存放字面值爲”abc”的地址,接着創建一個新的String類的對象o,並將o 的字符串值指向這個地址,而且在棧中這個地址旁邊記下這個引用的對象o。如果已經有了值爲”abc”的地址,則查找對象o,並返回o的地址。
  (3)將str指向對象o的地址。
  值得注意的是,一般String類中字符串值都是直接存值的。但像String str = “abc”;這種場合下,其字符串值卻是保存了一個指向存在棧中數據的引用!
爲了更好地說明這個問題,我們可以通過以下的幾個代碼進行驗證。
  String str1 = “abc”;
  String str2 = “abc”;
  System.out.println(str1==str2); //true
  注意,我們這裏並不用str1.equals(str2);的方式,因爲這將比較兩個字符串的值是否相等。==號,根據JDK的說明,只有在兩個引用都指向了同一個對象時才返回真值。而我們在這裏要看的是,str1與str2是否都指向了同一個對象。
  結果說明,JVM創建了兩個引用str1和str2,但只創建了一個對象,而且兩個引用都指向了這個對象。
  我們再來更進一步,將以上代碼改成:
  String str1 = “abc”;
  String str2 = “abc”;
  str1 = “bcd”;
  System.out.println(str1 + “,” + str2); //bcd, abc
  System.out.println(str1==str2); //false
  這就是說,賦值的變化導致了類對象引用的變化,str1指向了另外一個新對象!而str2仍舊指向原來的對象。上例中,當我們將str1的值改爲”bcd”時,JVM發現在棧中沒有存放該值的地址,便開闢了這個地址,並創建了一個新的對象,其字符串的值指向這個地址。
  事實上,String類被設計成爲不可改變(immutable)的類。如果你要改變其值,可以,但JVM在運行時根據新值悄悄創建了一個新對象,然後將這個對象的地址返回給原來類的引用。這個創建過程雖說是完全自動進行的,但它畢竟佔用了更多的時間。在對時間要求比較敏感的環境中,會帶有一定的不良影響。
  再修改原來代碼:
  String str1 = “abc”;
  String str2 = “abc”;
  str1 = “bcd”;
  String str3 = str1;
  System.out.println(str3); //bcd
  String str4 = “bcd”;
  System.out.println(str1 == str4); //true
  str3 這個對象的引用直接指向str1所指向的對象(注意,str3並沒有創建新對象)。當str1改完其值後,再創建一個String的引用str4,並指向因str1修改值而創建的新的對象。可以發現,這回str4也沒有創建新的對象,從而再次實現棧中數據的共享。
  我們再接着看以下的代碼。
  String str1 = new String(“abc”);
  String str2 = “abc”;
  System.out.println(str1==str2); //false
  創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。
  String str1 = “abc”;
  String str2 = new String(“abc”);
  System.out.println(str1==str2); //false
  創建了兩個引用。創建了兩個對象。兩個引用分別指向不同的兩個對象。
  以上兩段代碼說明,只要是用new()來新建對象的,都會在堆中創建,而且其字符串是單獨存值的,即使與棧中的數據相同,也不會與棧中的數據共享。
數據類型包裝類的值不可修改。不僅僅是String類的值不可修改,所有的數據類型包裝類都不能更改其內部的值。

結論與建議:
  (1)我們在使用諸如String str = “abc”;的格式定義類時,總是想當然地認爲,我們創建了String類的對象str。擔心陷阱!對象可能並沒有被創建!唯一可以肯定的是,指向 String類的引用被創建了。至於這個引用到底是否指向了一個新的對象,必須根據上下文來考慮,除非你通過new()方法來顯要地創建一個新的對象。因此,更爲準確的說法是,我們創建了一個指向String類的對象的引用變量str,這個對象引用變量指向了某個值爲”abc”的String類。清醒地認識到這一點對排除程序中難以發現的bug是很有幫助的。
  (2)使用String str = “abc”;的方式,可以在一定程度上提高程序的運行速度,因爲JVM會自動根據棧中數據的實際情況來決定是否有必要創建新對象。而對於String str = new String(“abc”);的代碼,則一概在堆中創建新對象,而不管其字符串值是否相等,是否有必要創建新對象,從而加重了程序的負擔。這個思想應該是享元模式的思想,但JDK的內部在這裏實現是否應用了這個模式,不得而知。
  (3)當比較包裝類裏面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個對象時,用==。

這裏寫圖片描述

String s1=new String(“123”);
String s2=new String(“123”);
一共創建了兩個對象。
一個是在堆內存裏創建的123 一個是在編譯期間常量池裏創建的。而在s2的時候 在編譯期發現常量池裏已經有了123這個對象 所以直接指向這個對象
而是s1和s2只是對象的引用 不算是對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章