來自:後端技術雜談 _ 颯然Hang
作者:颯然Hang
連結:http://www.rowkey.me/blog/2016/05/07/javamm/(點擊尾部閱讀原文前往)
已獲轉載授權
對於一個Java程式設計師來說,大多數情況下的確是無需對內存的分配、釋放做太多考慮,對Jvm也無需有多麼深的理解的。
但是在寫程序的過程中卻也往往因為這樣而造成了一些不容易察覺到的內存問題,並且在內存問題出現的時候,也不能很快的定位並解決。
因此,了解並掌握Java的內存管理是一個合格的Java程式設計師必需的技能,也只有這樣才能寫出更好的程序,更好地優化程序的性能。
一、背景知識根據網絡可以找到的資料以及筆者能夠打聽到的消息,目前國內外著名的幾個大型網際網路公司的語言選型概括如下:
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來講。
二、Jvm虛擬機內存簡介Java的運行時內存組成如下圖所示:
其中,對於這各個部分有一些是線程私有的,其他則是線程共享的。
線程私有的如下:
①程序計數器
當前線程所執行的字節碼的行號指示器
②Java虛擬機棧
Java方法執行的內存模型,每個方法被執行時都會創建一個棧幀,存儲局部變量表、操作棧、動態連結、方法出口等信息。
每個線程都有自己獨立的棧空間
線程棧只存基本類型和對象地址
方法中局部變量在線程空間中
③本地方法棧
Native方法服務。在HotSpot虛擬機中和Java虛擬機棧合二為一。
線程共享的如下:
①Java堆
存放對象實例,幾乎所有的對象實例以及其屬性都在這裡分配內存。
②方法區
存儲已經被虛擬機加載的類信息、常量、靜態變量、JIT編譯後的代碼等數據。
③運行時常量池
方法區的一部分。用於存放編譯期生成的各種字面量和符號引用。
④直接內存
NIO、Native函數直接分配的堆外內存。DirectBuffer引用也會使用此部分內存。
Java是面向對象的一種程式語言,那麼如何通過引用來訪問對象呢?一般有兩種方式:
①通過句柄訪問
①直接指針
此種方式也是HotSpot虛擬機採用的方式。
在JVM申請內存的過程中,會遇到無法申請到足夠內存,從而導致內存溢出的情況。一般有以下幾種情況:
①虛擬機棧和本地方法棧溢出
②Java堆溢出: 當創建大量對象並且對象生命周期都很長的情況下,會引發OutOfMemoryError
③運行時常量區溢出:OutOfMemoryError:PermGen space,這裡一個典型的例子就是String的intern方法,當大量字符串使用intern時,會觸發此內存溢出
④方法區溢出:方法區存放Class等元數據信息,如果產生大量的類(使用cglib),那麼就會引發此內存溢出,OutOfMemoryError:PermGen space,在使用Hibernate等框架時會容易引起此種情況。
其中,垃圾收集對性能的影響一般有以下幾個:
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主要是利用電腦程式的一個特點,即「越年輕的對象越容易死亡」,也就是存活的越久的對象越有機會存活下去(薑是老的辣)。
吞吐量與訪問時間的關係很複雜,有時可能以響應時間為代價而得到較高的吞吐量,而有時候又要以吞吐量為代價得到較好的響應時間。而在其他情況下,一個單獨的更改可能對兩者都有提高。
通常,平均響應時間越短,系統吞吐量越大;平均響應時間越長,系統吞吐量越小; 但是,系統吞吐量越大, 未必平均響應時間越短;因為在某些情況(例如,不增加任何硬體配置)吞吐量的增大,有時會把平均響應時間作為犧牲,來換取一段時間處理更多的請求。
針對於Java的垃圾回收來說,不同的垃圾回收器會不同程度地影響這兩個指標。例如:並行的垃圾收集器,其保證的是吞吐量,會在一定程度上犧牲響應時間。而並發的收集器,則主要保證的是請求的響應時間。
對於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
對於分代收集,有以下幾個相關理論
HotSpot虛擬機的分代收集,分為一個Eden區、兩個Survivor去以及Old Generation/Tenured區,其中Eden以及Survivor共同組成New Generatiton/Young space。
年青代通常使用Copy算法收集,會stop the world
老年代收集一般採用Mark-sweep-compact, 有可能會stop the world,也可以是concurrent或者部分concurrent。
上圖即為HotSpot虛擬機的垃圾收集器組成。
此收集器的一個工作流程如下如所示:
收集前:
收集後:
對比Serial收集器如下圖所示:
-XX:+UseParallelGC -XX:+UseParallelOldGC啟用此收集器
Server模式的默認老年代收集器
Parallel Scavenge的老年代版本,使用多線程和」mark-sweep」算法
關注點在吞吐量以及CPU資源敏感的場合使用
一般使用Parallel Scavenge + Parallel Old可以達到最大吞吐量保證
並發低停頓收集器
四個步驟:
初始標記 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算法在Java6中還是試驗性質的,在Java7中正式引入,但還未被廣泛運用到生產環境中。它的特點如下:
使用標記-清理算法
需要打開gc日誌並讀懂gc日誌:-XX:PrintHeapAtGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps
垃圾回收的最佳狀態是只有young gc,也就是避免生命周期很長的對象的存在。
從young gc開始,儘量給年青代大點的內存,避免full gc
注意Survivor大小
注意內存牆:4G~5G
①Parallel GC(-XX:+UseParallel[Old]GC)
②確實有必要才改成CMS或G1(for old gen collections)
①小對象allocate的代價很小,通常10個CPU指令;收集掉新對象也非常廉價;不用擔心活的很短的小對象
②大對象分配的代價以及初始化的代價很大;不同大小的大對象可能導致java堆碎片,尤其是CMS, ParallelGC 或 G1還好;儘量避免分配大對象
③避免改變數據結構大小,如避免改變數組或array backed collections / containers的大小;對象構建(初始化)時最好顯式批量定數組大小;改變大小導致不必要的對象分配,可能導致java堆碎片
④對象池可能潛在的問題
Java7帶來的內存方面的一個很大的改變就是String常量池從Perm區移動到了Heap中。調用String的intern方法時,如果存在堆中的對象,則會直接保存對象的引用,而不會重新創建對象。
Java7正式引入G1垃圾收集器用於替換CMS。
Java8中,取消掉了方法區(永久代),使用「元空間」替代,元空間只與系統內存相關。
Java 8 update 20所引入的一個很棒的優化就是G1回收器中的字符串去重(String deduplication)。由於字符串(包括它們內部的char[]數組)佔用了大多數的堆空間,這項新的優化旨在使得G1回收器能識別出堆中那些重複出現的字符串並將它們指向同一個內部的char[]數組,以避免同一個字符串的多份拷貝,那樣堆的使用效率會變得很低。可以使用-XX:+UseStringDeduplication這個JVM參數來試一下這個特性。
本文編號1768,以後想閱讀這篇文章直接輸入1768即可。
●本文分類「Java」,搜索分類名可以獲得相關文章。
●輸入m可以獲取到文章目錄
Java編程↓↓↓
安卓開發↓↓↓
更多推薦請看《15個技術類公眾微信》
涵蓋:程序人生、算法與數據結構、黑客技術與網絡安全、大數據技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。傳播計算機學習經驗、推薦計算機優秀資源:點擊前往《值得關注的15個技術類微信公眾號》