Java內存區域劃分、內存分配原理

轉載自:http://blog.csdn.net/OyangYujun/article/details/41173747

 運行時數據區域

        Java虛擬機在執行Java的過程中會把管理的內存劃分爲若干個不同的數據區域。這些區域有各自的用途,以及創建和銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,而有的區域則依賴線程的啓動和結束而創建和銷燬。

       Java虛擬機包括下面幾個運行時數據區域:



        程序計數器

        程序計數器是一塊較小的區域,它的作用可以看做是當前線程所執行的字節碼的行號指示器。在虛擬機的模型裏,字節碼指示器就是通過改變程序計數器的值來指定下一條需要執行的指令。分支,循環等基礎功能就是依賴程序計數器來完成的。

        由於java虛擬機的多線程是通過輪流切換並分配處理器執行時間來完成,一個處理器同一時間只會執行一條線程中的指令。爲了線程恢復後能夠恢復正確的執行位置,每條線程都需要一個獨立的程序計數器,以確保線程之間互不影響。所以程序計數器是“線程私有”的內存。

        如果虛擬機正在執行的是一個Java方法,則計數器指定的是字節碼指令對應的地址,如果正在執行的是一個本地方法,則計數器指定問空undefined。程序計數器區域是Java虛擬機中唯一沒有定義OutOfMemory異常的區域。


        Java虛擬機棧

        和程序計數器一樣也是線程私有的,生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會創建一個棧幀用於存儲局部變量表,操作棧,動態鏈接,方法出口等信息。每一個方法被調用的過程就對應一個棧幀在虛擬機棧中從入棧到出棧的過程。

        通常所說的虛擬機運行時分爲棧和堆,這裏的棧指的就是虛擬機棧或者說虛擬機棧中的局部變量表部分。

        局部變量表存放了編譯器可知的各種基本數據類型、對象引用和returnAddress類型(指向一條字節碼指令的地址)。局部變量表所需的內存空間在編譯器完成分配,當進入一個方法時這個方法需要在幀中分配多大的內存空間是完全確定的,運行期間不會改變局部變量表的大小。(64爲長度的long和double會佔用兩個局部變量空間,其他的數據類型佔用一個)

        Java虛擬機棧可能出現兩種類型的異常:1. 線程請求的棧深度大於虛擬機允許的棧深度,將拋出StackOverflowError。2.虛擬機棧空間可以動態擴展,當動態擴展是無法申請到足夠的空間時,拋出OutOfMemory異常。


        本地方法棧

        本地方法棧和虛擬機棧基本類似,只不過Java虛擬機棧執行的是Java代碼(字節碼),本地方法棧中執行的是本地方法的服務。本地方法棧中也會拋出StackOverflowError和OutOfMemory異常。

        

        

        堆是Java虛擬機所管理的內存中最大的一塊。堆是所有線程共享的一塊區域,在虛擬機啓動時創建。堆的唯一目的是存放對象實例,幾乎所有的對象實例都在這裏分配,不過隨着JIT編譯器的發展和逃逸技術的成熟,棧上分配和標量替換技術使得這種情況發生着微妙的變化,對上分配正變得不那麼絕對。

