Java記憶體分析一

Diy_os發表於2015-10-26
如果瞭解java記憶體的使用情況,對於程式的執行情況會更加清晰。關於java記憶體深度解析,請讀者自行參考JVM有關書籍文件,會得到更多更完善的資訊。下面透過一段簡單的程式碼來分析。
首先簡單介紹下JVM執行時記憶體資料區:

第一塊:PC暫存器
PC暫存器是用於儲存每個執行緒下一步將執行的JVM指令,如該方法為native的,則PC暫存器中不儲存任何資訊。

第二塊:JVM棧
JVM棧是執行緒私有的,每個執行緒建立的同時都會建立JVM棧,JVM棧中存放的為當前執行緒中區域性基本型別的變數(java中定義的八種基本型別:boolean、char、byte、short、int、long、float、double)、部分的返回結果以及Stack Frame,非基本型別的物件在JVM棧上僅存放一個指向堆上的地址。儲存區域性變數的值,包括:1.用來儲存基本資料型別的值;2.儲存類的例項,即堆區物件的引用(指標)。也可以用來儲存載入方法時的幀。

第三塊:堆(Heap)
1.它是JVM用來儲存物件例項以及陣列值的區域,可以認為Java中所有透過new建立的物件的記憶體都在此分配,Heap中的物件的記憶體需要等待GC進行回收。堆是JVM中所有執行緒共享的,因此在其上進行物件記憶體的分配均需要進行加鎖,這也導致了new物件的開銷是比較大的。
2.Sun Hotspot JVM為了提升物件記憶體分配的效率,對於所建立的執行緒都會分配一塊獨立的空間TLAB(Thread Local AllocationBuffer),其大小由JVM根據執行的情況計算而得,在TLAB上分配物件時不需要加鎖,因此JVM在給執行緒的物件分配記憶體時會盡量的在TLAB上分配,在這種情況下JVM中分配物件記憶體的效能和C基本是一樣高效。的,但如果物件過大的話則仍然是直接使用堆空間分配。
3.TLAB僅作用於新生代的Eden Space,因此在編寫Java程式時,通常多個小的物件比大的物件分配起來更加高效。
4.用來存放動態產生的資料,比如new出來的物件。注意建立出來的物件只包含屬於各自的成員變數,並不包括成員方法。因為同一個類的物件擁有各自的成員變數,儲存在各自的堆中,但是他們共享該類的方法,並不是每建立一個物件就把成員方法複製一次。

第四塊:方法區域(Method Area)
(1)在Sun JDK中這塊區域對應的為PermanetGeneration,又稱為持久代。
(2)方法區域存放了所載入的類的資訊(名稱、修飾符等)、類中的靜態變數、類中定義為final型別的常量、類中的Field資訊、類中的方法資訊,當開發人員在程式中透過Class。物件中的getName、isInterface等方法來獲取資訊時,這些資料都來源於方法區域,同時方法區域也是全域性共享的,在一定的條件下它也會被GC,當方法區域需要使用的記憶體超過其允許的大小時,會丟擲OutOfMemory的錯誤資訊。

第五塊:執行時常量池(Runtime Constant Pool)
存放的為類中的固定的常量資訊、方法和Field的引用資訊等,其空間從方法區域中分配。

第六塊:本地方法堆疊(Native Method Stacks)
JVM採用本地方法堆疊來支援native方法的執行,此區域用於儲存每個native方法呼叫的狀態。

下面透過一段簡單的程式來分析:

點選(此處)摺疊或開啟

  1. public class Student {
  2.     
  3.     private int age;
  4.     private int height;
  5.     private String name;
  6.     
  7.     Student(){
  8.         
  9.         
  10.     }
  11.     
  12.     Student(int a,int h,String n){
  13.         
  14.         age = a;
  15.         height = h;
  16.         name = n;
  17.     }

  18.     public int getage(){
  19.         
  20.         return age;
  21.     }
  22.     
  23.     public int setage(int age1){
  24.         
  25.         age =0;
  26.         return age;
  27.     }
  28.     public int getheight(){
  29.         
  30.         return height;
  31.     }
  32.     
  33.     public String getname(){
  34.         
  35.         return name;
  36.     }
  37.     
  38.     public void setStudent1(Student h){
  39.         
  40.         h.setage(1);
  41.     }
  42.     
  43.     public void setStudent2(Student s){
  44.         
  45.         s = new Student(1,1,"www");
  46.     }
  47.     
  48.     public String toString(){
  49.         
  50.         return age + " " + height + " " + name;
  51.     }
  52.     public static void main(String[] args){
  53.         
  54.         Student one = new Student();    //1
  55.         int  s = 5;                                   //2
  56.         Student a = new Student(20, 177, "diy");   //3
  57.         Student b = new Student(30, 180, "os");    //4
  58.         System.out.println(one);        //p
  59.         one.setage(s);       //5
  60.         System.out.println(one.getage());   //m
  61.         one.setStudent1(a);                 //6
  62.         System.out.println(a);              //q
  63.         one.setStudent2(b);                 //7
  64.         System.out.println(b);              //r
  65.     
  66.     }
  67.     
  68. }

