1. 程式人生 > 實用技巧 >Java記憶體機制

Java記憶體機制

  Java的JVM的記憶體可以分為3個區:堆(heap)、棧(stack)和靜態區(method)。

一、Java記憶體區域概念

堆區:堆主要存放Java在執行過程中new出來的物件和陣列以及物件的例項變數,凡是通過new生成的物件都存放在堆中,jvm只有一個堆區被所有執行緒共享,對於堆中的物件生命週期的管理由Java虛擬機器的垃圾回收機制GC進行回收和統一管理。

棧區:棧主要存放在執行期間用到的一些區域性變數(基本資料型別的變數)或者是指向其他物件的一些引用,當一段程式碼或者一個方法呼叫完畢後,棧中為這段程式碼所提供的基本資料型別或者物件的引用立即被釋放;另外需注意的是棧中存放變數的值是可以共享的,優先在棧中尋找是否有相同變數的值,如果有直接指向這個值,如果沒有則另外分配。每個執行緒包含一個棧區,每個棧中的資料都是私有的,其他棧不能訪問。

靜態區:存放類中以static宣告的靜態成員變數

方法區:主要存放一些程式碼段以供類呼叫的時候所共用。

常量區:常量池在編譯期間就將一部分資料存放於該區域,包含基本資料型別如int、long等和物件型別String、陣列等並以final宣告的常量值。特別注意的是對於執行期位於棧中的String常量的值可以通過 String.intern()方法將該值置入到常量池中。

堆和棧的區別

  • 棧(stack)與堆(heap)都是Java用來在Ram中存放資料的地方。Java自動管理棧和堆,程式設計師不能直接地設定棧或堆。
  • 棧中存放區域性變數(基本型別的變數)和物件的引用。棧的優勢是,存取速度比堆要快,僅次於暫存器,棧資料可以共享。但缺點是,存在棧中的資料大小與生存期必須是確定的,缺乏靈活性。棧是跟隨執行緒的,有執行緒就有棧。
  • 堆中存放物件,包括物件變數以及物件方法。堆的優勢是可以動態地分配記憶體大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的資料。但缺點是,由於要在執行時動態分配記憶體,存取速度較慢。堆是跟隨JVM的,有JVM就有堆記憶體。

為什麼把堆和棧區分出來

  1. 從軟體設計的角度上來看,棧代表了處理邏輯,而堆代表了資料。這樣分開,使得處理邏輯更為清晰。分而治之的思想。這種隔離、模組化的思想在軟體設計的方方面面都有體現;
  2. 堆與棧的分離,使得堆中的內容可以被多個棧共享(也可以理解為多個執行緒訪問同一個物件)。這種共享的收益是很多的。一方面這種共享提供了一種有效的資料互動方式(如:共享記憶體),另一方面,堆中的共享常量和快取可以被所有棧訪問,節省了空間;
  3. 棧因為執行時的需要,比如儲存系統執行的上下文,需要進行地址段的劃分。由於棧只能向上增長,因此就會限制住棧儲存內容的能力。而堆不同,堆中的物件是可以根據需要動態增長的,因此棧和堆的拆分,使得動態增長成為可能,相應棧中只需記錄堆中的一個地址即可;
  4. 面向物件就是堆和棧的完美結合。其實,面向物件方式的程式與以前結構化的程式在執行上沒有任何區別。但是,面向物件的引入,使得對待問題的思考方式發生了改變,而更接近於自然方式的思考。當我們把物件拆開,你會發現,物件的屬性其實就是資料,存放在堆中;而物件的行為(方法),就是執行邏輯,放在棧中。我們在編寫物件的時候,其實即編寫了資料結構,也編寫的處理資料的邏輯。

二、Java記憶體區域例項解析

/*
    Java記憶體儲存機制
 */
public class Ram {
    public static void main(String[] args) {
        String a = "a";
        String tempa="a";
        String b = "b";
        String c = "c";
        String abc = "abc";
        String a_b_c = "a"+"b"+"c";
        String a_b = a+b;
        String ab = "ab";
        String newabc = new String("abc");
        String abcintern = newabc.intern();
        final String finalb = "b";
        String a_b2 = "a"+finalb;

        System.out.println(tempa==a);//在棧中共享同一個值,共同指向常量池的字串a
        System.out.println(newabc==abc);//newabc指向的是堆中的一個引用,abc是在位於棧中的string物件的一個引用變數,然後在常量池中尋找字串abc
        System.out.println(a_b==ab);//a_b是在執行期字串的引用,而ab則是在編譯期間就指定了
        /*a_b_c在編譯期間就將常量字串連線到一起,所以他們指向同一個字串常量,而且"a"+"b"+"c",
        首先a和b組裝成一個常量ab放於常量池中,然後ab和c組裝在一起放於常量池中,然後將abc的地址賦給了a_b_c,
        由於String是不可變的,所以產生了很多臨時變數。
        */
        System.out.println(a_b_c==abc);
        System.out.println(abcintern==abc);//呼叫intern()方法則將abc字串放入了字串常量池,返回值則是直接指向常量池中的字串常量值所以相等
        System.out.println(ab==a_b2);//finalb是因為宣告為final修飾符它在編譯時被解析為常量值的一個本地拷貝儲存到自己的常量池中,相當於"a"+"b"
    }
}

小結

  1. 分清什麼是例項什麼是物件。Class a= new Class();此時a叫例項,而不能說a是物件。例項在棧中,物件在堆中,操作例項實際上是通過例項的指標間接操作物件。多個例項可以指向同一個物件。
  2. 棧中的資料和堆中的資料銷燬並不是同步的。方法一旦結束,棧中的區域性變數立即銷燬,但是堆中物件不一定銷燬。因為可能有其他變數也指向了這個物件,直到棧中沒有變數指向堆中的物件時,它才銷燬,而且還不是馬上銷燬,要等垃圾回收掃描時才可以被銷燬。
  3. 以上的棧、堆、程式碼段、資料段等等都是相對於應用程式而言的。每一個應用程式都對應唯一的一個JVM例項,每一個JVM例項都有自己的記憶體區域,互不影響。並且這些記憶體區域是所有執行緒共享的。這裡提到的棧和堆都是整體上的概念,這些堆疊還可以細分。
  4. 類的成員變數在不同物件中各不相同,都有自己的儲存空間(成員變數在堆中的物件中)。而類的方法卻是該類的所有物件共享的,只有一套,物件使用方法的時候方法才被壓入棧,方法不使用則不佔用記憶體。

三、記憶體相關知識點

Java是如何管理記憶體的

其中包括分配和釋放兩部分:

分配:記憶體的分配是由程式完成的,程式設計師需要通過關鍵字new為每個物件申請記憶體空間(基本型別除外),所有的物件都在堆(Heap)中分配空間。
釋放:物件的釋放是由垃圾回收機制決定和執行的,這樣做確實簡化了程式設計師的工作。但同時,它也加重了JVM的工作。因為,GC為了能夠正確釋放物件,GC必須監控每一個物件的執行狀態,包括物件的申請、引用、被引用、賦值等,GC都需要進行監控。

Java的記憶體洩漏

在java中,記憶體洩漏就是存在一些被分配的物件,這些物件有下面兩個特點,首先,這些物件是可達的,即在有向圖中,存在通路可以與其相連(也就是說仍存在該記憶體物件的引用);其次,這些物件是無用的,即程式以後不會再使用這些物件。如果物件滿足這兩個條件,這些物件就可以判定為Java中的記憶體洩漏,這些物件不會被GC所回收,然而它卻佔用記憶體。

Java中資料在記憶體是如何儲存的