附:在Java編程語言和環境中,即時編譯器(JIT compiler,just-in-time compiler)是一個把Java的字節碼(包括需要被解釋的指令的程序)轉換成可以直接發送給處理器的指令的程序。當你寫好一個Java程序後,源語言的語句將由Java編譯器編譯成字節碼,而不是編譯成與某個特定的處理器硬件平臺對應的指令代碼(比如,Intel的Pentium微處理器或IBM的System/390處理器)。字節碼是可以發送給任何平臺並且能在那個平臺上運行的獨立於平臺的代碼。

        Java堆是垃圾收集器管理的主要區域,所以也稱爲“GC堆”。由於現在的垃圾收集器基本上都是採用分代收集算法,所以Java堆還可細分爲:新生代和老生代。在細緻一點可分爲Eden空間,From Survivor空間,To Survivor空間。如果從內存分配的角度看,線程共享的Java堆可劃分出多個線程私有的分配緩衝區。不過無論如何劃分,都與存放內容無關,無論哪個區域,都是用來存放對象實例。細分的目的是爲了更好的回收內存或者更快的分配內存。

        Java堆可以是物理上不連續的空間,只要邏輯上連續即可,主流的虛擬機都是按照可擴展的方式來實現的。如果當前對中沒有內存完成對象實例的創建,並且不能在進行內存擴展,則會拋出OutOfMemory異常。


         方法區

         方法區也是線程共享的區域,用於存儲已經被虛擬機加載的類信息,常量,靜態變量和即時編譯器(JIT)編譯後的代碼等數據。Java虛擬機把方法區描述爲堆的一個邏輯分區,不過方法區有一個別名Non-Heap(非堆),用於區別於Java堆區。

         Java虛擬機規範對這個區域的限制也非常寬鬆,除了可以是物理不連續的空間外,也允許固定大小和擴展性,還可以不實現垃圾收集。相對而言,垃圾收集行爲在這個區域是比較少出現的(所以常量和靜態變量的定義要多注意)。方法區的內存收集還是會出現,不過這個區域的內存收集主要是針對常量池的回收和對類型的卸載。

        一般來說方法區的內存回收比較難以令人滿意。當方法區無法滿足內存分配需求時將拋出OutOfMemoryError異常。

 

       運行時常量池

       運行時常量池是方法區的一部分,Class文件中除了有類的版本,字段,方法,接口等信息以外,還有一項信息是常量池用於存儲編譯器生成的各種字面量和符號引用,這部分信息將在類加載後存放到方法區的運行時常量池中。Java虛擬機對類的每一部分(包括常量池)都有嚴格的規定,每個字節用於存儲哪種數據都必須有規範上的要求,這樣才能夠被虛擬機認可,裝載和執行。一般來說,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。

        運行時常量池相對於Class文件常量池的另外一個重要特徵是具備動態性,Java虛擬機並不要求常量只能在編譯期產生,也就是並非預置入Class文件常量池的內容才能進入方法區的運行時常量池中,運行期間也可將新的常量放入常量池中。

       常量池是方法區的一部分,所以受到內存的限制,當無法申請到足夠內存時會拋出OutOfMemoryError異常

       

       對象訪問

       對象訪問在Java語言中無處不在,即使是最簡單的訪問,也會涉及到Java棧,java堆,方法區這三個最重要的內存區域之間的關聯關係。如下面的代碼:

       Object obj = new Object();

       假設這段代碼出現在方法體中,那麼“Object obj”部分的語義將會反映到Java棧的本地變量表中,作爲一個reference類型的數據存在。而“new Object();”部分的語義將會反應到Java堆中,形成一塊存儲Object類型所有實例數據值(Instance Data)的結構化內存,根據具體類型以及虛擬機實現的對象分佈的不同,這塊內存的長度是不固定的。另外,在JAVA堆中還必須包含能查找到此對象內存數據的地址信息,這些類型數據則存儲在方法區中。

       由於reference類型在Java虛擬機中之規定了指向對象的引用,並沒有規定這個引用要通過哪種方式去定位,以及訪問到Java堆中的對象的具體位置,因此虛擬機實現的對象訪問方式會有所不同。主流的訪問方式有兩種:句柄訪問方式和直接指針。

       1. 如果使用句柄訪問方式,Java堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的地址,而句柄中包含了對象實例數據和類型數據各自的具體地址信息。

     

       2. 如果通過直接指針方式訪問,Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,reference中直接存儲的就是對象的地址。

        

        兩種方式各有優勢,局並訪問方式最大的好處是reference中存放的是穩定的句柄地址,在對象被移動時,只會改變句柄中的實例數據指針,而reference本身不需要被修改。而指針訪問的最大優勢是速度快,它節省了一次指針定位的開銷,由於對象訪問在Java中非常頻繁,一次這類開銷積少成多後也是一項非常可觀的成本。

        具體的訪問方式都是有虛擬機指定的,虛擬機Sun HotSpot使用的是直接指針方式,不過從整個軟件開發的範圍來看,各種語言和框架使用句柄訪問方式的情況十分常見。


