談談Java內存管理

對於一個Java程序員來講,大多數狀況下的確是無需對內存的分配、釋放作太多考慮,對Jvm也無需有多麼深的理解的。php

 

可是在寫程序的過程當中卻也每每由於這樣而形成了一些不容易察覺到的內存問題,而且在內存問題出現的時候,也不能很快的定位並解決。java

 

所以,瞭解並掌握Java的內存管理是一個合格的Java程序員必需的技能,也只有這樣才能寫出更好的程序,更好地優化程序的性能。程序員

 

1、背景知識


 

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

 

  • Google: C/C++ Go Python Java JavaScript,不得不提的是Google貢獻給java社區的guava包質量很是高,很是值得學習和使用。編程

  • Youtube、豆瓣: Python數組

  • Fackbook、Yahoo、Flickr、新浪:php(優化過的php vm)服務器

  • 網易、阿里、搜狐: Java、PHP、Node.js網絡

  • 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來說。

 

2、Jvm虛擬機內存簡介


 

>>2.1 Java運行時內存區

 

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

 

 

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

 

線程私有的以下:

 

①程序計數器

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

 

②Java虛擬機棧

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

 

  • 每一個線程都有本身獨立的棧空間

  • 線程棧只存基本類型和對象地址

  • 方法中局部變量在線程空間中

 

③本地方法棧

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

 

線程共享的以下:

 

①Java堆

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

 

②方法區

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

 

③運行時常量池

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

 

④直接內存

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

 

>>2.2 對象訪問

 

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

 

①經過句柄訪問

 

 

①直接指針

 

 

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

 

>>2.3 內存溢出

 

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

 

①虛擬機棧和本地方法棧溢出

 

  • StackOverflowError: 線程請求的棧深度大於虛擬機所容許的最大深度(循環遞歸)

  • OutOfMemoryError: 虛擬機在擴展棧是沒法申請到足夠的內存空間,通常能夠經過不停地建立線程引發此種狀況

 

②Java堆溢出: 當建立大量對象而且對象生命週期都很長的狀況下,會引起OutOfMemoryError

 

③運行時常量區溢出:OutOfMemoryError:PermGen space,這裏一個典型的例子就是String的intern方法,當大量字符串使用intern時,會觸發此內存溢出

 

④方法區溢出:方法區存放Class等元數據信息,若是產生大量的類(使用cglib),那麼就會引起此內存溢出,OutOfMemoryError:PermGen space,在使用Hibernate等框架時會容易引發此種狀況。

 

3、垃圾收集


 

>>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

 

  • 內存消耗:copy須要兩倍的最大live set內存;mark-sweep則只須要一倍。

  • 效率上:copy與live set成線性相關,效率高;mark-sweep則與堆大小線性相關,效率較低。

 

分代收集是目前比較先進的垃圾回收方案

 

對於分代收集,有如下幾個相關理論

 

  • 分代假設:大部分對象的壽命很短,「朝生夕死」,重點放在對年青代對象的收集,並且年青代一般只佔整個空間的一小部分。

  • 把年青代裏活的很長的對象移動到老年代。

  • 只有當老年代滿了纔去收集。

  • 收集效率明顯比不分代高。

 

HotSpot虛擬機的分代收集,分爲一個Eden區、兩個Survivor去以及Old Generation/Tenured區,其中Eden以及Survivor共同組成New Generatiton/Young space。

 

 

  • Eden區是分配對象的區域。

  • Survivor是minor/younger gc後存儲存活對象的區域。

  • Tenured區域存儲長時間存活的對象。

 

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

 

  • 年青代一般使用Copy算法收集,會stop the world

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

 

>>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

  • 垃圾回收的最佳狀態是隻有young gc,也就是避免生命週期很長的對象的存在。

  • 從young gc開始,儘可能給年青代大點的內存,避免full gc

  • 注意Survivor大小

  • 注意內存牆:4G~5G

 

GC日誌簡介

 

 

  • 第一個箭頭:35592K->1814K(36288K),箭頭指向的是新生段的內存佔用狀況; - 第二個箭頭:38508K->7792K(520256K),箭頭指向的是回收後的內存佔用狀況。

  • 垃圾收集停頓時間:0.0336

 

老年代使用建議

 

①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時間

  • 訪問(多線程)對象池須要鎖,可能帶來可擴展性的問題

  • 當心過於頻繁的對象池訪問

 

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


 

  • Java7帶來的內存方面的一個很大的改變就是String常量池從Perm區移動到了Heap中。調用String的intern方法時,若是存在堆中的對象,則會直接保存對象的引用,而不會從新建立對象。

  • Java7正式引入G1垃圾收集器用於替換CMS。

  • Java8中,取消掉了方法區(永久代),使用「元空間」替代,元空間只與系統內存相關。

  • Java 8 update 20所引入的一個很棒的優化就是G1回收器中的字符串去重(String deduplication)。因爲字符串(包括它們內部的char[]數組)佔用了大多數的堆空間,這項新的優化旨在使得G1回收器能識別出堆中那些重複出現的字符串並將它們指向同一個內部的char[]數組,以免同一個字符串的多份拷貝,那樣堆的使用效率會變得很低。可使用-XX:+UseStringDeduplication這個JVM參數來試一下這個特性。

相關文章
相關標籤/搜索