內存分配及變量存儲位置(堆、棧、方法區常量池、方法區靜態區)


程序運行時,有六個地方都可以保存數據:

  1、 寄存器:這是最快的保存區域,因為它位於和其他所有保存方式不同的地方:處理器內部。然而,寄存器的數量十分有限,所以寄存器是根據需要由編譯器分配。我們對此沒有直接的控制權,也不可能在自己的程序里找到寄存器存在的任何蹤跡。
  2、 堆棧:存放基本類型的數據和對象的引用,但對象本身不存放在棧中,而是存放在堆中(new 出來的對象)。駐留於常規RAM(隨機訪問存儲器)區域。但可通過它的“堆棧指針”獲得處理的直接支持。堆棧指針若向下移,會創建新的內存;若向上移,則會釋放那些內存。這是一種特別快、特別有效的數據保存方式,僅次於寄存器。創建程序時,java編譯器必須准確地知道堆棧內保存的所有數據的“長度”以及“存在時間”。這是由於它必須生成相應的代碼,以便向上和向下移動指針。這一限制無疑影響了程序的靈活性,所以盡管有些java數據要保存在堆棧里——特別是對象句柄,但java對象並不放到其中。
  3、 堆:存放用new產生的數據。一種常規用途的內存池(也在RAM區域),其中保存了java對象。和堆棧不同:“內存堆”或“堆”最吸引人的地方在於編譯器不必知道要從堆里分配多少存儲空間,也不必知道存儲的數據要在堆里停留多長的時間。因此,用堆保存數據時會得到更大的靈活性。要求創建一個對象時,只需用new命令編制相碰的代碼即可。執行這些代碼時,會在堆里自動進行數據的保存。當然,為達到這種靈活性,必然會付出一定的代價:在堆里分配存儲空間時會花掉更長的時間
  4、 靜態域:存放在對象中用static定義的靜態成員。這兒的“靜態”是指“位於固定位置”。程序運行期間,靜態存儲的數據將隨時等候調用。可用static關鍵字指出一個對象的特定元素是靜態的。但java對象本身永遠都不會置入靜態存儲空間。
  5、 常量池:存放常量。常數值通常直接置於程序代碼內部。這樣做是安全的。因為它們永遠都不會改變,有的常數需要嚴格地保護,所以可考慮將它們置入只讀存儲器(ROM)。
  6、 非RAM存儲:硬盤等永久存儲空間。若數據完全獨立於一個程序之外,則程序不運行時仍可存在,並在程序的控制范圍之外。其中兩個最主要的例子便是“流式對象”和“固定對象”。對於流式對象,對象會變成字節流,通常會發給另一台機器,而對於固定對象,對象保存在磁盤中。即使程序中止運行,它們仍可保持自己的狀態不變。對於這些類型的數據存儲,一個特別有用的技藝就是它們能存在於其他媒體中,一旦需要,甚至能將它們恢復成普通的、基於RAM的對象。

 

Java內存分配中的棧

  在函數中定義的一些基本類型的變量數據和對象的引用變量都在函數的棧內存中分配。

  當在一段代碼塊定義一個變量時,Java就在棧中 為這個變量分配內存空間,當該變量退出該作用域后,Java會自動釋放掉為該變量所分配的內存空間,該內存空間可以立即被另作他用。棧中的數據大小和生命周期是可以確定的,當沒有引用指向數據時,這個數據就會消失。

  1.每個線程包含一個棧區,棧中只保存基礎數據類型的對象和自定義對象的引用(不是對象),對象都存放在堆區中
  2.每個棧中的數據(原始類型和對象引用)都是私有的,其他棧不能訪問。
  3.棧分為3個部分:基本類型變量區、執行環境上下文、操作指令區(存放操作指令)。

 

Java內存分配中的堆

  堆內存用來存放由new創建的對象和數組。 在堆中分配的內存,由Java虛擬機的自動垃圾回收器來管理。

  在堆中產生了一個數組或對象后,還可以 在棧中定義一個特殊的變量,讓棧中這個變量的取值等於數組或對象在堆內存中的首地址,棧中的這個變量就成了數組或對象的引用變量。  引用變量就相當於是 為數組或對象起的一個名稱,以后就可以在程序中使用棧中的引用變量來訪問堆中的數組或對象。引用變量就相當於是為數組或者對象起的一個名稱。

  引用變量是普通的變量,定義時在棧中分配,引用變量在程序運行到其作用域之外后被釋放。而數組和對象本身在堆中分配,即使程序 運行到使用 new 產生數組或者對象的語句所在的代碼塊之外,數組和對象本身占據的內存不會被釋放,數組和對象在沒有引用變量指向它的時候,才變為垃圾,不能在被使用,但仍 然占據內存空間不放,在隨后的一個不確定的時間被垃圾回收器收走(釋放掉)。這也是 Java 比較占內存的原因。

  1.存儲的全部是對象,每個對象都包含一個與之對應的class的信息。(class的目的是得到操作指令)
  2.jvm只有一個堆區(heap)被所有線程共享,堆中不存放基本類型和對象引用,只存放對象本身 

  實際上,棧中的變量指向堆內存中的變量,這就是Java中的指針! 

 