程式的執行從main函式開始:

1:在棧中存入了一型別為Student的區域性變數one,其它的值我們未知,然後在堆中建立了一塊物件記憶體區域,系統會呼叫建構函式來初始化物件成員變數,由於new Student(),無參,所以系統會呼叫無引數構造器來初始化成員變數:
上述無參建構函式可以寫成:
Student(){
  
  age = 0;
  height = 0;
  name = null;

}
當執行p步驟時,系統會自動呼叫toString()方法,列印成員變數,得到的是:0 0 null
此時棧中的區域性變數one指向了堆中被初始化的Student物件記憶體區域(其實one中存放的就是被初始化的Student物件在堆中的實體地址,作用和C/C++指標一樣)

2.在棧中壓入一區域性變數s,並且賦值是5
由此可以發現,基礎型別的變數就佔用一塊記憶體,引用型別的變數佔用兩塊記憶體

3.在棧中申請一塊記憶體存放型別為Student的區域性變數a,並在堆中new出了一塊Student物件記憶體,然後呼叫建構函式 Student(int a,int h,String n),在棧中依次分配三塊記憶體區域存放區域性變數a,h,n,這三個變數分別被賦值為20, 177”diy",在建構函式內部,該例項的成員變數分別被區域性變數賦值:
        age = a;
     
 height = h;
      
name = n;
建構函式執行完畢後,區域性變數的記憶體空間被依次收回n,h,a,其記憶體空間消失(注意棧中儲存的特性,先入後出)
此時棧中Student 的引用a指向堆中的該Student 例項記憶體區域

4.分析同3

5.Student的引用one,呼叫函式setage(int age1),會在棧中分配區域性變數age1的記憶體空間,並賦值為s的值為5,但是此時age被賦上了0,並返回了age的值,返回的值也會在棧中申請一塊我們不知道的記憶體空間,用來放返回的age值,該函式呼叫完成,區域性變數age1記憶體空間被收回,我們不知道名字的記憶體空間也被收回。

6.Student引用one呼叫函式setStudent1(),引數傳入了3中的a 
setStudent1(Student h),該步在函式呼叫時,在棧中分配了一塊記憶體用來存放型別為Student的區域性變數h,h被賦值上a,此時a和h同時指向堆中的同一例項物件,h呼叫setage()函式來修改了age,此步與5有交叉,不做分析。該函式呼叫完成後,引用h的記憶體空間被收回,記憶體空間消失。但是此步驟的修改是永久性的。

7.Student的引用one呼叫函式setStudent2(),並傳入引數4中的b
setStudent2(Student s),該步在函式呼叫時,在棧中分配了一塊記憶體用來存放型別為Student的區域性變數s,s被賦值上b,此時b和s同時指向堆中的同一例項物件,但是在函式內部,又在堆中new Student(1,1,"www"),new出了一塊例項記憶體空間,此時把該例項所在堆中的地址賦值給s,所以s現在不再和b指向同一堆中例項記憶體空間,而是指向新的例項記憶體空間。當該函式呼叫完成,s的記憶體空間被收回,空間消失,而s之前指向的堆記憶體例項空間沒有引用指向,JAVA的垃圾收集器會在一定的時機回收該堆記憶體空間。

關於p,q,r,系統會自動呼叫toString()函式,列印資料成員
而main()中,該函式執行完後,會根據入棧順序相反的順序而依次回收棧記憶體空間,在堆中new出的物件,也會在沒有引用指向的同時,被Java垃圾收集器在一定的時機回收堆記憶體空間。

上述分析如有錯誤或不妥之處,請讀者指正!

參考參考:http://blog.csdn.net/fujianianhua/article/details/38416247

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/29876893/viewspace-1816246/,如需轉載,請註明出處,否則將追究法律責任。

相關文章