1、基本資料型別

  Java的基本型別資料有八種,分別為int、short、long、byte、float、double、boolean、char。這個型別的資料定義基本是通過int a = 1等這樣的形式來定義的,這裡 a 是一個指向 int 型別的引用,指向3這個字面值。這些字面值的資料,由於大小可知,生存期可知(這些字面值定義在某個程式塊裡面,程式塊退出後,欄位值就消失了),出於追求速度的原因,就存在於棧中。另外,棧有一個很重要的特性,就是儲存在棧中的資料可共享。

2、物件

  在Java中,建立一個物件包括物件的宣告和例項化兩部分,這裡定義一個類

public class Rectangle {
double width;
double height;
public Rectangle( double w, double h){
w = width;
h = height;
}
}
  • 宣告物件時的記憶體模型:用Rectangle rect;宣告一個物件rect時,將在棧記憶體為物件的引用變數rect分配記憶體空間,但Rectangle的值為空,這裡稱rect是一個空物件。空物件不能使用,因為它還沒有引用任何實體
  • 物件例項化時的記憶體模型:當執行rect=new Rectangle(3,5);時,會做兩件事情,在堆記憶體中為類的成員變數width,height分配記憶體,並將其初始化為各資料型別的預設值;接著進行顯式初始化;最後呼叫成員方法,為成員變數賦值。返回堆記憶體中物件的引用(相當於首地址)給引用變數rect,以後就可以通過rect來引用堆記憶體中的物件了。

3、建立多個不同的物件例項

  一個類可以使用new運算子來建立多個不同的物件例項,這些物件例項在堆中被分配不同的記憶體空間,改變其中一個物件的狀態不會影響其他物件的狀態。

  例如:Rectangler1=newRectangle(3,5); Rectangler2=newRectangle(4,6);此時,將在堆記憶體中分別為兩個物件的成員變數分配記憶體空間,兩個物件在堆記憶體中佔據的空間是互不相同的。

  例如:Rectangler1=newRectangle(3,5); Rectangler2=r1; 此時,則在堆記憶體中建立了一個物件例項,在棧記憶體中建立了兩個物件引用,兩個物件引用指向同一個物件例項。

4、包裝類

  基本型別都有對應的包裝類:如int對應Integer類,double對應Double類等,基本型別的定義都是直接在棧中,如果用包裝類來建立物件,就和普通物件一樣了。例如:int i=0;i直接儲存在棧中。Integer i(i此時是物件)= new Integer(5);這樣,i物件資料儲存在堆中,i的引用儲存在棧中,通過棧中的引用來操作物件。

5、String

  String是一個特殊的包裝類資料。可以用以下兩種方式建立:String str =newString(“abc”);String str = “abc”;

  第一種建立方式,和普通物件的的建立過程一樣;

  第二種建立方式,java內部將此語句轉化為以下幾個步驟:(1)先定義一個名為str的對String類的物件引用變數:String str;(2)在棧中查詢有沒有存放值為”abc”的地址,如果沒有,則開闢一個存放字面值為”abc”地址,接著建立一個新的String類的物件o,並將o的字串值指向這個地址,而且在棧這個地址旁邊記下這個引用的物件o。如果已經有了值為”abc”的地址,則查詢物件o,並回o的地址。(3)將str指向物件o的地址。

6、陣列

  當定義一個數組,int x[];或int[] x;時,在棧記憶體中建立一個數組引用,通過該引用(即陣列名)來引用陣列。x=new int[3];將在堆記憶體中分配3個儲存int型資料的空間,堆記憶體的首地址放到棧記憶體中,每個陣列元素被初始化為0。

7、靜態變數

  用static的修飾的變數和方法,實際上是指定了這些變數和方法在記憶體中的”固定位置”-static storage,可以理解為所有例項物件共有的記憶體空間。static變數有點類似於C中的全域性變數的概念;靜態表示的是記憶體的共享,就是它的每一個例項都指向同一個記憶體地址。把static拿來,就是告訴JVM它是靜態的,它的引用(含間接引用)都是指向同一個位置,在那個地方,你把它改了,它就不會變成原樣,你把它清理了,它就不會回來了。

  那靜態變數與方法是在什麼時候初始化的呢?對於兩種不同的類屬性,static屬性與instance屬性,初始化的時機是不同的。instance屬性在建立例項的時候初始化,static屬性在類載入,也就是第一次用到這個類的時候初始化,對於後來的例項的建立,不再次進行初始化。

