要理解java對(duì)象的生命周期,我們需要要明白兩個(gè)問題,
1、java是怎么分配內(nèi)存的 ,2、java是怎么回收內(nèi)存的。
喜歡java的人,往往因?yàn)樗膬?nèi)存自動(dòng)管理機(jī)制,不喜歡java的人,往往也是因?yàn)樗膬?nèi)存自動(dòng)管理。我屬于前者,這幾年的coding經(jīng)驗(yàn)讓我認(rèn)識(shí)到,要寫好java程序,理解java的內(nèi)存管理機(jī)制是多么的重要。任何語言,內(nèi)存管理無外乎分配和回收,在C中我們可以用malloc動(dòng)態(tài)申請(qǐng)內(nèi)存,調(diào)用free釋放申請(qǐng)的內(nèi)存;在C++中,我們可以用new操作符在堆中動(dòng)態(tài)申請(qǐng)內(nèi)存,編寫析構(gòu)函數(shù)調(diào)用delete釋放申請(qǐng)的內(nèi)存;那么在java中究竟是內(nèi)存怎樣管理的呢?要弄清這個(gè)問題,我們首先要了解java內(nèi)存的分配機(jī)制,在java虛擬機(jī)規(guī)范里,JVM被分為7個(gè)內(nèi)存區(qū)域,但是規(guī)范這畢竟只是規(guī)范,就像我們編寫的接口一樣,雖然最終行為一致,但是個(gè)人的實(shí)現(xiàn)可能千差萬別,各個(gè)廠商的JVM實(shí)現(xiàn)也不盡相同,在這里,我們只針對(duì)sun的Hotspot虛擬機(jī)討論,該虛擬機(jī)也是目前應(yīng)用最廣泛的虛擬機(jī)。
虛擬器規(guī)范中的7個(gè)內(nèi)存區(qū)域分別是三個(gè)線程私有的和四個(gè)線程共享的內(nèi)存區(qū),線程私有的內(nèi)存區(qū)域與線程具有相同的生命周期,它們分別是: 指令計(jì)數(shù)器、 線程棧和本地線程棧,四個(gè)共享區(qū)是所有線程共享的,在JVM啟動(dòng)時(shí)就會(huì)分配,分別是:方法區(qū)、 常量池、直接內(nèi)存區(qū)和堆(即我們通常所說的JVM的內(nèi)存分為堆和棧中的堆,后者就是前面的線程棧)。接下來我們逐一了解這幾個(gè)內(nèi)存區(qū)域。
1 指令計(jì)數(shù)器。我們都知道java的多線程是通過JVM切換時(shí)間片運(yùn)行的,因此每個(gè)線程在某個(gè)時(shí)刻可能在運(yùn)行也可能被掛起,那么當(dāng)線程掛起之后,JVM再次調(diào)度它時(shí)怎么知道該線程要運(yùn)行那條字節(jié)碼指令呢?這就需要一個(gè)與該線程相關(guān)的內(nèi)存區(qū)域記錄該線程下一條指令,而指令計(jì)數(shù)器就是實(shí)現(xiàn)這種功能的內(nèi)存區(qū)域。有多少線程在編譯時(shí)是不確定的,因此該區(qū)域也沒有辦法在編譯時(shí)分配,只能在創(chuàng)建線程時(shí)分配,所以說該區(qū)域是線程私有的,該區(qū)域只是指令的計(jì)數(shù),占用的空間非常少,所以虛擬機(jī)規(guī)范中沒有為該區(qū)域規(guī)定OutofMemoryError。
2 線程棧。先讓我看以下一段代碼:
- class Test{
- public static void main(String[] args) {
- Thread th = new Thread();
- th.start();
- }
- }
在運(yùn)行以上代碼時(shí),JVM將分配一塊棧空間給線程th,用于保存方法內(nèi)的局部變量,方法的入口和出口等,這些局部變量包括基本類型和對(duì)象引用類型,這里可能有人會(huì)問,java的對(duì)象引用不是分配在堆上嗎?有這樣疑惑的人,可能是沒有理解java中引用和對(duì)象之間的區(qū)別,當(dāng)我們寫出以下代碼時(shí):
- public Object test(){
- Object obj = new Object();
- return obj;
- }
其中的Object obj就是我們所說的引用類型,這樣的聲明本身是要占用4個(gè)字節(jié),而這4個(gè)字節(jié)在這里就是在棧空間里分配的,準(zhǔn)確的說是在線程棧中為test方法分配的棧幀中分配的,當(dāng)方法退出時(shí),將會(huì)隨棧幀的彈出而自動(dòng)銷毀,而new Object()則是在堆中分配的,由GC在適當(dāng)?shù)臅r(shí)間收回其占用的空間。每個(gè)棧空間的默認(rèn)大小為0.5M,在1.7里調(diào)整為1M,每調(diào)用一次方法就會(huì)壓入一個(gè)棧幀,如果壓入的棧幀深度過大,即方法調(diào)用層次過深,就會(huì)拋出StackOverFlow,,SOF最常見的場(chǎng)景就是遞歸中,當(dāng)遞歸沒辦法退出時(shí),就會(huì)拋此異常,Hotspot提供了參數(shù)設(shè)置改區(qū)域的大小,使用-Xss:xxK,就可以修改默認(rèn)大小。 3 本地線程棧.顧名思義,該區(qū)域主要是給調(diào)用本地方法的線程分配的,該區(qū)域和線程棧的最大區(qū)別就是,在該線程的申請(qǐng)的內(nèi)存不受GC管理,需要調(diào)用者自己管理,JDK中的Math類的大部分方法都是本地方法,一個(gè)值得注意的問題是,在執(zhí)行本地方法時(shí),并不是運(yùn)行字節(jié)碼,所以之前所說的指令計(jì)數(shù)器是沒法記錄下一條字節(jié)碼指令的,當(dāng)執(zhí)行本地方法時(shí),指令計(jì)數(shù)器置為undefined。 接下來是四個(gè)線程共享區(qū)。 1 方法區(qū)。這塊區(qū)域是用來存放JVM裝載的class的類信息,包括:類的方法、靜態(tài)變量、類型信息(接口/父類),我們使用反射技術(shù)時(shí),所需的信息就是從這里獲取的。 2 常量池。當(dāng)我們編寫如下的代碼時(shí):
- class Test1{
- private final int size=50;
- }
這個(gè)程序中size因?yàn)橛胒inal修飾,不能再修改它的值,所以就成為常量,而這常量將會(huì)存放在常量區(qū),這些常量在編譯時(shí)就知道占用空間的大小,但并不是說明該區(qū)域編譯就固定了,運(yùn)行期也可以修改常量池的大小,典型的場(chǎng)景是在使用String時(shí),你可以調(diào)用String的 intern(),JVM會(huì)判斷當(dāng)前所創(chuàng)建的String對(duì)象是否在常量池中,若有,則從常量區(qū)取,否則把該字符放入常量池并返回,這時(shí)就會(huì)修改常量池的大小,比如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中并沒有單獨(dú)為該區(qū)域分配,而是合并到方法區(qū)中。 3 直接內(nèi)存區(qū)。直接內(nèi)存區(qū)并不是JVM可管理的內(nèi)存區(qū)。在JDK1.4中提供的NIO中,實(shí)現(xiàn)了高效的R/W操作,這種高效的R/W操作就是通過管道機(jī)制實(shí)現(xiàn)的,而管道機(jī)制實(shí)際上使用了本地內(nèi)存,這樣就避免了從本地源文件復(fù)制JVM內(nèi)存,再從JVM復(fù)制到目標(biāo)文件的過程,直接從源文件復(fù)制到目標(biāo)文件,JVM通過DirectByteBuffer操作直接內(nèi)存。 4 堆。主角總是最后出場(chǎng),堆絕對(duì)是JVM中的一等公民,絕對(duì)的主角,我們通常所說的GC主要就是在這塊區(qū)域中進(jìn)行的,所有的java對(duì)象都在這里分配,這也是JVM中最大的內(nèi)存區(qū)域,被所有線程共享,成千上萬的對(duì)象在這里創(chuàng)建,也在這里被銷毀。 java內(nèi)存分配到這就算是一個(gè)完結(jié)了,接下來我們將討論java內(nèi)存的回收機(jī)制, 內(nèi)存回收主要包含以下幾個(gè)方面理解: 第一,局部變量占用內(nèi)存的回收,所謂局部變量,就是指在方法內(nèi)創(chuàng)建的變量,其中變量又分為基本類型和引用類型。如下代碼:
- public void test(){
- int x=1;
- char y='a';
- long z=10L;
- }
變量x y z即為局部變量,占用的空間將在test()所在的線程棧中分配,test()執(zhí)行完了后會(huì)自動(dòng)從棧中彈出,釋放其占用的內(nèi)存,再來看一段代碼:
- public void test2(){
- Date d = new Date();
- System.out.println("Now is "+d);
- }
我們都知道上述代碼會(huì)創(chuàng)建兩個(gè)對(duì)象,一個(gè)是Date d另一個(gè)是new Date。Date d叫做聲明了一個(gè)date類型的引用,引用就是一種類型,和int x一樣,它表明了這種類型要占用多少空間,在java中引用類型和int類型一樣占用4字節(jié)的空間,如果只聲明引用而不賦值,這4個(gè)字節(jié)將指向JVM中地址為0的空間,表示未初始化,對(duì)它的任何操作都會(huì)引發(fā)空指針異常。 如果進(jìn)行賦值如d = new Date()那么這個(gè)d就保存了new Date()這個(gè)對(duì)象的地址,通過之前的內(nèi)存分配策略,我知道new Date()是在jvm的heap中分配的,其占用的空間的回收我們將在后面著重分析,這里我們要知道的是這個(gè)Date d所占用的空間是在test2()所在的線程棧分配的,方法執(zhí)行完后同樣會(huì)被彈出棧,釋放其占用的空間。 第二.非局部變量的內(nèi)存回收,在上面的代碼中new Date()就和C++里的new創(chuàng)建的對(duì)象一樣,是在heap中分配,其占用的空間不會(huì)隨著方法的結(jié)束而自動(dòng)釋放需要一定的機(jī)制去刪除,在C++中必須由程序員在適當(dāng)時(shí)候delete掉,在java中這部分內(nèi)存是由GC自動(dòng)回收的,但是要進(jìn)行內(nèi)存回收必須解決兩問題:那些對(duì)象需要回收、怎么回收。判定那些對(duì)象需要回收,我們熟知的有以下方法: 一,引用計(jì)數(shù)法,這應(yīng)是絕大數(shù)的的java 程序員聽說的方法了,也是很多書上甚至很多老師講的方法,該方法是這樣描述的,為每個(gè)對(duì)象維護(hù)一個(gè)引用計(jì)數(shù)器,當(dāng)有引用時(shí)就加1,引用解除時(shí)就減1,那些長時(shí)間引用為0的對(duì)象就判定為回收對(duì)象,理論上這樣的判定是最準(zhǔn)確的,判定的效率也高,但是卻有一個(gè)致命的缺陷,請(qǐng)看以下代碼:
- package tmp;
- 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);
- }
- }
我們用以下參數(shù)運(yùn)行:-Xmx10M -Xms10M M 將jvm的大小設(shè)置為10M,不允許擴(kuò)展,按引用計(jì)數(shù)法,t1和t2相互引用,他們的引用計(jì)數(shù)都不可能為0,那么他們將永遠(yuǎn)不會(huì)回收,在我們的環(huán)境中JVM共10M,t1 t2占用8m,那么剩下的2M,是不足以創(chuàng)建t3的,理論上應(yīng)該拋出OOM。但是,程序正常運(yùn)行了,這說明JVM應(yīng)該是回收了t1和t2的我們加上-XX:+PrintGCDetails運(yùn)行,將打印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]
[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,這就從側(cè)面證明jvm不是采用這種策略判定對(duì)象是否可以回收的。
二,根搜索算法,這是當(dāng)前的大部分虛擬機(jī)采用的判定策略,GC線程運(yùn)行時(shí),它會(huì)以一些特定的引用作為起點(diǎn)稱為GCRoot,從這些起點(diǎn)開始搜索,把所用與這些起點(diǎn)相關(guān)聯(lián)的對(duì)象標(biāo)記,形成幾條鏈路,掃描完時(shí),那些沒有與任何鏈路想連接的對(duì)象就會(huì)判定為可回收對(duì)象。具體那些引用作為起點(diǎn)呢,一種是類級(jí)別的引用:靜態(tài)變量引用、常量引用,另一種是方法內(nèi)的引用,如之前的test()方法中的Date d對(duì)new Date()的引用,在我們的測(cè)試代碼中,在創(chuàng)建t3時(shí),jvm發(fā)現(xiàn)當(dāng)前的空間不足以創(chuàng)建對(duì)象,會(huì)出發(fā)一次GC,雖然t1和t2相互引用,但是執(zhí)行t1=t2=null后,他們不和上面的3個(gè)根引用中的任何一個(gè)相連接,所以GC會(huì)判定他們是可回收對(duì)象,并在隨后將其回收,從而為t3的創(chuàng)建創(chuàng)造空間,當(dāng)進(jìn)行回收后發(fā)現(xiàn)空間還是不夠時(shí),就會(huì)拋出OOM。
接下來我們就該討論GC 是怎么回收的了,目前版本的Hotspot虛擬機(jī)采用分代回收算法,它把heap分為新生代和老年代兩塊區(qū)域,如下圖:
默認(rèn)的配置中老年代占90% 新生代占10%,其中新生代又被分為一個(gè)eden區(qū)和兩個(gè)survivor區(qū),每次使用eden和其中的一個(gè)survivor區(qū),一般對(duì)象都在eden和其中的一個(gè)survivor區(qū)分配,但是那些占用空間較大的對(duì)象,就會(huì)直接在老年代分配,比如我們?cè)谶M(jìn)行文件操作時(shí)設(shè)置的緩沖區(qū),如byte[] buffer = new byte[1024*1024],這樣的對(duì)象如果在新生代分配將會(huì)導(dǎo)致新生代的內(nèi)存不足而頻繁的gc,GC運(yùn)行時(shí)首先會(huì)進(jìn)行會(huì)在新生代進(jìn)行,會(huì)把那些標(biāo)記還在引用的對(duì)象復(fù)制到另一塊survivor空間中,然后把整個(gè)eden區(qū)和另一個(gè)survivor區(qū)里所有的對(duì)象進(jìn)行清除,但也并不是立即清除,如果這些對(duì)象重寫了finalize方法,那么GC會(huì)把這些對(duì)象先復(fù)制到一個(gè)隊(duì)列里,以一個(gè)低級(jí)別的線程去觸發(fā)finalize方法,然后回收該對(duì)象,而那些沒有覆寫finalize方法的對(duì)象,將會(huì)直接被回收。在復(fù)制存活對(duì)象到另一個(gè)survivor空間的過程中可能會(huì)出現(xiàn)空間不足的情況,在這種情況下GC回直接把這些存活對(duì)象復(fù)制到老年代中,如果老年代的空間也不夠時(shí),將會(huì)觸發(fā)一次Full GC,Full gc會(huì)回收老年代中那些沒有和任何GC Root相連的對(duì)象,如果Full GC后發(fā)現(xiàn)內(nèi)存還是不足,將會(huì)出現(xiàn)OutofMemoryError。
Hotspot虛擬機(jī)下java對(duì)象內(nèi)存的分配和回收到此就算完結(jié)了