堆與棧

  Java的堆是一個運行時數據區,類的(對象從中分配空間。這些對象通過new、newarray、 anewarray和multianewarray等指令建立,它們不需要程序代碼來顯式的釋放。堆是由垃圾回收來負責的,堆的優勢是可以動態地分配內存 大小,生存期也不必事先告訴編譯器,因為它是在運行時動態分配內存的,Java的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由於要在運行時動態 分配內存,存取速度較慢。

  棧的優勢是,存取速度比堆要快,僅次於寄存器,棧數據可以共享。但缺點是,存在棧中的數據大小與生存期必須是 確定的,缺乏靈活性。棧中主要存放一些基本類型的變量數據(int, short, long, byte, float, double, boolean, char)和對象句柄(引用)。

       棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義:

  Java代碼

1 int a = 3;
3 int b = 3;

 

  編譯器先處理int a = 3;首先它會在棧中創建一個變量為a的引用,然后查找棧中是否有3這個值,如果沒找到,就將3存放進來,然后將a指向3。接着處理int b = 3;在創建完b的引用變量后,因為在棧中已經有3這個值,便將b直接指向3。這樣,就出現了a與b同時均指向3的情況。

  這時,如果再令 a=4;那么編譯器會重新搜索棧中是否有4值,如果沒有,則將4存放進來,並令a指向4;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響 到b的值。

  要注意這種數據的共享與兩個對象的引用同時指向一個對象的這種共享是不同的,因為這種情況a的修改並不會影響到b, 它是由編譯器完成的,它有利於節省空間。而一個對象引用變量修改了這個對象的內部狀態,會影響到另一個對象引用變量。

Java代碼 

1 int i1 = 9;  
2 int i2 = 9;  
3 int i3 = 9;   
4 public static final int INT1 = 9;  
5 public static final int INT2 = 9;  
6 public static final int INT3 = 9; 

對於成員變量和局部變量:成員變量就是方法外部,類的內部定義的變量;局部變量就是方法或語句塊內部定義的變量。局部變量必須初始化。 
形式參數是局部變量,局部變量的數據存在於棧內存中。棧內存中的局部變量隨着方法的消失而消失。 
成員變量存儲在堆中的對象里面,由垃圾回收器負責回收。 
如以下代碼: 
Java代碼 

 1 class BirthDate {  
 2         private int day;  
 3         private int month;  
 4         private int year;      
 5         public BirthDate(int d, int m, int y) {  
 6             day = d;   
 7             month = m;   
 8             year = y;  
 9         }  
10         //省略get,set方法………  
11     }  
12 
13     public class Test{  
14         public static void main(String args[]){  
15             int date = 9;  
16             Test test = new Test();        
17             test.change(date);   
18             BirthDate d1= new BirthDate(7,7,1970);         
19         }    
20         public void change1(int i){  
21             i = 1234;  
22         }  
23     }    

 


對於以上這段代碼,date為局部變量,i,d,m,y都是形參為局部變量,day,month,year為成員變量。下面分析一下代碼執行時候的變化: 
1. main方法開始執行:int date = 9; 
date局部變量,基礎類型,引用和值都存在棧中。 
2. Test test = new Test(); 
test為對象引用,存在棧中,對象(new Test())存在堆中。 
3. test.change(date); 
i為局部變量,引用和值存在棧中。當方法change執行完成后,i就會從棧中消失。 
4. BirthDate d1= new BirthDate(7,7,1970);  
d1 為對象引用,存在棧中,對象(new BirthDate())存在堆中,其中d,m,y為局部變量存儲在棧中,且它們的類型為基礎類型,因此它們的數據也存儲在棧中。 day,month,year為成員變量,它們存儲在堆中(new BirthDate()里面)。當BirthDate構造方法執行完之后,d,m,y將從棧中消失。
5.main方法執行完之后,date變量,test,d1引用將從棧中消失,new Test(),new BirthDate()將等待垃圾回收。

靜態域

 方法區:
  1.又叫靜態區,跟堆一樣,被所有的線程共享。方法區包含所有的class和static變量。
  2.方法區中包含的都是在整個程序中永遠唯一的元素,如class,static變量。

  java里面是沒有靜態變量這個概念的,不信你自己在方法里面定義一個static int i =0;java里只有靜態成員變量。它屬於類的屬性。至於他放在那里?深入jvm里是是翻譯為方法區的(應該也可叫靜態域吧)。虛擬機的體系結構:堆,方法區,本地方法棧,pc寄存器。而方法區保存的就是一個類的模板,堆是放類的實例的。棧是一般來用來函數計算的。隨便找本計算機底層的書都知道了。棧里的數據,函數執行完就不會存儲了。這就是為什么局部變量每一次都是一樣的。就算給他加一后,下次執行函數的時候還是原來的樣子。

常量池 (constant pool)

  常量池指的是在編譯期被確定,並被保存在已編譯的.class文件中的一些數據。

除了包含代碼中所定義的各種基本類型(如int、long等等)和對象型(如String及數組)的常量值(final)還包含一些以文本形式出現的符號引用,比如:

  ◆類和接口的全限定名;

  ◆字段的名稱和描述符;

  ◆方法和名稱和描述符。

如果是編譯期已經創建好(直接用雙引號定義的)的就存儲在常量池中,如果是運行期(new出來的)才能確定的就存儲在堆中。對於equals相等的字符串,在常量池中永遠只有一份,在堆中有多份。

String是一個特殊的包裝類數據。可以用:

  Java代碼

String str = new String("abc");
String str = "abc";

 

  兩種的形式來創建,第一種是用new()來新建對象的,它會在存放於堆中。每調用一次就會創建一個新的對象。而第二種是先在棧中創建一個對 String類的對象引用變量str,然后通過符號引用去字符串常量池 里找有沒有"abc",如果沒有,則將"abc"存放進字符串常量池 ,並令str指向”abc”,如果已經有”abc” 則直接令str指向“abc”。

  比較類里面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個對象時,用==,下面用例子說明上面的理論。

  Java代碼

String str1 = "abc";
String str2 = "abc";
System.out.println(str1==str2); //true

 

  可以看出str1和str2是指向同一個對象的。

  Java代碼

String str1 =new String ("abc");
String str2 =new String ("abc");
System.out.println(str1==str2); // false

 

  用new的方式是生成不同的對象。每一次生成一個。

  因此用第二種方式創建多個”abc”字符串,在內存中 其實只存在一個對象而已. 這種寫法有利與節省內存空間. 同時它可以在一定程度上提高程序的運行速度,因為JVM會自動根據棧中數據的實際情況來決定是否有必要創建新對象。而對於String str = new String("abc");的代碼,則一概在堆中創建新對象,而不管其字符串值是否相等,是否有必要創建新對象,從而加重了程序的負擔。

  另 一方面, 要注意: 我們在使用諸如String str = "abc";的格式定義類時,總是想當然地認為,創建了String類的對象str。擔心陷阱!對象可能並沒有被創建!而可能只是指向一個先前已經創建的 對象。只有通過new()方法才能保證每次都創建一個新的對象。

 

String常量池問題的幾個例子

示例1:

Java代碼

String s0="kvill";
String s1="kvill";
String s2="kv" + "ill";
System.out.println( s0==s1 );
System.out.println( s0==s2 );

 

結果為:

  true

  true

分析:首先,我們要知結果為道Java 會確保一個字符串常量只有一個拷貝。

  因為例子中的 s0和s1中的”kvill”都是字符串常量,它們在編譯期就被確定了,所以s0==s1為true;而”kv”和”ill”也都是字符串常量,當一個字 符串由多個字符串常量連接而成時,它自己肯定也是字符串常量,所以s2也同樣在編譯期就被解析為一個字符串常量,所以s2也是常量池中” kvill”的一個引用。所以我們得出s0==s1==s2;

示例2:

示例:

  Java代碼

String s0="kvill";
String s1=new String("kvill");
String s2="kv" + new String("ill");
System.out.println( s0==s1 );
System.out.println( s0==s2 );
System.out.println( s1==s2 );

 

  結果為:

  false

  false

  false

分析:用new String() 創建的字符串不是常量,不能在編譯期就確定,所以new String() 創建的字符串不放入常量池中,它們有自己的地址空間。

s0還是常量池 中"kvill”的應用,s1因為無法在編譯期確定,所以是運行時創建的新對象”kvill”的引用,s2因為有后半部分 new String(”ill”)所以也無法在編譯期確定,所以也是一個新創建對象”kvill”的應用;明白了這些也就知道為何得出此結果了。

示例3:

Java代碼

 1 String a = "a1";
 2 String b = "a" + 1;
 3 System.out.println((a == b)); //result = true 
 4 
 5 String a = "atrue";
 6 String b = "a" + "true";
 7 System.out.println((a == b)); //result = true 
 8 
 9 String a = "a3.4";
10 String b = "a" + 3.4;
11 System.out.println((a == b)); //result = true

 

分析:JVM對於字符串常量的"+"號連接,將程序編譯期,JVM就將常量字符串的"+"連接優化為連接后的值,拿"a" + 1來說,經編譯器優化后在class中就已經是a1。在編譯期其字符串常量的值就確定下來,故上面程序最終的結果都為true。

示例4:

Java代碼

String a = "ab";
String bb = "b";
String b = "a" + bb;

System.out.println((a == b)); //result = false

 

分析:JVM對於字符串引用,由於在字符串的"+"連接中,有字符串引用存在,而引用的值在程序編譯期是無法確定的,即"a" + bb無法被編譯器優化,只有在程序運行期來動態分配並將連接后的新地址賦給b。所以上面程序的結果也就為false。

示例5:

Java代碼

String a = "ab";
final String bb = "b";
String b = "a" + bb;

System.out.println((a == b)); //result = true

 

分析:和[4]中唯一不同的是bb字符串加了final修飾,對於final修飾的變量,它在編譯時被解析為常量值的一個本地拷貝存儲到自己的常量 池中或嵌入到它的字節碼流中。所以此時的"a" + bb和"a" + "b"效果是一樣的。故上面程序的結果為true。

示例6:

Java代碼

String a = "ab";
final String bb = getBB();
String b = "a" + bb;

System.out.println((a == b)); //result = false

private static String getBB() 
{  
    return "b";  
 }

 

分析:JVM對於字符串引用bb,它的值在編譯期無法確定,只有在程序運行期調用方法后,將方法的返回值和"a"來動態連接並分配地址為b,故上面 程序的結果為false。

 

關於String是不可變的

       通過上面例子可以得出得知:

  String  s  =  "a" + "b" + "c";

  就等價於String s = "abc";

  String  a  =  "a";

  String  b  =  "b";

  String  c  =  "c";

  String  s  =   a  +  b  +  c;

  這個就不一樣了,最終結果等於:

  Java代碼

StringBuffer temp = new StringBuffer();
temp.append(a).append(b).append(c);
String s = temp.toString();

 

  由上面的分析結果,可就不難推斷出String 采用連接運算符(+)效率低下原因分析,形如這樣的代碼:

  Java代碼

 1 public class Test {
 2 
 3      public static void main(String args[]) {
 4 
 5        String s = null;
 6 
 7        for(int i = 0; i < 100; i++) {
 8 
 9            s += "a";
10       }
11    } 
12 }

 

  每做一次 + 就產生個StringBuilder對象,然后append后就扔掉。下次循環再到達時重新產生個StringBuilder對象,然后 append 字符串,如此循環直至結束。如果我們直接采用 StringBuilder 對象進行 append 的話,我們可以節省 N - 1 次創建和銷毀對象的時間。所以對於在循環中要進行字符串連接的應用,一般都是用StringBuffer或StringBulider對象來進行 append操作。

  由於String類的immutable性質,這一說又要說很多,大家只 要知道String的實例一旦生成就不會再改變了,比如說:String str=”kv”+”ill”+” “+”ans”; 就是有4個字符串常量,首先”kv”和”ill”生成了”kvill”存在內存中,然后”kvill”又和” ” 生成 “kvill “存在內存中,最后又和生成了”kvill ans”;並把這個字符串的地址賦給了str,就是因為String的”不可變”產生了很多臨時變量,這也就是為什么建議用StringBuffer的原 因了,因為StringBuffer是可改變的。

 

String中的final用法和理解

  Java代碼

  final StringBuffer a = new StringBuffer("111");

  final StringBuffer b = new StringBuffer("222");

  a=b;//此句編譯不通過

  final StringBuffer a = new StringBuffer("111");

  a.append("222");// 編譯通過

  可見,final只對引用的"值"(即內存地址)有效,它迫使引用只能指向初始指向的那個對象,改變它的指向會導致編譯期錯誤。至於它所指向的對象 的變化,final是不負責的。

 

總結

  棧中用來存放一些原始數據類型的局部變量數據和對象的引用(String,數組.對象等等)但不存放對象內容

  堆中存放使用new關鍵字創建的對象.

  字符串是一個特殊包裝類,其引用是存放在棧里的,而對象內容必須根據創建方式不同定(常量池和堆).有的是編譯期就已經創建好,存放在字符串常 量池中,而有的是運行時才被創建.使用new關鍵字,存放在堆中。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM