目錄
對于一個Java程序員來說,,大多數(shù)情況下的確是無需對內(nèi)存的分配,、釋放做太多考慮,對Jvm也無需有多么深的理解的,。但是在寫程序的過程中卻也往往因為這樣而造成了一些不容易察覺到的內(nèi)存問題,,并且在內(nèi)存問題出現(xiàn)的時候,也不能很快的定位并解決,。因此,,了解并掌握J(rèn)ava的內(nèi)存管理是一個合格的Java程序員必需的技能,也只有這樣才能寫出更好的程序,,更好地優(yōu)化程序的性能,。
一. 背景知識
根據(jù)網(wǎng)絡(luò)可以找到的資料以及筆者能夠打聽到的消息,目前國內(nèi)外著名的幾個大型互聯(lián)網(wǎng)公司的語言選型概括如下:
- Google: c/c python java js,,不得不提的是Google貢獻給java社區(qū)的guava包質(zhì)量非常高,。
- Youtube、豆瓣: python
- fackbook,、yahoo,、flickr、新浪:php(優(yōu)化過的php)
- 網(wǎng)易,、阿里,、搜狐: java、php,、node.js
- Twitter: ruby->java,之所以如此就在于與jvm相比,,Ruby的runtime是非常慢的。并且ruby的應(yīng)用比起java還是比較小眾的,。
可見,,雖然最近這些年很多言論都號稱java已死或者不久即死,但是Java的語言應(yīng)用占有率一直居高不下,。與高性能的c/c 相比,,java具有g(shù)c機制,并且沒有那讓人望而生畏的指針,,上手門檻相對較低,;而與上手成本更低的php、ruby來說,,又比這些腳本語言有性能上的優(yōu)勢(這里暫時忽略fb自己開發(fā)的php vm),。
對于Java來說,最終是要依靠字節(jié)碼運行在jvm上的,。目前,,常見的jvm有以下幾種:
- Sun HotSpot
- BEA Jrockit
- IBM J9
- Dalvik(Android)
其中以HotSpot應(yīng)用最廣泛。目前sun jdk的最新版本已經(jīng)到了8,,但鑒于新版的jdk使用并未普及,,因此本文僅僅針對HotSpot虛擬機的jdk6來講,。
二. Jvm虛擬機內(nèi)存簡介
2.1 Java運行時內(nèi)存區(qū)
Java的運行時內(nèi)存組成如下圖所示:
其中,對于這各個部分有一些是線程私有的,,其他則是線程共享的,。
線程私有的如下:
程序計數(shù)器
當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器
Java虛擬機棧
Java方法執(zhí)行的內(nèi)存模型,每個方法被執(zhí)行時都會創(chuàng)建一個棧幀,,存儲局部變量表,、操作棧,、動態(tài)鏈接,、方法出口等信息。
- 每個線程都有自己獨立的??臻g
- 線程棧只存基本類型和對象地址
- 方法中局部變量在線程空間中
本地方法棧
Native方法服務(wù),。在HotSpot虛擬機中和Java虛擬機棧合二為一。
線程共享的如下:
Java堆
存放對象實例,,幾乎所有的對象實例以及其屬性都在這里分配內(nèi)存,。
方法區(qū)
存儲已經(jīng)被虛擬機加載的類信息、常量,、靜態(tài)變量,、JIT編譯后的代碼等數(shù)據(jù)。
運行時常量池
方法區(qū)的一部分,。用于存放編譯期生成的各種字面量和符號引用,。
直接內(nèi)存
NIO、Native函數(shù)直接分配的堆外內(nèi)存,。DirectBuffer引用也會使用此部分內(nèi)存,。
2.2 對象訪問
Java是面向?qū)ο蟮囊环N編程語言,那么如何通過引用來訪問對象呢,?一般有兩種方式:
通過句柄訪問
直接指針
此種方式也是HotSpot虛擬機采用的方式,。
2.3 內(nèi)存溢出
在JVM申請內(nèi)存的過程中,會遇到無法申請到足夠內(nèi)存,,從而導(dǎo)致內(nèi)存溢出的情況,。一般有以下幾種情況:
- 虛擬機棧和本地方法棧溢出
- StackOverflowError: 線程請求的棧深度大于虛擬機所允許的最大深度(循環(huán)遞歸)
- OutOfMemoryError: 虛擬機在擴展棧是無法申請到足夠的內(nèi)存空間,一般可以通過不停地創(chuàng)建線程引起此種情況
- Java堆溢出: 當(dāng)創(chuàng)建大量對象并且對象生命周期都很長的情況下,,會引發(fā)OutOfMemoryError
- 運行時常量區(qū)溢出:OutOfMemoryError:PermGen space,,這里一個典型的例子就是String的intern方法,當(dāng)大量字符串使用intern時,,會觸發(fā)此內(nèi)存溢出
- 方法區(qū)溢出:方法區(qū)存放Class等元數(shù)據(jù)信息,,如果產(chǎn)生大量的類(使用cglib),那么就會引發(fā)此內(nèi)存溢出,,OutOfMemoryError:PermGen space,,在使用Hibernate等框架時會容易引起此種情況,。
三. 垃圾收集
3.1 理論基礎(chǔ)
在通常情況下,我們掌握java的內(nèi)存管理就是為了應(yīng)對網(wǎng)站/服務(wù)訪問慢,,慢的原因一般有以下幾點:
- 內(nèi)存:垃圾收集占用cpu,;放入了太多數(shù)據(jù),造成內(nèi)存泄露(java也是有這種問題的^_^)
- 線程死鎖
- I/O速度太慢
- 依賴的其他服務(wù)響應(yīng)太慢
- 復(fù)雜的業(yè)務(wù)邏輯或者算法造成響應(yīng)的緩慢
其中,,垃圾收集對性能的影響一般有以下幾個:
- 內(nèi)存泄露
- 程序暫停
- 程序吞吐量顯著下降
- 響應(yīng)時間變慢
先來看垃圾收集的一些基本概念
- Concurrent Collector:收集的同時可運行其他的工作進程
- Parallel Collector: 使用多CPU進行垃圾收集
- Stop-the-word(STW):收集時必須暫停其他所有的工作進程
- Sticky-reference-count:對于使用“引用計數(shù)”(reference count)算法的GC,,如果對象的計數(shù)器溢出,則起不到標(biāo)記某個對象是垃圾的作用了,,這種錯誤稱為sticky-reference-count problem,,通常可以增加計數(shù)器的bit數(shù)來減少出現(xiàn)這個問題的幾率,,但是那樣會占用更多空間,。一般如果GC算法能迅速清理完對象,也不容易出現(xiàn)這個問題,。
- Mutator:mutate的中文是變異,,在GC中即是指一種JVM程序,專門更新對象的狀態(tài)的,,也就是讓對象“變異”成為另一種類型,,比如變?yōu)槔?/li>
- On-the-fly:用來描述某個GC的類型:on-the-fly reference count garbage collector。此GC不用標(biāo)記而是通過引用計數(shù)來識別垃圾,。
- Generational gc:這是一種相對于傳統(tǒng)的“標(biāo)記-清理”技術(shù)來說,,比較先進的gc,特點是把對象分成不同的generation,,即分成幾代人,,有年輕的,有年老的,。這類gc主要是利用計算機程序的一個特點,,即“越年輕的對象越容易死亡”,也就是存活的越久的對象越有機會存活下去(姜是老的辣),。
牽扯到垃圾收集,,還需要搞清楚吞吐量與響應(yīng)時間的含義
- 吞吐量是對單位時間內(nèi)完成的工作量的量度。如:每分鐘的 Web 服務(wù)器請求數(shù)量
- 響應(yīng)時間是提交請求和返回該請求的響應(yīng)之間使用的時間,。如:訪問Web頁面花費的時間
吞吐量與訪問時間的關(guān)系很復(fù)雜,,有時可能以響應(yīng)時間為代價而得到較高的吞吐量,而有時候又要以吞吐量為代價得到較好的響應(yīng)時間,。而在其他情況下,,一個單獨的更改可能對兩者都有提高。通常,平均響應(yīng)時間越短,,系統(tǒng)吞吐量越大,;平均響應(yīng)時間越長,系統(tǒng)吞吐量越??;
但是,系統(tǒng)吞吐量越大,, 未必平均響應(yīng)時間越短,;因為在某些情況(例如,不增加任何硬件配置)吞吐量的增大,,有時會把平均響應(yīng)時間作為犧牲,,來換取一段時間處理更多的請求。
針對于Java的垃圾回收來說,,不同的垃圾回收器會不同程度地影響這兩個指標(biāo),。例如:并行的垃圾收集器,,其保證的是吞吐量,,會在一定程度上犧牲響應(yīng)時間。而并發(fā)的收集器,,則主要保證的是請求的響應(yīng)時間,。
對于GC(垃圾回收)的流程的基本描述如下:
- 找出堆中活著的對象
- 釋放死對象占用的資源
- 定期調(diào)整活對象的位置
GC算法一般有以下幾種:
- Mark-Sweep 標(biāo)記-清除
- Mark-Sweep-Compact 標(biāo)記-整理
Copying Collector 復(fù)制算法
Mark-標(biāo)記
從”GC roots”開始掃描(這里的roots包括線程棧、靜態(tài)常量等),,給能夠沿著roots到達的對象標(biāo)記為”live”,最終所有能夠到達的對象都被標(biāo)記為”live”,而無法到達的對象則為”dead”,。效率和存活對象的數(shù)量是線性相關(guān)的。
Sweep-清除
掃描堆,,定位到所有”dead”對象,,并清理掉。效率和堆的大小是線性相關(guān)的,。
Compact-壓縮
對于對象的清除,,會產(chǎn)生一些內(nèi)存碎片,這時候就需要對這些內(nèi)存進行壓縮,、整理,。包括:relocate(將存貨的對象移動到一起,從而釋放出連續(xù)的可用內(nèi)存),、remap(收集所有的對象引用指向新的對象地址),。效率和存活對象的數(shù)量是線性相關(guān)的。
Copy-復(fù)制
將內(nèi)存分為”from”和”to”兩個區(qū)域,,垃圾回收時,,將from區(qū)域的存活對象整體復(fù)制到to區(qū)域中。效率和存活對象的數(shù)量是線性相關(guān)的。
其中,,Copy對比Mark-sweep
- 內(nèi)存消耗:copy需要兩倍的最大live set內(nèi)存,;mark-sweep則只需要一倍。
- 效率上:copy與live set成線性相關(guān),,效率高,;mark-sweep則與堆大小線性相關(guān),效率較低,。
分代收集是目前比較先進的垃圾回收方案
對于分代收集,,有以下幾個相關(guān)理論
- 分代假設(shè):大部分對象的壽命很短,“朝生夕死”,,重點放在對年青代對象的收集,,而且年青代通常只占整個空間的一小部分。
- 把年青代里活的很長的對象移動到老年代,。
- 只有當(dāng)老年代滿了才去收集,。
- 收集效率明顯比不分代高。
HotSpot虛擬機的分代收集,,分為一個Eden區(qū),、兩個Survivor去以及Old Generation/Tenured區(qū),其中Eden以及Survivor共同組成New Generatiton/Young space,。
- Eden區(qū)是分配對象的區(qū)域,。
- Survivor是minor/younger gc后存儲存活對象的區(qū)域。
- Tenured區(qū)域存儲長時間存活的對象,。
分代收集中典型的垃圾收集算法組合描述如下:
- 年青代通常使用Copy算法收集,,會stop the world
- 老年代收集一般采用Mark-sweep-compact, 有可能會stop the world,也可以是concurrent或者部分concurrent,。
3.2 HotSpot垃圾收集器
上圖即為HotSpot虛擬機的垃圾收集器組成,。
Serial收集器
- -XX: UserSerialGC參數(shù)打開此收集器
- Client模式下新生代默認(rèn)的收集器。
- 較長的stop the world時間
- 簡單而高效
此收集器的一個工作流程如下如所示:
收集前:
收集后:
ParNew收集器
- -XX: UserParNewGC
- UseConcuMarkSweepGC時默認(rèn)開啟
- Serial收集器的多線程版本
- 默認(rèn)線程數(shù)與CPU數(shù)目相同
- -XX:ParrallelGCThreads指定線程數(shù)目
對比Serial收集器如下圖所示:
Parallel Scavenge收集器
- 新生代并行收集器
- 采用Copy算法
- 主要關(guān)注的是達到可控制的吞吐量,,“吞吐量優(yōu)先”
- -XX:MaxGCPauseMillis -XX:GCTimeRAtion兩個參數(shù)精確控制吞吐量
- -XX:UseAdaptiveSizePolicy GC自適應(yīng)調(diào)節(jié)策略
- Server模式的默認(rèn)新生代收集器
Serial Old收集器
- Serial的老年代版本
- Client模式的默認(rèn)老年代收集器
- CMS收集器的后備預(yù)案,,Concurrent Mode Failure時使用
- -XX: UseSerialGC開啟此收集器
Parallel Old收集器
- -XX: UseParallelGC -XX: UseParallelOldGC啟用此收集器
- Server模式的默認(rèn)老年代收集器
- Parallel Scavenge的老年代版本,使用多線程和”mark-sweep”算法
- 關(guān)注點在吞吐量以及CPU資源敏感的場合使用
- 一般使用Parallel Scavenge Parallel Old可以達到最大吞吐量保證
CMS收集器
并發(fā)低停頓收集器
- -XX:UseConcMarkSweepGC 開啟CMS收集器,,(默認(rèn)使用ParNew作為年輕代收集器,,SerialOld作為收集失敗的垃圾收集器)
- 以獲取最短回收停頓時間為目標(biāo)的收集器,重視響應(yīng)速度,,希望系統(tǒng)停頓時間最短,,會和互聯(lián)網(wǎng)應(yīng)用。
四個步驟:
- 初始標(biāo)記 Stop the world: 只標(biāo)記GC roots能直接關(guān)聯(lián)到的對象,,速度很快,。
- 并發(fā)標(biāo)記:進行GC roots tracing,與用戶線程并發(fā)進行
- 重新標(biāo)記 Stop the world:修正并發(fā)標(biāo)記期間因程序繼續(xù)運行導(dǎo)致變動的標(biāo)記記錄
- 并發(fā)清除
對比serial old收集器如下圖所示:
CMS有以下的缺點:
- CMS是唯一不進行compact的垃圾收集器,當(dāng)cms釋放了垃圾對象占用的內(nèi)存后,,它不會把活動對象移動到老年代的一端
- 對CPU資源非常敏感,。不會導(dǎo)致線程停頓,但會導(dǎo)致程序變慢,,總吞吐量降低,。CPU核越多越不明顯
- 無法處理浮動垃圾??赡艹霈F(xiàn)“concurrent Mode Failure”失敗,, 導(dǎo)致另一次full GC ,可以通過調(diào)整-XX:CMSInitiatingOccupancyFraction來控制內(nèi)存占用達到多少時觸發(fā)gc
- 大量空間碎片。這個可以通過設(shè)置-XX:UseCMSCompacAtFullCollection(是否在full gc時開啟compact)以及-XX:CMSFullGCsBeforeCompaction(在進行compact前full gc的次數(shù))
G1收集器
G1算法在Java6中還是試驗性質(zhì)的,,在Java7中正式引入,,但還未被廣泛運用到生產(chǎn)環(huán)境中。它的特點如下:
- 使用標(biāo)記-清理算法
- 不會產(chǎn)生碎片
- 可預(yù)測的停頓時間
- 化整為零:將整個Java堆劃分為多個大小相等的獨立區(qū)域
- -XX: UseG1GC可以打開此垃圾回收器
- -XX:MaxGCPauseMillis=200可以設(shè)置最大GC停頓時間,,當(dāng)然JVM并不保證一定能夠達到,,只是盡力。
3.3 調(diào)優(yōu)經(jīng)驗
- 需要打開gc日志并讀懂gc日志:-XX:PrintHeapAtGC -XX: PrintGCDetails -XX: PrintGCDateStamps
- 垃圾回收的最佳狀態(tài)是只有young gc,,也就是避免生命周期很長的對象的存在,。
- 從young gc開始,盡量給年青代大點的內(nèi)存,,避免full gc
- 注意Survivor大小
- 注意內(nèi)存墻:4G~5G
GC日志簡介
- 第一個箭頭:35592K->1814K(36288K),,箭頭指向的是新生段的內(nèi)存占用情況; - 第二個箭頭:38508K->7792K(520256K),,箭頭指向的是回收后的內(nèi)存占用情況。
- 垃圾收集停頓時間:0.0336
老年代使用建議
- Parallel GC(-XX: UseParallel[Old]GC)
- Parallel GC的minor GC時間是最快的,, CMS的young gc要比parallel慢,, 因為內(nèi)存碎片
- 可以保證最大的吞吐量
- 確實有必要才改成CMS或G1(for old gen collections)
開發(fā)建議
- 小對象allocate的代價很小,通常10個CPU指令,;收集掉新對象也非常廉價,;不用擔(dān)心活的很短的小對象
- 大對象分配的代價以及初始化的代價很大;不同大小的大對象可能導(dǎo)致java堆碎片,,尤其是CMS, ParallelGC 或 G1還好,;盡量避免分配大對象
- 避免改變數(shù)據(jù)結(jié)構(gòu)大小,如避免改變數(shù)組或array backed collections / containers的大小;對象構(gòu)建(初始化)時最好顯式批量定數(shù)組大小;改變大小導(dǎo)致不必要的對象分配,,可能導(dǎo)致java堆碎片
- 對象池可能潛在的問題
- 增加了活對象的數(shù)量,,可能增加GC時間
- 訪問(多線程)對象池需要鎖,可能帶來可擴展性的問題
- 小心過于頻繁的對象池訪問
四. Java7,、8帶來的一些變化
- Java7帶來的內(nèi)存方面的一個很大的改變就是String常量池從Perm區(qū)移動到了Heap中。調(diào)用String的intern方法時,如果存在堆中的對象,,則會直接保存對象的引用,,而不會重新創(chuàng)建對象。
- Java7正式引入G1垃圾收集器用于替換CMS,。
- Java8中,,取消掉了方法區(qū)(永久代),使用“元空間”替代,,元空間只與系統(tǒng)內(nèi)存相關(guān),。
- Java 8 update 20所引入的一個很棒的優(yōu)化就是G1回收器中的字符串去重(String deduplication)。由于字符串(包括它們內(nèi)部的char[]數(shù)組)占用了大多數(shù)的堆空間,,這項新的優(yōu)化旨在使得G1回收器能識別出堆中那些重復(fù)出現(xiàn)的字符串并將它們指向同一個內(nèi)部的char[]數(shù)組,,以避免同一個字符串的多份拷貝,那樣堆的使用效率會變得很低,??梢允褂?XX: UseStringDeduplication這個JVM參數(shù)來試一下這個特性。
|