JVM內存模型及String對象內存分配

昨天看了一篇關於《Java後端程序員1年工作經驗總結》的文章,其中有一段關於String和StringBuffer的描述,對於執行結果仍然把握不準,趁此機會也總結了下JVM內存模型。

1、JVM運行時數據區域

關於JVM內存模型之前也瞭解過一些,也是看過就忘,好記性比如爛筆頭,記下來吧。參考此文章http://chenzhou123520.iteye.com/blog/1585224

圖1 JVM運行時數據區域


(1)、程序計數器(Program Counter Register):
     程序計數器是一塊較小的內存空間,可以看做是當前線程所執行的字節碼的行號指示器。
     由於java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)都只會執行一條線程中的指令。因此爲了線程切換後能恢復到正確的執行位置,每個線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,是‘線程私有’的內存。
(2)、JAVA虛擬機棧(Java Virtual Machine Stack):
     與程序計數器一樣,java虛擬機棧也是線程私有的,虛擬機棧描述的是Java方法執行的內存模型:在執行的同時會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
     局部變量表存放了編譯期可知的各種基本數據類型()、對象引用和returnAddress類型(指向了一條字節碼指令的地址)
     局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。
JVM Stack 異常情況:

StackOverflowError:當線程請求分配的棧容量超過JVM允許的最大容量時拋出

OutOfMemoryError:如果JVM Stack可以動態擴展,但是在嘗試擴展時無法申請到足夠的內存去完成擴展,或者在建立新的線程時沒有足夠的內存去創建對應的虛擬機棧時拋出
(3)、本地方法棧(Native Method Stack):
      本地方法棧與虛擬機棧所發揮的作用是非常相似,區別不過是虛擬機棧爲虛擬機執行java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法(使用Java語言以外的其它語言編寫的方法)服務。
     本地方法棧也可以拋出StackOverflowError和OutOfMemoryError異常
(4)、JAVA堆(Java Heap):
      虛擬機管理的內存中最大的一塊,同時也是被所有線程所共享的,它在虛擬機啓動時創建,此內存區域存在的唯一目的就是存放對象實例,幾乎所有的對象實例以及數組都要在這裏分配內存。這裏面的對象被自動管理,也就是俗稱的GC(Garbage Collector)所管理。用就是了,有GC扛着呢,不用操心銷燬回收的事兒。
     Java堆的容量可以是固定大小,也可以隨着需求動態擴展(-Xms和-Xmx),並在不需要過多空間時自動收縮。
     Java堆所使用的內存不需要保證是物理連續的,只要邏輯上是連續的即可。
     JVM實現應當提供給程序員調節Java 堆初始容量的手段,對於可動態擴展和收縮的堆來說,則應當提供調節其最大和最小容量的手段。
     如果堆中沒有內存完成實例分配並且堆也無法擴展,就會拋OutOfMemoryError。
(5)、方法區(Method Area):
      跟堆一樣是被各個線程共享的內存區域,用於存儲以被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然這個區域被虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它的別名叫非堆,用來與堆做一下區別。
(6)、運行時常量池(Runtime Constant Pool):
     運行時常量池是方法區一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。

2、String對象內存分配分析
先看以下代碼,運行後,結果如代碼1,2,3,4,5,6所示
package com.xtli.controller;
 
public class StringTest {
     public static void main(String[] args) {
          String s1 = "hello";
          String s2 = "world";
          System.out.println(s1+"---"+s2);//1:hello---world
          change(s1,s2);
          System.out.println(s1+"---"+s2);//3:hello---world
 
          StringBuffer sb1 = new StringBuffer("hello");
          StringBuffer sb2 = new StringBuffer("world");
          System.out.println(sb1+"---"+sb2);//4:hello---world
          change(sb1,sb2);
          System.out.println(sb1+"---"+sb2);//6:hello---worldworld
     }
 
