對於C、C++程序員來講,在內存管理領域,他們既擁有每個對象的「全部權」,又擔負着每個對象生命開始到終結的維護責任。html
對Java程序員來講,在虛擬機的自動內存管理機制的幫助下,再也不須要爲每一個new操做去寫匹對的 delete/free 代碼,不容易出現內存泄露和內存溢出的問題。java
根據《Java虛擬機規範(Java SE 7版)》規定,Java虛擬機所管理的內存將包括如下幾個運行時數據區域,如圖:程序員
線程私有的內存區域:web
全部線程共享的內存區域: 算法
在語言層面,建立對象(例如:clone,反序列化)一般是一個 new 關鍵字,而在虛擬機中,對象建立的過程是如何呢?數組
在虛擬機遇到 new 指令時:安全
1. 類加載:確保常量池中存放的是已解釋的類,且對象所屬類型已經初始化過,若是沒有,則先執行類加載數據結構
2. 爲新生對象分配內存:對象所需內存大小在類加載時能夠肯定,將肯定大小的內存從Java堆中劃分出來併發
3. 將分配的內存空間初始化爲零值:保證對象的實例在Java代碼中能夠不賦值就可直接使用,能訪問到這些字段的數據類型對應的零值(例如,int類型參數默認爲0)oracle
4. 設置對象頭:設置對象的類的元數據信息、哈希碼、GC分代年齡等
5. 執行<init>方法初始化:將對象按照程序員的意願初始化
在HotSpot虛擬機中,對象在內存中存儲的佈局分爲3個區域,以下圖所示:
除程序計數器外,JVM其餘幾個運行時區域均可能發生OutOfMemoryError異常。
1. 堆內存溢出,OutOfMemoryError:java heap space
緣由:Java堆用於存儲對象實例,只要不斷建立對象,並保證GC Roots到對象間有可達路徑避免這些對象的GC,當對象數量達到堆的最大容量限制後就會產生OOM
解決方法:
2. 棧內存溢出,StackOverflowError
緣由:
解決方法:
堆中存放着幾乎全部的對象實例,GC收集器在對堆進行回收前,首先要肯定哪些對象是「存活」的,哪些是「死去」的
1. 引用計數法
給每一個對象添加一個引用計數器,每當有地方引用它時,計數器 +1;引用失效時,計數器 -1。當計數器爲0時對象就再也不被引用。
但主流Java虛擬機沒有采用這種算法,主要緣由是:它難以解決對象之間循環引用的問題
2. 可達性分析算法
經過一系列稱爲「GC Roots」的對象做爲起始點,從這些節點向下搜索,搜索的路徑稱爲引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連(即從 GC Roots 到該對象不可達),則此對象是不可用的,會判斷爲可回收對象。
Java中,可做爲 GC Roots 的對象包括:
垃圾回收主要是回收堆內存。在堆中,新生代常規應用進行一次GC通常可回收 70%~95% 的空間,永久代的 GC效率遠低於此
方法區進行垃圾回收的「性價比」通常比較低,主要回收兩部份內容:廢棄常量和無用的類
堆外內存是把內存對象分配在Java虛擬機的堆之外的內存,包括JVM自身運行過程當中分配的內存,JNI 裏分配的內存、java.nio.DirectByteBuffer 分配的內存等,這些內存直接受操做系統管理。這樣能必定程度的減小GC對應用程序的影響。但 JVM 不直接管理這些堆外內存,存在 OOM 的風險,能夠在 JVM 啓動參數加上 -XX:MaxDirectMemorySize,對申請的堆外內存大小進行限制
DirectByteBuffer 對象表示堆外內存,DirectByteBuffer 對象中持有 Cleaner 對象,它惟一保存了堆外內存的數據、開始地址、大小和容量。在建立完後的下一次 Full GC 時, Cleaner對象會對堆外內存回收
① 標記-清除算法
標記-清除算法分爲「標記」階段和「清除」階段。標記階段是把全部活動對象都作上標記。清除階段是把那些沒有標記的對象(非活動對象)回收
它主要有兩個不足:
② 複製算法
複製算法是將可用內存劃分爲大小相等的兩塊,每次只使用一塊,當一塊內存用完了,就將存活的對象複製到另外一塊上,而後將已使用的內存空間一次清理掉。
這樣分配內存時不用考慮內存碎片等複雜狀況,但代價是將內存縮小爲原來的一半。當對象存活率較高時,就要較多的複製操做,效率也會下降。
如今的商業虛擬機都採用複製算法來回收新生代。IBM專門的研究代表:新生代中對象 98% 是「朝生夕死」的,全部不須要 1:1 來劃分空間,HotSpot虛擬機是將內存分爲1塊大的 Eden 和 2塊小的 Survivor 空間,大小比例爲 8:1:1。每次使用 Eden 和 其中一塊 Survivor。當回收時,將 Eden 和 一塊 Survivor 中的存活對象複製到另外一塊 Survivor 上,最後清理掉剛纔的 Eden 和 Survivor。新生代每次可利用的整個新生態內存的 90%,10% 會被浪費掉。但當每次回收多餘 10% 對象存活時,即剩下一個 Survivor 空間不夠時,須要老年代內存擔保,這些對象將直接進入老年代中。
③ 標記-整理算法
標記-整理算法在「標記」階段和標記-清除同樣,但後續是讓全部存活對象都向一端移動,而後清理掉端邊界外的內存
④ 分代算法
根據對象存活週期的不一樣將內存劃分爲幾塊看,通常把堆分爲「年輕代」和「老年代」,根據各個年代的特色採用適當的收集算法。
新生態中,每次 GC 只有少許的對象存活,就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集
老年代中,對象存活率高、沒有額外的擔保空間,就必須使用「標記-清除」或「標記-整理」算法
垃圾回收算法性能:
高吞吐量和低暫停時間不可兼得。爲了得到最大吞吐量,JVM 必須儘量少地運行 GC,只有在無可奈何的狀況下才運行GC,好比:新生代或者老年代已經滿了。可是推遲運行 GC 的結果是,每次運行 GC 時須要作的事情會不少,好比有更多的對象積累在堆上等待回收,所以每次的 GC 時間則會變高,由此引發的平均和最大暫停時間也會很高
垃圾收集器是內存回收算法的具體實現。本文主要介紹 HotSpot 虛擬機中的垃圾回收器,如圖所示:
若是兩個收集器之間存在連線,說明他們可搭配使用。各垃圾回收器的功能比較以下表:
該選用哪種垃圾回收器?
1. 客戶端程序: 通常使用 -XX:+UseSerialGC (Serial + Serial Old). 特別注意, 當一臺機器上起多個 JVM, 每一個 JVM 也能夠採用這種 GC 組合
2. 吞吐率優先的服務端程序(計算密集型): -XX:+UseParallelGC 或者 -XX:+UseParallelOldGC
3. 響應時間優先的服務端程序: -XX:+UseConcMarkSweepGC
4. 響應時間優先同時也要兼顧吞吐率的服務端程序:-XX:+UseG1GC
CMS(Concurrent Mark Sweep)垃圾收集器是以最短回收停頓時間爲目標的垃圾收集器。通常B/S或互聯網站的服務端比較重視響應速度,但願系統的停頓時間最短,從而帶給用戶更好的體驗,CMS就比較符合這類應用的需求。
① 執行過程
CMS是基於 "標記-清除" 算法實現的,由上文『複製GC算法』中所描述,新生代98%對象是朝生夕死的,因此將新生代分爲1個Eden和2個survivor區(默認內存大小是8:1:1),每次使用Eden和一個survivor區,回收時,將活着的對象拷貝到剩餘的一個survivor區,並清理以前使用的Eden和survivor區的空間。
它運做過程分爲如下幾個階段:
一、初始標記(須要 Stop The World):標記 GC Roots 能直接關聯到的對象,速度很快
二、併發標記(和用戶線程一塊兒工做):GC Roots Tracing的過程,例如:A是GC Root關聯到的對象,A引用B,A在初始階段標記出來,這個階段就是標記B對象
三、併發預清理(和用戶線程一塊兒工做):併發查找在併發標記階段,重新生代晉升到老年代的對象、或直接在老年代分配的大對象、或被用戶線程更新的對象,來減小 "從新標記" 階段的工做量
四、從新標記(須要 Stop The World):修正『併發標記』和『併發預清理』用戶線程與GC線程併發執行,用戶線程產生了新對象,將這些對象從新標記。這階段 STW 時間會比『初始標記』階段長一些,但遠比『併發標記』的時間短。暫停用戶線程,GC線程從新掃描堆中的對象,進行可達性分析,標記活着的對象
五、併發清理(和用戶線程一塊兒工做):移除不用的對象,回收他們佔用的堆空間。此時會產生新的垃圾,在這次GC沒法清除,只好等到下次清理,這些垃圾名爲:浮動垃圾
六、併發重置:從新設置 CMS 內部的數據結構,準備下一次 CMS 生命週期的使用
併發標記階段修改了對象如何處理?
上述 CMS GC過程當中第3個步驟:併發預清理,如何處理併發標記階段被修改的對象呢?初始標記階段的引用爲 A → B → C,併發標記時,引用關係由用戶程序改成 A → C,B再也不引用C ,因爲C在 "併發標記" 階段沒法被標記,就會被回收,而這是不容許的。
這能夠經過三色標記法解決,將GC中的對象分爲三種狀況:
初始標記時,A 會被標記爲灰色(正在掃描 A 相關),而後掃描 A 的引用,將 B 標記爲灰色,而後 A 就掃描完成了,變爲黑色
併發標記時,若是用戶線程將 A 引用改成了 C,即 A → C,此時 CMS 在寫屏障(Write Barrier)裏發現有一個白色對象的引用(C)被賦值到黑色對象(A)的字段裏,那就會將 C 這個白色對象設置爲灰色,即增量更新(Imcremental update)。
出現老年代引用新生代的對象,GC 時如何處理?
JVM採用卡片標記(Card Marking)方法,避免 Minor GC 時須要掃描整個老年代。作法是:將老年代按照必定大小分片,每一片對應 Cards 中一項,若是老年代的對象發生了修改或指向了新生代對象,就將這個老年代的 Card 標記爲 dirty。Young GC 時,dirty card 加入待掃描的 GC Roots 範圍,避免掃描整個老年代
優勢:
一、併發收集、低停頓,Sun公司的一些官方文檔也稱之爲併發低停頓收集器(Concurrent Low Pause Collector)
缺點:
一、對CPU資源很是敏感:在併發階段,它雖然不會致使用戶線程停頓,但會由於佔用一部分線程(或CPU資源)而致使應用程序變慢,總吞吐量下降
二、產生空間碎片:基於「標記-清除」算法實現,意味着收集結束後會有大量空間碎片產生,給大對象分配帶來麻煩
三、須要更大的堆空間:CMS標記階段應用程序還在繼續執行,就會有堆空間繼續分配的狀況,爲保證 CMS 將堆回收完以前還有空間分配給正在運行的程序,必須預留一部分空間
③ CMS調優策略
-XX:CMSInitiatingOccupancyFraction=70 : 該值表明老年代堆空間的使用率,默認值是92,假如設置爲70,就表示第一次 CMS 垃圾收集會在老年代佔用 70% 時觸發。過大會使 STW 時間過程,太小會影響吞吐率
-XX:+UseCMSCompactAtFullCollection,-XX:CMSFullGCsBeforeCompaction=4:執行4次不壓縮的 Full GC 後,會執行一次內存壓縮的過程,用來消除內存碎片
-XX:+ConcGCThreads:併發 CMS 過程運行時的線程數,CMS 默認回收線程數是 (CPU+3) / 4。更多的線程會加快併發垃圾回收過程,但會帶來額外的同步開銷。
年輕代調優:Young GC 頻次高,則增大新生代;Young GC 時間長,則減小新生代。儘可能在 Young GC 時候回收大部分垃圾
G1(Garbage-First)是一款面向服務端應用的垃圾收集器,G1的設計初衷是最小化 STW 中斷時間,一般限制 GC 停頓時間比最大化吞吐率更重要。在Java9裏,G1已經成爲默認的垃圾回收器。
① 執行過程
G1的內存佈局和其餘垃圾收集器有很大區別,它將整個Java堆分爲 n 個大小相等的 Region,每一個 Region 佔有一塊連續的虛擬內存地址。新生代和老年代再也不是物理隔離,而是一部分 Region 的集合。
Region的大小能夠經過 -XX:G1HeapRegionSize 指定,若是未設置,默認將堆內存平均分爲 2048 份。G1仍屬於分代收集器,除了Eden、Survivor、Old區外,還有 Humongous 區用於專門存放巨型對象(一個對象佔用空間>50%分區容量),減小短時間存在的巨型對象對垃圾收集器形成的負面影響。
G1 的運做過程分爲如下幾個步驟:
一、全局併發標記:基於 STAB(snapshot-at-the-beginning)形式的併發標記,標記完成後,G1 基本知道了哪一個區域是空的,它首先會收集哪些產出大量空閒空間的區域,這也是它命名爲 Garbage-First 的緣由
1.1 初始標記(STW,耗時很短):標記 GC Roots 能直接關聯到的對象,將它們的字段壓入掃描棧
1.2 併發標記(與用戶線程併發執行,耗時較長):GC 線程不斷從掃描棧中取出引用,而後遞歸標記,直至掃描棧清空
1.3 最終標記(STW):從新標記『併發標記』期間因用戶程序執行而致使引用發生變更的那一部分標記(寫入屏障 Write Barrier 標記的對象)
1.4 清理(STW):統計各個 Region 被標記存活的對象有多少,若是發現沒有存活對象的 Region,就會將其總體回收到可分配的 Region 中
二、拷貝存活對象:將一部分 Region 裏的存活對象拷貝到空 Region 裏去,而後回收本來的 Region 的空間。
G1 的 GC 能夠分爲 Young GC 和 Mixed GC 兩種類型。Young GC 是選定全部新生態的 Region,經過控制新生代的 Region 個數控制 Young GC 的開銷。Mixed GC 是選定全部新生代裏的 Region,外加根據『全局併發標記』統計的收益較高的若干老年代 Region,在用戶指定的停頓時間內儘量選擇收益較高的老年代 Region。G1 裏不存在 Full GC,老年代的收集全靠 Mixed GC 來完成。
在 G1 中,使用 Rememberd Set 跟蹤 Region 內的對象引用,來避免全堆掃描的。每一個 Region 都有一個與之對應的 Rememberd Set,當程序對 Reference 類型的數據進行寫操做時,會產生 Write Barrier 暫停中斷寫操做,檢查 Reference 引用的對象是否處於不一樣的 Region 之中,若是是,便經過 CardTable 把相關引用信息記錄到被引用對象所屬 Region 的 Rememberd Set 中。當 GC 時,在GC Root 的枚舉範圍中加入 Rememberd Set 便可保證不對全堆掃描,也不會遺漏。
② G1與CMS的比較
G1的設計目標是取消CMS收集器,G1與CMS相比,有一些顯而易見的優勢:
一、簡單可行的性能調優:-XX:+UseG1GC -Xmx32g,使用這兩個參數便可應用於生產環境,表示開啓G1,堆最大內存爲32G;-XX:MaxGCPauseMillis=n 使用這個參數可設置指望的GC中暫停時間。取消老年代的物理空間劃分,無需對每一個代的空間進行大小設置
二、可預測的 STW 停頓時間:G1除了追求低停頓外,還能創建可預測的停頓時間模型,能讓使用者明確指定 GC 的停頓時間不超過 n 毫秒。這是經過跟蹤各個 Region 裏面的垃圾堆積的價值大小(回收所得到的空間大小以及回收所需時間的經驗值),在後臺維護優先列表,每次根據容許的收集時間,優先回收價值最大的Region,保證了 G1 能在有限的時間內能夠獲取儘量高的收集效率
三、空間整合:G1 的兩個 Region 之間是基於『複製』算法實現,在運做期間不會產生內存碎片,分配大對象時不會由於沒法找到連續空間而提早出發 Full GC
③ CMS調優策略
-XX:MaxGCPauseMillis=n:設置GC時最大暫停時間,這個目標不必定能知足,JVM會盡最大努力實現它,不建議設置的太小(<50ms)
-XX:InitiatingHeapOccupancyPercent=n:觸發G1啓動 Mixed GC,表示垃圾對象在整個 G1 堆內存空間的佔比
避免使用 -Xmn 或 -XX:NewRatio等其餘顯式設置年輕代大小的選項,固定年輕代大小會覆蓋暫停時間目標