談談Java內存管理

目錄

java

對於一個Java程序員來講,大多數狀況下的確是無需對內存的分配、釋放作太多考慮,對Jvm也無需有多麼深的理解的。可是在寫程序的過程當中卻也每每由於這樣而形成了一些不容易察覺到的內存問題,而且在內存問題出現的時候,也不能很快的定位並解決。所以,瞭解並掌握Java的內存管理是一個合格的Java程序員必需的技能,也只有這樣才能寫出更好的程序,更好地優化程序的性能。php

一. 背景知識

根據網絡能夠找到的資料以及筆者可以打聽到的消息,目前國內外著名的幾個大型互聯網公司的語言選型歸納以下:java

  1. Google: C/C++ Go Python Java JavaScript,不得不提的是Google貢獻給java社區的guava包質量很是高,很是值得學習和使用。
  2. Youtube、豆瓣: Python
  3. Fackbook、Yahoo、Flickr、新浪:php(優化過的php vm)
  4. 網易、阿里、搜狐: Java、PHP、Node.js
  5. Twitter: Ruby->Java,之因此如此就在於與Jvm相比,Ruby的runtime是很是慢的。而且Ruby的應用比起Java仍是比較小衆的。不過最近twitter有往scala上遷移的趨勢。

可見,雖然最近這些年不少言論都號稱java已死或者不久即死,可是Java的語言應用佔有率一直居高不下。與高性能的C/C++相比,Java具備gc機制,而且沒有那讓人望而生畏的指針,上手門檻相對較低;而與上手成本更低的PHP、Ruby等腳本語言來講,又比這些腳本語言有性能上的優點(這裏暫時忽略FB本身開發的HHVM)。程序員

對於Java來講,最終是要依靠字節碼運行在jvm上的。目前,常見的jvm有如下幾種:算法

  • Sun HotSpot
  • BEA Jrockit
  • IBM J9
  • Dalvik(Android)

其中以HotSpot應用最普遍。目前sun jdk的最新版本已經到了8,但鑑於新版的jdk使用並未普及,所以本文僅僅針對HotSpot虛擬機的jdk6來說。編程

二. Jvm虛擬機內存簡介

2.1 Java運行時內存區

Java的運行時內存組成以下圖所示:數組

java-runtime-memory.jpg

其中,對於這各個部分有一些是線程私有的,其餘則是線程共享的。服務器

線程私有的以下:網絡

  • 程序計數器數據結構

    當前線程所執行的字節碼的行號指示器多線程

  • Java虛擬機棧

    Java方法執行的內存模型,每一個方法被執行時都會建立一個棧幀,存儲局部變量表、操做棧、動態連接、方法出口等信息。

    • 每一個線程都有本身獨立的棧空間
    • 線程棧只存基本類型和對象地址
    • 方法中局部變量在線程空間中
  • 本地方法棧

    Native方法服務。在HotSpot虛擬機中和Java虛擬機棧合二爲一。

線程共享的以下:

  • Java堆

    存放對象實例,幾乎全部的對象實例以及其屬性都在這裏分配內存。

  • 方法區

    存儲已經被虛擬機加載的類信息、常量、靜態變量、JIT編譯後的代碼等數據。

  • 運行時常量池

    方法區的一部分。用於存放編譯期生成的各類字面量和符號引用。

  • 直接內存

    NIO、Native函數直接分配的堆外內存。DirectBuffer引用也會使用此部份內存。

2.2 對象訪問

Java是面向對象的一種編程語言,那麼如何經過引用來訪問對象呢?通常有兩種方式:

  1. 經過句柄訪問

  2. 直接指針

    此種方式也是HotSpot虛擬機採用的方式。

2.3 內存溢出

在JVM申請內存的過程當中,會遇到沒法申請到足夠內存,從而致使內存溢出的狀況。通常有如下幾種狀況:

  • 虛擬機棧和本地方法棧溢出
    • StackOverflowError: 線程請求的棧深度大於虛擬機所容許的最大深度(循環遞歸)
    • OutOfMemoryError: 虛擬機在擴展棧是沒法申請到足夠的內存空間,通常能夠經過不停地建立線程引發此種狀況
  • Java堆溢出: 當建立大量對象而且對象生命週期都很長的狀況下,會引起OutOfMemoryError
  • 運行時常量區溢出:OutOfMemoryError:PermGen space,這裏一個典型的例子就是String的intern方法,當大量字符串使用intern時,會觸發此內存溢出
  • 方法區溢出:方法區存放Class等元數據信息,若是產生大量的類(使用cglib),那麼就會引起此內存溢出,OutOfMemoryError:PermGen space,在使用Hibernate等框架時會容易引發此種狀況。

