1. 程式人生 > >Java的記憶體分配機制(初步整理)

Java的記憶體分配機制(初步整理)

    Java程式是執行在Java虛擬機器(Java Virtual Machine,JVM)上的,可以把JVM理解為Java程式和作業系統之間的橋樑,JVM實現了Java的跨平臺,Java記憶體分配原理一切都是在JVM中進行的,JVM是記憶體分配原理的基礎與前提。

    一個完整的Java程式執行過程會涉及以下記憶體區域:

 暫存器:JVM內部虛擬暫存器,存取速度非常快,程式不可控制。

記憶體:儲存區域性變數的值。在函式中定義的一些基本資料型別的變數和物件的引用變數都是在函式的棧記憶體中分配,當一段程式碼塊定義一個變數時,Java就在棧中為這個變數分配記憶體空間,當超過變數的作用域後(比如:在函式A中呼叫函式B,在函式B中定義變數a,變數a的作用域只是函式B,在函式B執行完以後,變數a會自動被銷燬,分配給它的記憶體會被收回),Java會自動釋放掉為該變數分配的記憶體空間,該記憶體空間可以立即被另作它用。

堆記憶體:用來存放Java世界中幾乎所有的物件例項(如new建立的物件和陣列,注意:創建出來的物件只包含屬於各自的成員變數,不包括成員方法,因為同一個類的物件擁有各自的成員變數,儲存在各自的堆中,但是它們共享該類的方法,並不是每建立一個物件就把成員方法複製一次),在堆中分配記憶體,由Java虛擬機器的自動垃圾回收器(GC)來管理。在堆中產生了一個物件或陣列後,還可以在棧中定義一個特殊的變數,讓棧中的這個變數的取值等於陣列或是物件在堆記憶體中的首地址,棧中的這個變數就成了物件或陣列的引用變數,以後就可以在程式中使用棧中的引用變數來訪問堆中的物件或陣列,引用變數就相當於為物件或陣列取的一個名稱。引用變數是普通變數,定義時在棧中分配,引用變數在程式執行到其作用域之外後被釋放。而物件或陣列本身在堆中分配,即使程式執行到使用new產生物件或陣列的語句所在的程式碼塊之外,物件或陣列本身所佔據的記憶體不會被釋放,物件和陣列在沒有引用變數指向它的時候才變為垃圾,不能再被使用,但仍然佔據記憶體空間不放,在隨後的一個不

確定的時間被垃圾回收器(GC)收走。這也是Java比較佔記憶體的原因,實際上,棧中的變數指向堆記憶體中變數就是Java中的指標。

常量池:JVM為每個已載入的型別維護一個常量池,常量池就是這個型別用到的常量的一個有序集合。包括直接常量(基本型別,String)和對其他型別、方法、欄位的符號引用(*)。池中的資料和陣列一樣通過索引訪問。由於常量池包含了一個型別所有的對其他型別、方法、欄位的符號引用,所以常量池在Java的動態連結中起了核心作用。常量池存在於堆中。

程式碼段:用來存放從硬碟上讀取的源程式程式碼。

資料段:用來存放static定義的靜態成員,一直佔用記憶體。

 記憶體表示圖:


    預備知識:

 1、一個Java檔案,只要有main入口方法,我們就認為這是一個Java程式,可以單獨編譯執行。

 2、無論是普通型別的變數還是引用型別的變數(俗稱例項),都可以作為區域性變數,他們都可以出現在棧中。只不過普通型別的變數在棧中直接儲存它所對應的值,而引用型別的變數儲存的是一個指向堆區的指標,通過這個指標,就可以找到這個例項在堆區對應的物件。因此普通型別變數只在棧區佔用一塊記憶體,而引用型別變數要在棧區和堆區各佔一塊記憶體。