java中的String類常量池詳解


從一個博客上看到的6個題,先看看吧,如果都會了,這部分的知識就掌握的不錯啦!輸出結果在代碼註釋後面:


test1:

複製代碼
package StringTest;

public class test1 {

/**
*
@param args
*/
public static void main(String[] args){
String a
= "a1";
String b
= "a"+ 1;
System.out.println(a
==b);
}
//true

}
複製代碼



test2:

複製代碼
package StringTest;

public class test2 {

/**
*
@param args
*/
public static void main(String[] args){
String a
= "ab";
String bb
= "b";
String b
= "a"+ bb; //編譯器不能確定爲常量
System.out.println(a==b);
}
//false

}
複製代碼

test3:

複製代碼
package StringTest;

public class test3 {

/**
*
@param args
*/
public static void main(String[] args){
String a
= "ab";
final String bb = "b";
String b
= "a"+ bb; //bb加final後是常量,可以在編譯器確定b
System.out.println(a==b);
}
//true

}
複製代碼

test4:

複製代碼
package StringTest;

public class test4 {

/**
*
@param args
*/
public static void main(String[] args){
String a
= "ab";
final String bb = getBB();
String b
= "a"+ bb;//bb是通過函數返回的,雖然知道它是final的,但不知道具體是啥,要到運行期才知道bb的值
System.out.println(a==b);
}
//false
private static String getBB(){ return "b"; }

}
複製代碼

test5:

複製代碼
package StringTest;

public class test5 {

/**
*
@param args
*/
private static String a = "ab";
public static void main(String[] args){
String s1
= "a";
String s2
= "b";
String s
= s1 + s2;//+的用法
System.out.println(s == a);
System.out.println(s.intern()
== a);//intern的含義
}//flase true

}
複製代碼

test6:

複製代碼
package StringTest;

public class test6 {

/**
*
@param args
*/
private static String a = new String("ab");
public static void main(String[] args){
String s1
= "a";
String s2
= "b";
String s
= s1 + s2;
System.out.println(s
== a);
System.out.println(s.intern()
== a);
System.out.println(s.intern()
== a.intern());
}
//flase false true
}
複製代碼

-------------------------------------------------------------------------------------------------------------------------------------------------