三. 垃圾收集

3.1 理論基礎

在一般狀況下,咱們掌握java的內存管理就是爲了應對網站/服務訪問慢,慢的緣由通常有如下幾點:

  • 內存:垃圾收集佔用cpu;放入了太多數據,形成內存泄露(java也是有這種問題的^_^)
  • 線程死鎖
  • I/O速度太慢
  • 依賴的其餘服務響應太慢
  • 複雜的業務邏輯或者算法形成響應的緩慢

其中,垃圾收集對性能的影響通常有如下幾個:

  • 內存泄露
  • 程序暫停
  • 程序吞吐量顯著降低
  • 響應時間變慢

垃圾收集的一些基本概念

  • Concurrent Collector:收集的同時可運行其餘的工做進程
  • Parallel Collector: 使用多CPU進行垃圾收集
  • Stop-the-word(STW):收集時必須暫停其餘全部的工做進程
  • Sticky-reference-count:對於使用「引用計數」(reference count)算法的GC,若是對象的計數器溢出,則起不到標記某個對象是垃圾的做用了,這種錯誤稱爲sticky-reference-count problem,一般能夠增長計數器的bit數來減小出現這個問題的概率,可是那樣會佔用更多空間。通常若是GC算法能迅速清理完對象,也不容易出現這個問題。
  • Mutator:mutate的中文是變異,在GC中便是指一種JVM程序,專門更新對象的狀態的,也就是讓對象「變異」成爲另外一種類型,好比變爲垃圾。
  • On-the-fly:用來描述某個GC的類型:on-the-fly reference count garbage collector。此GC不用標記而是經過引用計數來識別垃圾。
  • Generational gc:這是一種相對於傳統的「標記-清理」技術來講,比較先進的gc,特色是把對象分紅不一樣的generation,即分紅幾代人,有年輕的,有年老的。這類gc主要是利用計算機程序的一個特色,即「越年輕的對象越容易死亡」,也就是存活的越久的對象越有機會存活下去(薑是老的辣)。

吞吐量與響應時間

牽扯到垃圾收集,還須要搞清楚吞吐量與響應時間的含義

  • 吞吐量是對單位時間內完成的工做量的量度。如:每分鐘的 Web 服務器請求數量
  • 響應時間是提交請求和返回該請求的響應之間使用的時間。如:訪問Web頁面花費的時間

吞吐量與訪問時間的關係很複雜,有時可能以響應時間爲代價而獲得較高的吞吐量,而有時候又要以吞吐量爲代價獲得較好的響應時間。而在其餘狀況下,一個單獨的更改可能對二者都有提升。一般,平均響應時間越短,系統吞吐量越大;平均響應時間越長,系統吞吐量越小; 可是,系統吞吐量越大, 未必平均響應時間越短;由於在某些狀況(例如,不增長任何硬件配置)吞吐量的增大,有時會把平均響應時間做爲犧牲,來換取一段時間處理更多的請求。

針對於Java的垃圾回收來講,不一樣的垃圾回收器會不一樣程度地影響這兩個指標。例如:並行的垃圾收集器,其保證的是吞吐量,會在必定程度上犧牲響應時間。而併發的收集器,則主要保證的是請求的響應時間。

GC的流程

  • 找出堆中活着的對象
  • 釋放死對象佔用的資源
  • 按期調整活對象的位置

GC算法

  • Mark-Sweep 標記-清除
  • Mark-Sweep-Compact 標記-整理
  • Copying Collector 複製算法

  • Mark-標記

    從」GC roots」開始掃描(這裏的roots包括線程棧、靜態常量等),給可以沿着roots到達的對象標記爲」live」,最終全部可以到達的對象都被標記爲」live」,而沒法到達的對象則爲」dead」。效率和存活對象的數量是線性相關的。

  • Sweep-清除

    掃描堆,定位到全部」dead」對象,並清理掉。效率和堆的大小是線性相關的。

  • Compact-壓縮

    對於對象的清除,會產生一些內存碎片,這時候就須要對這些內存進行壓縮、整理。包括:relocate(將存貨的對象移動到一塊兒,從而釋放出連續的可用內存)、remap(收集全部的對象引用指向新的對象地址)。效率和存活對象的數量是線性相關的。

  • Copy-複製

    將內存分爲」from」和」to」兩個區域,垃圾回收時,將from區域的存活對象總體複製到to區域中。效率和存活對象的數量是線性相關的。