示例:

 1)、JVM自動尋找main方法,執行第一句程式碼,建立一個Test類的例項,在棧中分配一塊記憶體,存放一個指向堆區物件的指標110925。

 2)、建立一個int型的變數date,由於是基本型別,直接在棧中存放date對應的值9。

 3)、建立兩個BirthDate類的例項d1和d2,在棧中分別存放了對應的指標指向各自的物件,他們在例項化時呼叫了有引數的構造方法,因此物件中有自定義初始值。

 呼叫test物件的change1方法,並且以date為引數。JVM讀到這段程式碼時,檢測到 i 是區域性變數,因此會把 i 放到棧中,並且把date的值賦給 i 。

 把1234賦給 i 。很簡單的一步。

 change1方法執行完畢,立即釋放區域性變數 i 所佔用的棧空間。

 呼叫test物件的change2方法,以例項d1為引數,JVM檢測到change2方法中的b引數為區域性變數,立即加入到棧中,由於是引用型別的變數,所以b中儲存的是d1中的指標,此時b和d1指向同一個堆中的物件。在b和d1之間傳遞是指標。

 change2方法中又例項化了一個BirthDate物件,並且賦給b。在內部的執行過程是:在堆區new了一個物件,並且把該物件的指標儲存在棧中的b對應空間,此時例項b不再指向例項d1所指向的物件,但是例項d1所指向的物件並無變化,這樣無法對d1造成任何影響。

 change2方法執行完畢,立即釋放區域性引用變數b所佔的棧空間,注意只是釋放了棧空間,堆空間要等到自動回收。

 呼叫test例項的change3方法,以例項d2為引數,同理,JVM會在棧中為區域性引用變數b分配空間,並且把d2中的指標存放在b中,此時d2和b指向同一個物件。再呼叫例項b的setDay方法,其實就是呼叫d2指向的物件的setDay方法。

呼叫例項b的setDay方法會影響d2,因為二者指向的是同一個物件。

 change3方法執行完畢,立即釋放區域性引用變數b。

 以上就是Java程式執行時記憶體分配的大致情況,就是兩種型別的變數:基本型別和引用型別。二者作為區域性變數,都放在棧中,基本型別直接在棧中儲存值,引用型別只儲存一個指向堆區的指標,真正的物件在堆裡。作為引數時基本型別就直接傳值,引用型別傳指標。

    小結:

1、分清楚什麼是例項什麼是物件。

 Class a = new Class();此時a叫例項,而不能說是物件。例項在棧中,物件在堆中,操作例項實際上是通過例項的指標間接操作物件。多個例項可以指向同一個物件。

 2、棧中的資料和堆中的資料銷燬並不是同步的。方法一旦結束,棧中的區域性變數立即銷燬,但是堆中的物件不一定銷燬。因為可能有其他變數也指向了這個物件,直到棧中沒有變數指向堆中的物件時,它才銷燬,而且還不是馬上銷燬,要等垃圾回收掃描時才可以被銷燬。

 3、每一個應用程式都對應唯一的一個JVM例項,每一個JVM例項都有自己的記憶體區域,互不影響。並且這些記憶體區域是所有執行緒共享的。這裡提到的棧和堆都是整體上的概念,這些堆疊還可以細分。以上的堆、棧、程式碼段、資料段等都是相對於應用程式而言的。

 4、類的成員變數在位於資料段中一直佔用記憶體。而類的方法卻是該類的所有物件共享的,只有一套,物件使用方法的時候方法才被壓入棧,方法不使用則不佔用記憶體。

    常量池的補充:

預備知識:基本型別和基本型別的包裝類。基本型別有:byte、short、char、int、long、boolean。基本型別的包裝類:Byte、Short、Character、Integer、Long、Boolean。注意區分大小寫。二者的區別是:基本型別體現在程式中是普通變數,基本型別的包裝類是類,體現在程式中是引用變數。因此二者在記憶體中的儲存位置不同:基本型別儲存在棧中,而基本型別的包裝類儲存在堆中。上面的這些包裝類都實現了常量池的技術,另外兩種浮點數型別的包裝類則沒有實現。另外,String型別也實現了常量池技術。

 常量池在java用於儲存在編譯期已確定的,已編譯的class檔案中的一份資料。它包括了類、方法、介面等中的常量,也包括字串常量,如String s="java"這種申明方式,當然也可以擴充,執行器產生的常量也會放入常量池,因此認為常量池是JVM的一塊特殊的記憶體空間。

 常量池中除了包含程式碼中所定義的各種基本型別(如int、long等)和物件型(如String及陣列)的常量值外,還包含一些以文字形式出現的符號引用(*),比如類和介面的全限定名、欄位的名稱和描述符、方法的名稱和描述符。

 所以,與Java語言中的所謂的“常量”不同,class檔案中的“常量”內容很豐富,這些常量集中在class中的一個區域存放,一個緊接一個,稱為“常量池”。

