【Java對象生命週期】Java對象的生命週期:java是怎麼分配內存的和怎麼回收的?

  http://www.uml.org.cn/j2ee/201301183.aspjava

要理解java對象的生命週期,咱們須要要明白兩個問題,程序員

  一、java是怎麼分配內存的 ,二、java是怎麼回收內存的。算法

  喜歡java的人,每每由於它的內存自動管理機制,不喜歡java的人,每每也是由於它的內存自動管理。我屬於前者,這幾年的coding經驗讓我認識到,要寫好java程序,理解java的內存管理機制是多麼的重要。任何語言,內存管理無外乎分配和回收,在C中咱們能夠用malloc動態申請內存,調用free釋放申請的內存;在C++中,咱們能夠用new操做符在堆中動態申請內存,編寫析構函數調用delete釋放申請的內存;那麼在java中到底是內存怎樣管理的呢?要弄清這個問題,咱們首先要了解java內存的分配機制,在java虛擬機規範裏,JVM被分爲7個內存區域,可是規範這畢竟只是規範,就像咱們編寫的接口同樣,雖然最終行爲一致,可是我的的實現可能千差萬別,各個廠商的JVM實現也不盡相同,在這裏,咱們只針對sun的Hotspot虛擬機討論,該虛擬機也是目前應用最普遍的虛擬機。多線程

  虛擬器規範中的7個內存區域分別是三個線程私有的和四個線程共享的內存區,線程私有的內存區域與線程具備相同的生命週期,它們分別是: 指令計數器、 線程棧和本地線程棧,四個共享區是全部線程共享的,在JVM啓動時就會分配,分別是:方法區、 常量池、直接內存區和堆(即咱們一般所說的JVM的內存分爲堆和棧中的堆,後者就是前面的線程棧)。接下來咱們逐一瞭解這幾個內存區域。jvm

  1 指令計數器。咱們都知道java的多線程是經過JVM切換時間片運行的,所以每一個線程在某個時刻可能在運行也可能被掛起,那麼當線程掛起以後,JVM再次調度它時怎麼知道該線程要運行那條字節碼指令呢?這就須要一個與該線程相關的內存區域記錄該線程下一條指令,而指令計數器就是實現這種功能的內存區域。有多少線程在編譯時是不肯定的,所以該區域也沒有辦法在編譯時分配,只能在建立線程時分配,因此說該區域是線程私有的,該區域只是指令的計數,佔用的空間很是少,因此虛擬機規範中沒有爲該區域規定OutofMemoryError。函數

  二、線程棧。先讓我看如下一段代碼:測試

  class Test{this

    public static void main(String[] args) {spa

       Thread th = new Thread();線程

       th.start();

    }

  }

  在運行以上代碼時,JVM將分配一塊棧空間給線程th,用於保存方法內的局部變量,方法的入口和出口等,這些局部變量包括基本類型和對象引用類型,這裏可能有人會問,java的對象引用不是分配在堆上嗎?有這樣疑惑的人,多是沒有理解java中引用和對象以前的區別,當咱們寫出如下代碼時:

  public Object test()

  {

     Object obj = new Object();

     return obj;

  }

  其中的Object obj就是咱們所說的引用類型,這樣的聲明自己是要佔用4個字節,而這4個字節在這裏就是在棧空間裏分配的,準確的說是在線程棧中爲test方法分配的棧幀中分配的,當方法退出時,將會隨棧幀的彈出而自動銷燬,而new Object()則是在堆中分配的,由GC在適當的時間收回其佔用的空間。每一個棧空間的默認大小爲0.5M,在1.7裏調整爲1M,每調用一次方法就會壓入一個棧幀,若是壓入的棧幀深度過大,即方法調用層次過深,就會拋出StackOverFlow,,SOF最多見的場景就是遞歸中,當遞歸沒辦法退出時,就會拋此異常,Hotspot提供了參數設置改區域的大小,使用-Xss:xxK,就能夠修改默認大小。

  三、本地線程棧。顧名思義,該區域主要是給調用本地方法的線程分配的,該區域和線程棧的最大區別就是,在該線程的申請的內存不受GC管理,須要調用者本身管理,JDK中的Math類的大部分方法都是本地方法,一個值得注意的問題是,在執行本地方法時,並非運行字節碼,因此以前所說的指令計數器是無法記錄下一條字節碼指令的,當執行本地方法時,指令計數器置爲undefined。

  接下來是四個線程共享區。

  一、方法區。這塊區域是用來存放JVM裝載的class的類信息,包括:類的方法、靜態變量、類型信息(接口/父類),咱們使用反射技術時,所需的信息就是從這裏獲取的。

  二、常量池。當咱們編寫以下的代碼時:

  class Test1{

     private final int size=50;

  }

  這個程序中size由於用final修飾,不能再修改它的值,因此就成爲常量,而這常量將會存放在常量區,這些常量在編譯時就知道佔用空間的大小,但並非說明該區域編譯就固定了,運行期也能夠修改常量池的大小,典型的場景是在使用String時,你能夠調用String的 intern(),JVM會判斷當前所建立的String對象是否在常量池中,如有,則從常量區取,不然把該字符放入常量池並返回,這時就會修改常量池的大小,好比JDK中java.io.ObjectStreamField的一段代碼:

  ....

  ObjectStreamField(Field field, boolean unshared, boolean showType) {

     this.field = field;

     this.unshared = unshared;

     name = field.getName();

     Class ftype = field.getType();

     type = (showType || ftype.isPrimitive()) ? ftype : Object.class;

     signature = ObjectStreamClass.getClassSignature(ftype).intern();

  }

  這段代碼將獲取的類的簽名放入常量池。HotSpot中並無單獨爲該區域分配,而是合併到方法區中。

  三、直接內存區。直接內存區並非JVM可管理的內存區。在JDK1.4中提供的NIO中,實現了高效的R/W操做,這種高效的R/W操做就是經過管道機制實現的,而管道機制實際上使用了本地內存,這樣就避免了從本地源文件複製JVM內存,再從JVM複製到目標文件的過程,直接從源文件複製到目標文件,JVM經過DirectByteBuffer操做直接內存。

  四、堆。主角老是最後出場,堆絕對是JVM中的一等公民,絕對的主角,咱們一般所說的GC主要就是在這塊區域中進行的,全部的java對象都在這裏分配,這也是JVM中最大的內存區域,被全部線程共享,成千上萬的對象在這裏建立,也在這裏被銷燬。

  java內存分配到這就算是一個完結了,接下來咱們將討論java內存的回收機制,內存回收主要包含如下幾個方面理解:

  第一,局部變量佔用內存的回收,所謂局部變量,就是指在方法內建立的變量,其中變量又分爲基本類型和引用類型。以下代碼:

  ...

  public void test()

  {

     int x=1;

     char y='a';

     long z=10L;

  }

  變量x y z即爲局部變量,佔用的空間將在test()所在的線程棧中分配,test()執行完了後會自動從棧中彈出,釋放其佔用的內存,再來看一段代碼:

  ....

  public void test2()

  {

     Date d = new Date();

     System.out.println("Now is "+d);

  }

  咱們都知道上述代碼會建立兩個對象,一個是Date d另外一個是new Date。Date d叫作聲明瞭一個date類型的引用,引用就是一種類型,和int x同樣,它代表了這種類型要佔用多少空間,在java中引用類型和int類型同樣佔用4字節的空間,若是隻聲明引用而不賦值,這4個字節將指向JVM中地址爲0的空間,表示未初始化,對它的任何操做都會引起空指針異常。

  若是進行賦值如d = new Date()那麼這個d就保存了new Date()這個對象的地址,經過以前的內存分配策略,我知道new Date()是在jvm的heap中分配的,其佔用的空間的回收咱們將在後面着重分析,這裏咱們要知道的是這個Date d所佔用的空間是在test2()所在的線程棧分配的,方法執行完後一樣會被彈出棧,釋放其佔用的空間。

  第二,非局部變量的內存回收,在上面的代碼中new Date()就和C++裏的new建立的對象同樣,是在heap中分配,其佔用的空間不會隨着方法的結束而自動釋放須要必定的機制去刪除,在C++中必須由程序員在適當時候delete掉,在java中這部份內存是由GC自動回收的,可是要進行內存回收必須解決兩問題:那些對象須要回收、怎麼回收。斷定那些對象須要回收,咱們熟知的有如下方法:

  一,引用計數法,這應是絕大數的的java 程序員據說的方法了,也是不少書上甚至不少老師講的方法,該方法是這樣描述的,爲每一個對象維護一個引用計數器,當有引用時就加1,引用解除時就減1,那些長時間引用爲0的對象就斷定爲回收對象,理論上這樣的斷定是最準確的,斷定的效率也高,可是卻有一個致命的缺陷,請看如下代碼:

  package com.mail.czp;

 

  import java.util.ArrayList;

  import java.util.List;

 

  public class Test {

 

    private byte[] buffer;

    private List ls;

 

    public Test() {

      this.buffer = new byte[4*1024*1024];

      this.ls = new ArrayList();

    }

    private List getList() {

      return ls;

    }

 

    public static void main(String[] args) {

      Test t1 = new Test();

      Test t2 = new Test();

      t1.getList().add(t2);

      t2.getList().add(t1);

      t1 = t2 = null;

      Test t3 = new Test();

      System.out.println(t3);

    }

  }

  咱們用如下參數運行:-Xmx10M -Xms10M M 將jvm的大小設置爲10M,不容許擴展,按引用計數法,t1和t2相互引用,他們的引用計數都不可能爲0,那麼他們將永遠不會回收,在咱們的環境中JVM共10M,t1 t2佔用8m,那麼剩下的2M,是不足以建立t3的,理論上應該拋出OOM。可是,程序正常運行了,這說明JVM應該是回收了t1和t2的咱們加上-XX:+PrintGCDetails運行,將打印GC的回收日記:

  [GC [DefNew: 252K->64K(960K), 0.0030166 secs][Tenured: 8265K->137K(9216K), 0.0109869 secs] 8444K->137K(10176K), 

[Perm : 2051K->2051K(12288K)], 0.0140892 secs] [Times: user=0.01 sys=0.00, real=0.02 secs] 

  com.mail.czp.Test@2ce908

  Heap

   def new generation   total 960K, used 27K [0x029e0000, 0x02ae0000, 0x02ae0000)

    eden space 896K,   3% used [0x029e0000, 0x029e6c40, 0x02ac0000)

    from space 64K,   0% used [0x02ad0000, 0x02ad0000, 0x02ae0000)

    to   space 64K,   0% used [0x02ac0000, 0x02ac0000, 0x02ad0000)

   tenured generation   total 9216K, used 4233K [0x02ae0000, 0x033e0000, 0x033e0000)

     the space 9216K,  45% used [0x02ae0000, 0x02f02500, 0x02f02600, 0x033e0000)

   compacting perm gen  total 12288K, used 2077K [0x033e0000, 0x03fe0000, 0x073e0000)

     the space 12288K,  16% used [0x033e0000, 0x035e74d8, 0x035e7600, 0x03fe0000)

  No shared spaces configured.

  從打印的日誌咱們能夠看出,GC照常回收了t1 t2,這就從側面證實jvm不是採用這種策略斷定對象是否能夠回收的。

  二,根搜索算法,這是當前的大部分虛擬機採用的斷定策略,GC線程運行時,它會以一些特定的引用做爲起點稱爲GCRoot,從這些起點開始搜索,把所用與這些起點相關聯的對象標記,造成幾條鏈路,掃描完時,那些沒有與任何鏈路想鏈接的對象就會斷定爲可回收對象。具體那些引用做爲起點呢,一種是類級別的引用:靜態變量引用、常量引用,另外一種是方法內的引用,如以前的test()方法中的Date d對new Date()的引用,在咱們的測試代碼中,在建立t3時,jvm發現當前的空間不足以建立對象,會出發一次GC,雖然t1和t2相互引用,可是執行t1=t2=null後,他們不和上面的3個根引用中的任何一個相鏈接,因此GC會斷定他們是可回收對象,並在隨後將其回收,從而爲t3的建立創造空間,當進行回收後發現空間仍是不夠時,就會拋出OOM。

  接下來咱們就該討論GC 是怎麼回收的了,目前版本的Hotspot虛擬機採用分代回收算法,它把heap分爲新生代和老年代兩塊區域,以下圖:

  

  默認的配置中老年代佔90% 新生代佔10%,其中新生代又被分爲一個eden區和兩個survivor區,每次使用eden和其中的一個survivor區,通常對象都在eden和其中的一個survivor區分配,可是那些佔用空間較大的對象,就會直接在老年代分配,好比咱們在進行文件操做時設置的緩衝區,如byte[] buffer = new byte[1024*1024],這樣的對象若是在新生代分配將會致使新生代的內存不足而頻繁的gc,GC運行時首先會進行會在新生代進行,會把那些標記還在引用的對象複製到另外一塊survivor空間中,而後把整個eden區和另外一個survivor區裏全部的對象進行清除,但也並非當即清除,若是這些對象重寫了finalize方法,那麼GC會把這些對象先複製到一個隊列裏,以一個低級別的線程去觸發finalize方法,而後回收該對象,而那些沒有覆寫finalize方法的對象,將會直接被回收。在複製存活對象到另外一個survivor空間的過程當中可能會出現空間不足的狀況,在這種狀況下GC回直接把這些存活對象複製到老年代中,若是老年代的空間也不夠時,將會觸發一次Full GC,Full gc會回收老年代中那些沒有和任何GC Root相連的對象,若是Full GC後發現內存仍是不足,將會出現OutofMemoryError。

Hotspot虛擬機下java對象內存的分配和回收就算完結了,後續咱們將討論java代碼的重構。

相關文章
相關標籤/搜索