     public static void change(String s1, String s2) {
          s1 = s2;
          s2 = s1+s2;
          System.out.println("change(s1,s2)---"+s1+"---"+s2);//2:change(s1,s2)---world---worldworld
     }
 
     public static void change(StringBuffer sb1, StringBuffer sb2) {
          sb1 = sb2;
          sb2.append(sb1);
          System.out.println("change(sb1,sb2)---"+sb1+"---"+sb2);//5:change(sb1,sb2)---worldworld---worldworld
     }
}
對以上代碼進行分析說明,如下
public class StringTest {
     public static void main(String[] args) {
        //在main方法的棧中創建引用s1和引用s2,此引用s1和引用s2存放在棧(main方法的棧)中;編譯時,在常量池中創建兩個常量"hello"和"world",s1和s2分別
        //指向兩個常量
          String s1 = "hello";
          String s2 = "world";
          System.out.println(s1+"---"+s2);//1:hello---world
          change(s1,s2);//引用s1和s2作爲參數傳遞到change方法中
          //change方法中的引用s1,s2和main方法中的引用s1,s2存放地址並不同,以下輸出的是main方法棧中的s1和s2,並沒有發生變化,故代碼3有以下輸出
          System.out.println(s1+"---"+s2);//3:hello---world
 
          //以下兩行代碼將會在main方法棧中創建引用sb1和sb2,並在堆內存中創建兩個對象"hello"和"world",sb1和sb2分別指向兩個對象
          StringBuffer sb1 = new StringBuffer("hello");
          StringBuffer sb2 = new StringBuffer("world");
          System.out.println(sb1+"---"+sb2);//4:hello---world
          change(sb1,sb2);//引用sb1和sb2作爲參數傳遞到change方法中
          //main方法中的sb1所指向的堆內存地址未發生變化,故仍爲"hello",而change(sb1,sb2)方法改變了main方法中sb2所指向的堆內存地址的內容,故代碼6有以下輸出
          System.out.println(sb1+"---"+sb2);//6:hello---worldworld
     }
 
     public static void change(String s1, String s2) {//在change方法的棧中創建引用s1和s2,並指向常量池中的常量
          s1 = s2;//將引用s1指向s2的常量池中的"world"
          s2 = s1+s2;//在堆內存中創建"worldworld"對象,並將s2指向此堆內存地址
          System.out.println("change(s1,s2)---"+s1+"---"+s2);//2:change(s1,s2)---world---worldworld
     }
 
     public static void change(StringBuffer sb1, StringBuffer sb2) {//在change方法的棧(和上面的change方法棧不同)中創建引用sb1和sb2,並指向main方法棧中sb1和sb2所指向的對象
          sb1 = sb2;//將引用sb1指向sb2所引用的對象"world"
          sb2.append(sb1);//引用sb2所指向的對象發生變化,變爲"worldworld",注意此時外部main方法中的sb2和此方法中的sb1均指向此堆內存地址,
          //此地址內容發生變化後,外部main方法中的sb2指向的內容也跟着變化
          System.out.println("change(sb1,sb2)---"+sb1+"---"+sb2);//5:change(sb1,sb2)---worldworld---worldworld
     }
}
爲了進一步說明change(String s1, String s2)中的結果,可以進行以下驗證。
public static void change(String s1, String s2) {
          String s= "world";
          String ss= "worldworld";
          s1 = s2;
          System.out.println(s==s1);//輸出true
          s2 = s1+s2;
          System.out.println(ss==s2);//輸出false
          System.out.println("change(s1,s2)---"+s1+"---"+s2);//2:change(s1,s2)---world---worldworld
     }
故在change(String s1, String s2)方法中s1=s2後,s1所指向的是常量池中的"world",s2=s1+s2代碼執行後,會在堆內存中重新創建對象,並將s2指向此堆內存地址。
--------------------- 
作者:BiggerLee 
來源:CSDN 
原文:https://blog.csdn.net/lixingtao0520/article/details/77978333 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

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