Java的記憶體管理例項

Java程式的多個部分(方法,變數,物件)駐留在記憶體中以下兩個位置:即堆和棧,現在我們只關心三類事物:例項變數,區域性變數和物件:例項變數和物件駐留在堆上,區域性變數駐留在棧上。

垃圾回收機制

問題一:什麼叫垃圾回收機制?
垃圾回收是一種動態儲存管理技術,它自動地釋放不再被程式引用的物件,按照特定的垃圾收集演算法來實現資源自動回收的功能。當一個物件不再被引用的時候,記憶體回收它佔領的空間,以便空間被後來的新物件使用,以免造成記憶體洩露。

問題二:java的垃圾回收有什麼特點?
jAVA語言不允許程式設計師直接控制記憶體空間的使用。記憶體空間的分配和回收都是由JRE負責在後臺自動進行的,尤其是無用記憶體空間的回收操作(garbagecollection,也稱垃圾回收),只能由執行環境提供的一個超級執行緒進行監測和控制。

問題三:垃圾回收器什麼時候會執行?
一般是在CPU空閒或空間不足時自動進行垃圾回收,而程式設計師無法精確控制垃圾回收的時機和順序等。、

問題四:什麼樣的物件符合垃圾回收條件?
當沒有任何獲得執行緒能訪問一個物件時,該物件就符合垃圾回收條件。

問題五:垃圾回收器是怎樣工作的?
垃圾回收器如發現一個物件不能被任何活執行緒訪問時,他將認為該物件符合刪除條件,就將其加入回收佇列,但不是立即銷燬物件,何時銷燬並釋放記憶體是無法預知的。垃圾回收不能強制執行,然而java提供了一些方法(如:System.gc()方法),允許你請求JVM執行垃圾回收,而不是要求,虛擬機器會盡其所能滿足請求,但是不能保證JVM從記憶體中刪除所有不用的物件。

問題六:一個java程式能夠耗盡記憶體嗎?
可以。垃圾收集系統嘗試在物件不被使用時把他們從記憶體中刪除。然而,如果保持太多活的物件,系統則可能會耗盡記憶體。垃圾回收器不能保證有足夠的記憶體,只能保證可用記憶體儘可能的得到高效的管理。

問題七:如何顯示的使物件符合垃圾回收條件?
(1)空引用:當物件沒有對他可到達引用時,他就符合垃圾回收的條件。也就是說如果沒有對他的引用,刪除物件的引用就可以達到目的,因此我們可以把引用變數設定為null,來符合垃圾回收的條件。


  1. StringBuffersb=newStringBuffer("hello");
  2. System.out.println(sb);
  3. sb=null;

(2)重新為引用變數賦值:可以通過設定引用變數引用另一個物件來解除該引用變數與一個物件間的引用關係。
StringBuffer sb1 =newStringBuffer(“hello”);
StringBuffer sb2 =newStringBuffer(“goodbye”);
System.out.println(sb1);
sb1=sb2;//此時”hello”符合回收條件
(3)方法內建立的物件:所建立的區域性變數僅在該方法的作用期間記憶體在。一旦該方法返回,在這個方法內建立的物件就符合垃圾收集條件。有一種明顯的例外情況,就是方法的返回物件。


  1. publicstaticvoidmain(String[]args){
  2. Dated=getDate();
  3. System.out.println("d="+d);
  4. }
  5. privatestaticDategetDate(){
  6. Dated2=newDate();
  7. StringBuffernow=newStringBuffer(d2.toString());
  8. System.out.println(now);
  9. returnd2;
  10. }

