注:本篇文章中的內容是根據《深刻理解Java虛擬機--JVM高級特性與最佳實踐》而總結的,若有理解錯誤,歡迎你們指正!java
java與c++之間有一堵由內存動態分配和垃圾回收技術所圍成的「高牆」,牆外的人想進去,牆裏面的人卻想出去。c++
1.1 java虛擬機運行時的數據區
- 程序計數器
它是一塊較小的內存空間,能夠把它看做是當前線程所執行的字節碼行號指示器,線程私有。
- 虛擬機棧
每一個方法在執行的同時,都會建立一個棧幀,用於存放局部變量表,操做數表,方法出口等信息,每一個方法從調用直到執行完成的過程,就對應着一個棧幀在虛擬機棧中的入棧和出棧的過程,線程私有。
- 本地方法棧
相似於虛擬機棧,本地方法是這樣的一個方法:該方法的實現由非java語言實現。
- java堆
java堆是虛擬機所管理的內存中最大的一塊,是全部線程共享的。它的惟一目的就是存放對象實例。java堆是垃圾收集器管理的主要區域。(新生代和老年代)
- 方法區
線程共享,它用於存儲已經被虛擬機加載的類信息、常量、靜態變量等。它被稱爲永久代,自JDK1.8以後,永久代被移除,取而代之的是「元空間」。元空間和永久代之間的差異在於:元空間並不在虛擬機中,而是在本地內存中,所以,在默認狀況下,元空間的大小僅受本地內存限制。
1.2 垃圾回收
1.2.1 對象是否已死?
在堆裏面存放着java世界中幾乎全部的對象實例,垃圾回收器在對堆進行回收以前,第一件事情就是要肯定這些對象之中哪些還存活着,哪些已經死去。判斷對象是否存活的算法主要有如下兩種:算法
- 引用計數算法
給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器就減1;當計數器爲0時,表示它沒有被任何地方引用,能夠被回收了。算法實現簡單,效率也很高,但沒法解決對象之間相互引用而形成的內存泄漏。
- 可達性分析算法
該算法的基本思想是:經過一系列的稱爲「GC Roots」的對象做爲起點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連時,則證實此對象是不可用的。在Java語言中,可做爲GC Roots的對象包括:虛擬機棧中引用的對象;方法區中類靜態屬性引用的對象;方法區中常量引用的對象;本地方法棧中引用的對象。
1.2.2 最後的自我拯救
即便在可達性分析算法中不可達的對象,也並不是是非死不可,這個時候它們暫處於緩刑階段。finalize()方法是對象逃脫死亡命運的最後一次機會,只須要重寫該方法,並在方法中從新將此對象與其餘引用鏈上的對象創建關聯關係便可。要真正宣告一個對象死亡,至少要經歷兩次標記過程:若是對象不可達,那它將會被第一次標記並進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法,當對象沒有重寫finalize()方法或者虛擬機已經調用了它的finalize()方法,則被視爲沒有必要執行。若是這個對象被斷定有必要執行finalize()方法,那麼這個對象將被放進一個叫作F-Queue的隊列之中,稍後GC將對F-Queue中的對象進行第二次標記,若是此時對象經過finalize()方法拯救了本身,那在第二次標記時此對象將被移除即將回收的集合,不然,它將被做爲垃圾進行回收。安全
1.2.3 垃圾回收算法
- 標記-清除算法(Mark-Sweep)
算法分爲標記和清除兩個階段,首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。它有兩個不足之處:一是標記和清除兩個過程的效率都不高;二是容易產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大對象時,沒法找到足夠的連續內存空間而不得不提早觸發一次垃圾回收動做。
- 複製算法
它將內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊內存使用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。缺點:一是內存被縮小到了原來的一半,二時是在對象存活率較高時就要進行較多的複製操做,效率將會變低。
如今的商業虛擬機都採用複製算法來回收年輕代,根據IBM公司的專門研究代表,年輕代中的對象98%是「朝生夕死」,因此並不須要按照1:1的比例來劃份內存空間,而是將內存分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。但回收時,將Eden和Survivor中還存活的對象一次性複製到另一塊Survivor中,最後清理掉Eden和剛纔用過的Survivor空間。HotSpot虛擬機默認Eden和Survivor的大小比例時8:1,也就是每次新生代中可用內存空間爲整個新生代容量的90%,只有10%的內存會被浪費掉。
- 標記-整理算法
標記過程仍然與「標記-清理」算法同樣,但後續步驟不是直接對可回收的對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。
- 分代收集算法
根據對象存活週期的不一樣將內存劃分爲幾塊,通常是把Java堆分爲年輕代和老年代,這樣就能夠根據各個年代的特色採用最適當的收集算法。在年輕代中,每次垃圾收集時都會發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。而老年代中由於對象的存活率高,沒有額外空間對它進行分配擔保,就必須使用標記-清理或者標記-整理算法來進行回收。
1.2.4 Minor GC 、 Major GC 與 Full GC
- Minor GC(年輕代 GC):指發生在年輕代的垃圾回收動做,由於Java對象大多都具有朝生夕死的特性,因此Minor GC很是頻繁,通常回收速度也比較快。
- Major GC(老年代GC):指發生在老年代的GC,出現了Major GC,常常會伴隨至少一次的Minor GC。Major GC的速度通常會比Minor GC慢10倍以上。
- Full GC: 清理整個堆空間,回收年輕代和老年代。一般Major GC和Full GC是等價的。
1.3 內存分配策略
- 對象優先在Eden區分配
大多數狀況下,對象在年輕代Eden區分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。
- 大對象直接進入老年代
所謂大對象,是指須要大量連續內存空間的Java對象。常常出現大對象容易致使內存還有很多空間時就提早觸發垃圾收集以獲取足夠的連續空間來安置它們。虛擬機提供一個-XX:pretenureSizeThreshold參數,令大於這個值的對象直接在老年代分配,這樣的目的是避免在Eden區和兩個Survivor區之間發生大量的內存複製。
- 長期存活的對象將進入老年代
既然虛擬機採用了分代收集的思想來管理內存,那麼內存回收時就必須能識別出哪些對象應該放在年輕代,哪些對象應該放在老年代中。爲了作到這一點,虛擬機給每一個對象定義了一個對象年齡(Age)計數器。若是對象在Eden區出生,並通過一個Minor GC後仍然存活,並且可以被Survivor容納的話,該對象將被移動到Survivor空間中,且對象的年齡設置爲1。對象在Survivor區中每熬過一次Minor GC,年齡就增長1歲,當年齡增長到必定程度(默認爲15歲),就會被晉升到老年代中。對象晉升老年代的年齡閾值,能夠經過參數-XX:MaxTenuringThreshold設置。
- 動態對象年齡斷定
虛擬機並非永遠要求對象的年齡必須大於MaxTenuringThreshold纔可以晉升爲老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或者等於該年齡的對象就能夠直接進入老年代中。
- 空間分配擔保在發生Minor GC以前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是這個條件成立,那麼Minor GC能夠確保是安全的。若是不成立,則虛擬機會查看HandlePromotionFailure設置值是否容許擔保失敗,若是容許,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代對象的平均大小,若是大於,將嘗試着進行一次Minor GC,若是小於,或者HandlePromotionFailur設置爲不容許冒險,那這時要改成進行一次Full GC.