示例:


結果:


    結果分析:

1、i和i0均是普通型別(int)的變數,所以資料直接儲存在棧中,而棧有一個很重要的特性:棧中的資料可以分享。當我們定義了int i = 40;,再定義int i0 = 40;,這時候會自動檢查棧中是否有40這個資料,如果有,i0會直接指向i的40,不會再新增一個新的40。

 2、i1和i2均是引用型別,在棧中儲存指標,因為Integer是包裝類。由於Integer包裝類實現了常量池技術,因此i1、i2的40均是從常量池中獲取的,均指向同一個地址,因此i1=i2。

 3、很明顯這是一個加法運算,Java的數學運算都是在棧中進行的,Java會自動對i1、i2進行拆箱操作轉化成整形,因此i1在數值上等於i2+i3。

 4、i4和i5均是引用型別,在棧中儲存指標,因為Integer是包裝類。但是由於他們各自都是new出來的,因此不再從常量池尋找資料,而是從堆中各自new一個物件,然後各自儲存指向物件的指標,所以i4和i5不相等,因為他們所存指標不同,所指向物件不同。

 5、這是一個加法運算,和3同理。

 6、d1和d2均是引用型別,在棧中儲存指標,因為Double是包裝類。但Double包裝類沒有實現常量池技術,因此Double d1 = 1.0; 相當於Double d1 = new Double(1.0);,是從堆new一個物件,d2同理。因此d1和d2存放的指標不同,指向的物件不同,所以不相等。

    示例:


結果分析:

用new String() 建立的字串不是常量,不能在編譯器就能確定,所以new String()建立的字串不放入常量池中,他們有自己的地址空間。

 String物件(記憶體)的不變性機制會使修改String字串時,產生大量的物件,因為每次改變字串,都會生成一個新的String。Java為了更有效的使用記憶體,常量池在編譯期遇見String字串時,它會檢查該池內是否已經存在相同的String字串,如果找到,就把新變數的引用指向現有的字串物件,不建立任何新的String常量物件,沒找到再建立新的。所以對一個字串物件的任何修改,都會產生一個新的字串物件,原來的依然存在,等待垃圾回收。

 String a = "test";

 String b = "test";

 String b = b + "java";

 a、b同時指向常量池中的常量值”test“,b = b + "java"之後,b原先指向一個常量,內容為"test",通過對b進行+"java"操作後,b之前所指向的那個值沒有改變,但此時b不指向原來那個變數值了,而指向了另一個String變數,內容為”test java“。原來那個變數還存在於記憶體之中,只是b這個變數不再指向它了。

    示例:

在值小於127時可以使用常量池

  Integer i1 = 127;

  Integer i2 = 127;

  System.out.println(i1==i2); // true

  值大於127時,不會從常量池中取物件

  Integer i3 = 128;

  Integer i4 = 128;

  System.out.println(i3==i4); // false

  Boolean類也實現了常量池技術

  Boolean b1 = true;

  Boolean b2 = true;

  System.out.println(b1==b2); // true

  浮點型別的包裝類沒有實現常量池技術

  Double d1 = 1.0;

  Double d2 = 1.0;

  System.out.println(d1==d2); // false

    小結:

1、常量池維護的常量僅僅是【-128至127】這個範圍內的常量,如果常量值超過這個範圍,就會從堆中建立物件,不再從常量池中取。如:Integer i1 = 400;Integer i2 = 400;很明顯超過了127,無法從常量池中獲取常量,就用從堆中new新的Integer物件,這是i1和i2就不相等了。

 2、String型別也實現了常量池技術,但是稍微有點不同,String型是先檢測常量池中有沒有對應字串,如果有,則取出來,如果沒有,則把當前的新增進去。