(4)隔離引用:這種情況中,被回收的物件仍具有引用,這種情況稱作隔離島。若存在這兩個例項,他們互相引用,並且這兩個物件的所有其他引用都刪除,其他任何執行緒無法訪問這兩個物件中的任意一個。也可以符合垃圾回收條件。


  1. publicclassIsland{
  2. Islandi;
  3. publicstaticvoidmain(String[]args){
  4. Islandi2=newIsland();
  5. Islandi3=newIsland();
  6. Islandi4=newIsland();
  7. i2.i=i3;
  8. i3.i=i4;
  9. i4.i=i2;
  10. i2=null;
  11. i3=null;
  12. i4=null;
  13. }
  14. }

問題八:垃圾收集前進行清理——finalize()方法
java提供了一種機制,使你能夠在物件剛要被垃圾回收之前執行一些程式碼。這段程式碼位於名為finalize()的方法內,所有類從Object類繼承這個方法。由於不能保證垃圾回收器會刪除某個物件。因此放在finalize()中的程式碼無法保證執行。因此建議不要重寫finalize();

如何讓程式變得更加健壯

1、儘早釋放無用物件的引用。
好的辦法是使用臨時變數的時候,讓引用變數在退出活動域後,自動設定為null,暗示垃圾收集器來收集該物件,防止發生記憶體洩露。對於仍然有指標指向的例項,jvm就不會回收該資源,因為垃圾回收會將值為null的物件作為垃圾,提高GC回收機制效率;

2、定義字串應該儘量使用String str=”hello”;的形式,避免使用String str = new String(“hello”);的形式。因為要使用內容相同的字串,不必每次都new一個String。

3、我們的程式裡不可避免大量使用字串處理,避免使用String,應大量使用StringBuffer,因為String被設計成不可變(immutable)類,所以它的所有物件都是不可變物件,

4、儘量少用靜態變數,因為靜態變數是全域性的,GC不會回收的;

5、儘量避免在類的建構函式裡建立、初始化大量的物件,防止在呼叫其自身類的構造器時造成不必要的記憶體資源浪費,尤其是大物件,JVM會突然需要大量記憶體,這時必然會觸發GC優化系統記憶體環境;顯示的宣告陣列空間,而且申請數量還極大。

6、儘量在合適的場景下使用物件池技術以提高系統性能,縮減縮減開銷,但是要注意物件池的尺寸不宜過大,及時清除無效物件釋放記憶體資源,綜合考慮應用執行環境的記憶體資源限制,避免過高估計執行環境所提供記憶體資源的數量。

7、大集合物件擁有大資料量的業務物件的時候,可以考慮分塊進行處理,然後解決一塊釋放一塊的策略。

8、不要在經常呼叫的方法中建立物件,尤其是忌諱在迴圈中建立物件。可以適當的使用hashtable,vector建立一組物件容器,然後從容器中去取那些物件,而不用每次new之後又丟棄。

9、一般都是發生在開啟大型檔案或跟資料庫一次拿了太多的資料,造成Out Of Memory Error的狀況,這時就大概要計算一下資料量的最大值是多少,並且設定所需最小及最大的記憶體空間值。

10、儘量少用finalize函式,因為finalize()會加大GC的工作量,而GC相當於耗費系統的計算能力。

11、不要過濫使用雜湊表,有一定開發經驗的開發人員經常會使用hash表(hash表在JDK中的一個實現就是HashMap)來快取一些資料,從而提高系統的執行速度。比如使用HashMap快取一些物料資訊、人員資訊等基礎資料,這在提高系統速度的同時也加大了系統的記憶體佔用,特別是當快取的資料比較多的時候。其實我們可以使用作業系統中的快取的概念來解決這個問題,也就是給被快取的分配一個一定大小的快取容器,按照一定的演算法淘汰不需要繼續快取的物件,這樣一方面會因為進行了物件快取而提高了系統的執行效率,同時由於快取容器不是無限制擴大,從而也減少了系統的記憶體佔用。現在有很多開源的快取實現專案,比如ehcache、oscache等,這些專案都實現了FIFO、MRU等常見的快取演算法。