String常量池詳解:

  1.String使用private final char value[]來實現字符串的存儲,也就是說String對象創建之後,就不能再修改此對象中存儲的字符串內容,就是因爲如此,才說String類型是不 可變的(immutable)。String類有一個特殊的創建方法,就是使用""雙引號來創建.例如new String("i am")實際創建了2個
  String對象,一個是"i am"通過""雙引號創建的,另一個是通過new創建的.只不過他們創建的時期不同,
  一個是編譯期,一個是運行期!java對String類型重載了+操作符,可以直接使用+對兩個字符串進行連接。運行期調用String類的intern()方法可以向String Pool中動態添加對象。
  
  例1
  String s1 = "sss111";
  //此語句同上
  String s2 = "sss111";
  System.out.println(s1 == s2); //結果爲true
  例2
  String s1 = new String("sss111");
  String s2 = "sss111";
  System.out.println(s1 == s2); //結果爲false
  例3
  String s1 = new String("sss111");
  s1 = s1.intern();
  String s2 = "sss111";
  System.out.println(s1 == s2);//結果爲true
  例4
  String s1 = new String("111");
  String s2 = "sss111";
  String s3 = "sss" + "111";
  String s4 = "sss" + s1;
  System.out.println(s2 == s3); //true
  System.out.println(s2 == s4); //false
  System.out.println(s2 == s4.intern()); //true
  

  結果上面分析,總結如下:

   1.單獨使用""引號創建的字符串都是常量,編譯期就已經確定存儲到String Pool中;

  2,使用new String("")創建的對象會存儲到heap中,是運行期新創建的;

  3,使用只包含常量的字符串連接符如"aa" + "aa"創建的也是常量,編譯期就能確定,已經確定存儲到String Pool中;

  4,使用包含變量的字符串連接符如"aa" + s1創建的對象是運行期才創建的,存儲在heap中;

  還有幾個經常考的面試題:
  
  String s1 = new String("s1") ;
  String s2 = new String("s1") ;
  上面創建了幾個String對象?
  答案:3個 ,編譯期Constant Pool中創建1個,運行期heap中創建2個.(用new創建的每new一次就在堆上創建一個對象,用引號創建的如果在常量池中已有就直接指向,不用創建

  String s1 = "s1";
  String s2 = s1;
  s2 = "s2";
  s1指向的對象中的字符串是什麼?
  答案: "s1"。(永遠不要忘了String不可變的,s2 = "s2";實際上s2的指向就變了,因爲你不可以去改變一個String,)

--------------------------------------------------------------------------------------------------------------------------------------------------

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

比較類裏面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個對象時,用==,下面用例子說明上面的理論。 
String str1 = "abc"; 
String str2 = "abc"; 
System.out.println(str1==str2); //true 
可以看出str1和str2是指向同一個對象的。 

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類的immutable性質,當String變量需要經常變換其值時,應該考慮使用StringBuffer類,以提高程序效率。 
1. 首先String不屬於8種基本數據類型,String是一個對象。 
因爲對象的默認值是null,所以String的默認值也是null;但它又是一種特殊的對象,有其它對象沒有的一些特性。 

2. new String()和new String(”")都是申明一個新的空字符串,是空串不是null; 

3. String str=”kvill”;String str=new String (”kvill”)的區別

看例1: 

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;用new String() 創建的字符串不是常量,不能在編譯期就確定,所以new String() 創建的字符串不放入常量池中,它們有自己的地址空間。 

看例2: 
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 

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

4. String.intern(): 
再補充介紹一點:存在於.class文件中的常量池,在運行期被JVM裝載,並且可以擴充。String的intern()方法就是擴充常量池的 一個方法;當一個String實例str調用intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,如果有,則返回其的引用,如果沒有,則在常量池中增加一個Unicode等於str的字符串並返回它的引用;看例3就清楚了 

例3: 
String s0= "kvill"; 
String s1=new String("kvill"); 
String s2=new String("kvill"); 
System.out.println( s0==s1 ); 
System.out.println( "**********" ); 
s1.intern(); 
s2=s2.intern(); //把常量池中"kvill"的引用賦給s2 
System.out.println( s0==s1); 
System.out.println( s0==s1.intern() ); 
System.out.println( s0==s2 ); 
結果爲: 
false 
********** 
false //雖然執行了s1.intern(),但它的返回值沒有賦給s1 
true //說明s1.intern()返回的是常量池中"kvill"的引用 
true 

最 後我再破除一個錯誤的理解:有人說,“使用 String.intern() 方法則可以將一個 String 類的保存到一個全局 String 表中 ,如果具有相同值的 Unicode 字符串已經在這個表中,那麼該方法返回表中已有字符串的地址,如果在表中沒有相同值的字符串,則將自己的地址註冊到表中”如果我把他說的這個全局的 String 表理解爲常量池的話,他的最後一句話,”如果在表中沒有相同值的字符串,則將自己的地址註冊到表中”是錯的: 

看例4: 
String s1=new String("kvill"); 
String s2=s1.intern(); 
System.out.println( s1==s1.intern() ); 
System.out.println( s1+" "+s2 ); 
System.out.println( s2==s1.intern() ); 
結果: 
false 
kvill kvill 
true 

在這個類中我們沒有聲名一個”kvill”常量,所以常量池中一開始是沒有”kvill”的,當我們調用s1.intern()後就在常量池中新添加了一個”kvill”常量,原來的不在常量池中的”kvill”仍然存在,也就不是“將自己的地址註冊到常量池中”了。 
s1==s1.intern()爲false說明原來的”kvill”仍然存在;s2現在爲常量池中”kvill”的地址,所以有s2==s1.intern()爲true。 


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