其中,Copy對比Mark-sweep

  1. 內存消耗:copy須要兩倍的最大live set內存;mark-sweep則只須要一倍。
  2. 效率上:copy與live set成線性相關,效率高;mark-sweep則與堆大小線性相關,效率較低。

分代收集

分代收集是目前比較先進的垃圾回收方案。有如下幾個相關理論

  • 分代假設:大部分對象的壽命很短,「朝生夕死」,重點放在對年青代對象的收集,並且年青代一般只佔整個空間的一小部分。
  • 把年青代裏活的很長的對象移動到老年代。
  • 只有當老年代滿了纔去收集。
  • 收集效率明顯比不分代高。

HotSpot虛擬機的分代收集,分爲一個Eden區、兩個Survivor去以及Old Generation/Tenured區,其中Eden以及Survivor共同組成New Generatiton/Young space。一般將對New Generation進行的回收稱爲Minor GC;對Old Generation進行的回收稱爲Major GC,但因爲Major GC除併發GC外均需對整個堆以及Permanent Generation進行掃描和回收,所以又稱爲Full GC。

  • Eden區是分配對象的區域。
  • Survivor是minor/younger gc後存儲存活對象的區域。
  • Tenured區域存儲長時間存活的對象。

分代收集中典型的垃圾收集算法組合描述以下:

  • 年青代一般使用Copy算法收集,會stop the world
  • 老年代收集通常採用Mark-sweep-compact, 有可能會stop the world,也能夠是concurrent或者部分concurrent。

那麼什麼時候進行Minor GC、什麼時候進行Major GC? 通常的過程以下:

  • 對象在Eden Space完成內存分配
  • 當Eden Space滿了,再建立對象,會由於申請不到空間,觸發Minor GC,進行New(Eden + S0 或 Eden S1) Generation進行垃圾回收
  • Minor GC時,Eden Space不能被回收的對象被放入到空的Survivor(S0或S1,Eden確定會被清空),另外一個Survivor裏不能被GC回收的對象也會被放入這個Survivor,始終保證一個Survivor是空的
  • 在Step3時,若是發現Survivor區滿了,則這些對象被copy到old區,或者Survivor並無滿,可是有些對象已經足夠Old,也被放入Old Space。
  • 當Old Space被放滿以後,進行Full GC

但這個具體還要看JVM是採用的哪一種GC方案。

New Generation的GC有如下三種:

  • Serial
  • ParallelScavenge
  • ParNew

對於上述三種GC方案均是在Eden Space分配不下時,觸發GC。

Old Generation的GC有如下四種:

  • Serial Old
  • Parallel
  • CMS

對於Serial Old, Parallel Old而言觸發機制爲

  • Old Generation空間不足
  • Permanent Generation空間不足
  • Minor GC時的悲觀策略
  • Minor GC後在Eden上分配內存仍然失敗
  • 執行Heap Dump時
  • 外部調用System.gc,可經過-XX:+DisableExplicitGC來禁止,。這裏須要注意的是禁用System.gc()會引發使用NIO時的OOM,因此此選項慎重使用。具體可見:http://hllvm.group.iteye.com/group/topic/27945

對於CMS而言觸發機制爲:

  • 當Old Generation空間使用到必定比率時觸發,HopSpot V1.6中默認是92%,可經過PrintCMSInitiationStatistics(此參數在V1.5中不能用)來查看這個值究竟是多少,經過CMSInitiatingOccupancyFaction來強制指定。默認值是根據以下公式計算出來的:((100 -MinHeapFreeRatio) +(double)(CMSTriggerRatio* MinHeapFreeRatio) / 100.0)/ 100.0,MinHeapFreeRatio默認值爲40,CMSTriggerRatio默認值爲80。
  • 當Permanent Generation採用CMS收集且空間使用到必定比率觸發,Permanent Generation採用CMS收集需設置:-XX:+CMSClassUnloadingEnabled。 Hotspot V1.6中默認爲92%,可經過CMSInitiatingPermOccupancyFraction來強制指定。一樣,它是根據以下公式計算出來的:((100 -MinHeapFreeRatio) +(double)(CMSTriggerPermRatio* MinHeapFreeRatio) / 100.0)/ 100.0,MinHeapFreeRatio默認值爲40,CMSTriggerPermRatio默認值爲80。
  • Hotspot根據成本計算決定是否須要執行CMS GC,可經過-XX:+UseCmsInitiatingOccupancyOnly來去掉這個動態執行的策略。
  • 外部調用System.gc,且設置了ExplicitGCIInvokesConcurrent或者ExplicitGCInvokesConcurrentAndUnloadsClasses。

3.2 HotSpot垃圾收集器

上圖即爲HotSpot虛擬機的垃圾收集器組成。

Serial收集器

  • -XX:+UserSerialGC參數打開此收集器
  • Client模式下新生代默認的收集器。
  • 較長的stop the world時間
  • 簡單而高效

此收集器的一個工做流程以下如所示:

收集前:

收集後:

ParNew收集器

  • -XX:+UserParNewGC
  • +UseConcuMarkSweepGC時默認開啓
  • Serial收集器的多線程版本
  • 默認線程數與CPU數目相同
  • -XX:ParrallelGCThreads指定線程數目

對比Serial收集器以下圖所示:

Parallel Scavenge收集器

  • 新生代並行收集器
  • 採用Copy算法
  • 主要關注的是達到可控制的吞吐量,「吞吐量優先」
  • -XX:MaxGCPauseMillis -XX:GCTimeRAtion兩個參數精確控制吞吐量
  • -XX:UseAdaptiveSizePolicy GC自適應調節策略
  • Server模式的默認新生代收集器

Serial Old收集器

  • Serial的老年代版本
  • Client模式的默認老年代收集器
  • CMS收集器的後備預案,Concurrent Mode Failure時使用
  • -XX:+UseSerialGC開啓此收集器

Parallel Old收集器

  • -XX:+UseParallelGC -XX:+UseParallelOldGC啓用此收集器
  • Server模式的默認老年代收集器
  • Parallel Scavenge的老年代版本,使用多線程和」mark-sweep」算法
  • 關注點在吞吐量以及CPU資源敏感的場合使用
  • 通常使用Parallel Scavenge + Parallel Old能夠達到最大吞吐量保證

CMS收集器

併發低停頓收集器

  • -XX:UseConcMarkSweepGC 開啓CMS收集器,(默認使用ParNew做爲年輕代收集器,SerialOld做爲收集失敗的垃圾收集器)
  • 以獲取最短回收停頓時間爲目標的收集器,重視響應速度,但願系統停頓時間最短,會和互聯網應用。

四個步驟:

  • 初始標記 Stop the world: 只標記GC roots能直接關聯到的對象,速度很快。
  • 併發標記:進行GC roots tracing,與用戶線程併發進行
  • 從新標記 Stop the world:修正併發標記期間因程序繼續運行致使變更的標記記錄
  • 併發清除

對比serial old收集器以下圖所示:

CMS有如下的缺點:

  • CMS是惟一不進行compact的垃圾收集器,當cms釋放了垃圾對象佔用的內存後,它不會把活動對象移動到老年代的一端
  • 對CPU資源很是敏感。不會致使線程停頓,但會致使程序變慢,總吞吐量下降。CPU核越多越不明顯
  • 沒法處理浮動垃圾。可能出現「concurrent Mode Failure」失敗, 致使另外一次full GC ,能夠經過調整-XX:CMSInitiatingOccupancyFraction來控制內存佔用達到多少時觸發gc
  • 大量空間碎片。這個能夠經過設置-XX:UseCMSCompacAtFullCollection(是否在full gc時開啓compact)以及-XX:CMSFullGCsBeforeCompaction(在進行compact前full gc的次數)

G1收集器

G1算法在Java6中仍是試驗性質的,在Java7中正式引入,但還未被普遍運用到生產環境中。它的特色以下:

  • 使用標記-清理算法
  • 不會產生碎片
  • 可預測的停頓時間
  • 化整爲零:將整個Java堆劃分爲多個大小相等的獨立區域
  • -XX:+UseG1GC能夠打開此垃圾回收器
  • -XX:MaxGCPauseMillis=200能夠設置最大GC停頓時間,固然JVM並不保證必定可以達到,只是盡力。

3.3 調優經驗

  • 須要打開gc日誌並讀懂gc日誌:-XX:PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamp -Xloggc:$CATALINA_BASE/logs/gc.log
  • 垃圾回收的最佳狀態是隻有young gc,也就是避免生命週期很長的對象的存在。
  • 從young gc開始,儘可能給年青代大點的內存,避免full gc
  • 注意Survivor大小
  • 注意內存牆:4G~5G

GC日誌簡介

1403682.561: [GC [PSYoungGen: 1375104K->11376K(1386176K)] 4145665K->2782002K(4182400K), 0.0174410 secs] [Times: user=0.27 sys=0.00, real=0.02 secs]

  • 1403682.561:發生的時間點,JVM運行的時間長度,以度爲單位,也能夠格式化成固定的時間格式(使用-XX:+PrintGCDateStamps)
  • PSYoungGen:發生了何種類型的GC,此處表明發生了年輕代的GC
  • 1375104K:回收前的大小
  • 11376K:回收後的大小
  • 1386176K:YOUNG代的大小
  • 4145665 K:回收前總的佔用大小
  • 2782002K:回收後的佔用大小
  • 4182400K:總佔用大小
  • 0.0174410:垃圾收集停頓時間
  • 0.27和0.00:表明在用戶態(user)和系統狀(sys)的CPU運行時間
  • 0.02 secs:表明實際的GC的運行時間

注:上面實際GC的運行時間小於用戶態和系統態的時間總和,是因爲前者僅指CPU的運行時間,包括等待或IO阻塞的時間,而如今的GC是採用多線程收集的,同時機器也是多個CPU,所以,大部分是兩者之和要比前面的值大。若是是採用串形化收集器的話,兩者時間幾乎相差很少。

老年代使用建議

  • Parallel GC(-XX:+UseParallel[Old]GC)
    • Parallel GC的minor GC時間是最快的, CMS的young gc要比parallel慢, 由於內存碎片
    • 能夠保證最大的吞吐量
  • 確實有必要才改爲CMS或G1(for old gen collections)

開發建議

  • 小對象allocate的代價很小,一般10個CPU指令;收集掉新對象也很是廉價;不用擔憂活的很短的小對象
  • 大對象分配的代價以及初始化的代價很大;不一樣大小的大對象可能致使java堆碎片,尤爲是CMS, ParallelGC 或 G1還好;儘可能避免分配大對象
  • 避免改變數據結構大小,如避免改變數組或array backed collections / containers的大小;對象構建(初始化)時最好顯式批量定數組大小;改變大小致使沒必要要的對象分配,可能致使java堆碎片
  • 對象池可能潛在的問題
    • 增長了活對象的數量,可能增長GC時間
    • 訪問(多線程)對象池須要鎖,可能帶來可擴展性的問題
    • 當心過於頻繁的對象池訪問

GC的龐氏騙局

雖然GC在大多數狀況下仍是正常的,但有時候JVM也會發生欺騙你的場景, JVM不停的在垃圾回收,但是每次回收完後堆卻仍是滿的,很明顯程序內存被使用完了,已經沒法正常工做了,但JVM就是不拋出OutOfMemoryError(OOM)這個異常來告訴程序員內部發出了什麼,只是不停的作老好人嘗試幫咱們作垃圾回收,把服務器的資源耗光了。

出現這種現象的一種典型狀況就是GC的GCTimeLimit和GCHeapFreeLimit參數設置不合適。GCTimeLimit的默認值是98%,也就是說若是大於等於98%的時間都用花在GC上,則會拋出OutOfMemoryError。GCHeapFreeLimit是回收後可用堆的大小,默認值是2%,也就是說只要有多餘2%的內存可用就認爲這次gc是成功的。若是GCTimeLimit設置過大或者GCHeapFreeLimit設置太小那麼就會形成GC的龐式騙局,不停地進行垃圾回收。

四. Java七、8帶來的一些變化

  • Java7帶來的內存方面的一個很大的改變就是String常量池從Perm區移動到了Heap中。調用String的intern方法時,若是存在堆中的對象,則會直接保存對象的引用,而不會從新建立對象。
  • Java7正式引入G1垃圾收集器用於替換CMS。
  • Java8中,取消掉了方法區(永久代),使用「元空間」替代,元空間只與系統內存相關。
  • Java 8 update 20所引入的一個很棒的優化就是G1回收器中的字符串去重(String deduplication)。因爲字符串(包括它們內部的char[]數組)佔用了大多數的堆空間,這項新的優化旨在使得G1回收器能識別出堆中那些重複出現的字符串並將它們指向同一個內部的char[]數組,以免同一個字符串的多份拷貝,那樣堆的使用效率會變得很低。可使用-XX:+UseStringDedup
相關文章
相關標籤/搜索