[Full GC 121376K->10414K(130112K), 0.0650971 secs]html
[GC [DefNew: 8614K->8614K(9088K), 0.0000665 secs][Tenured: 112761K->10414K(121024K), 0.0433488 secs] 121376K->10414K(130112K), 0.0436268 secs]java
傳統分代垃圾回收方式,已經在必定程度上把垃圾回收給應用帶來的負擔降到了最小,把應用的吞吐量推到了一個極限。可是他沒法解決的一個問題,就是Full GC所帶來的應用暫停。在一些對實時性要求很高的應用場景下,GC暫停所帶來的請求堆積和請求失敗是沒法接受的。這類應用可能要求請求的返回時間在幾百甚至幾十毫秒之內,若是分代垃圾回收方式要達到這個指標,只能把最大堆的設置限制在一個相對較小範圍內,可是這樣有限制了應用自己的處理能力,一樣也是不可接受的。mysql
分代垃圾回收方式確實也考慮了實時性要求而提供了併發回收器,支持最大暫停時間的設置,可是受限於分代垃圾回收的內存劃分模型,其效果也不是很理想。web
爲了達到實時性的要求(其實Java語言最初的設計也是在嵌入式系統上的),一種新垃圾回收方式呼之欲出,它既支持短的暫停時間,又支持大的內存空間分配。能夠很好的解決傳統分代方式帶來的問題。算法
增量收集的方式在理論上能夠解決傳統分代方式帶來的問題。增量收集把對堆空間劃分紅一系列內存塊,使用時,先使用其中一部分(不會所有用完),垃圾收集時把以前用掉的部分中的存活對象再放到後面沒有用的空間中,這樣能夠實現一直邊使用邊收集的效果,避免了傳統分代方式整個使用完了再暫停的回收的狀況。sql
固然,傳統分代收集方式也提供了併發收集,可是他有一個很致命的地方,就是把整個堆作爲一個內存塊,這樣一方面會形成碎片(沒法壓縮),另外一方面他的每次收集都是對整個堆的收集,沒法進行選擇,在暫停時間的控制上仍是很弱。而增量方式,經過內存空間的分塊,偏偏能夠解決上面問題。數據庫
這部分的內容主要參考這裏,這篇文章算是對G1算法論文的解讀。我也沒加什麼東西了。編程
從設計目標看G1徹底是爲了大型應用而準備的。緩存
支持很大的堆服務器
高吞吐量
-- 支持多CPU和垃圾回收線程
-- 在主線程暫停的狀況下,使用並行收集
-- 在主線程運行的狀況下,使用併發收集
實時目標:可配置在N毫秒內最多隻佔用M毫秒的時間進行垃圾回收
固然G1要達到實時性的要求,相對傳統的分代回收算法,在性能上會有一些損失。
圖1 G1收集器
G1可謂博採衆家之長,力求到達一種完美。他吸收了增量收集優勢,把整個堆劃分爲一個一個等大小的區域(region)。內存的回收和劃分都以region爲單位;同時,他也吸收了CMS的特色,把這個垃圾回收過程分爲幾個階段,分散一個垃圾回收過程;並且,G1也認同分代垃圾回收的思想,認爲不一樣對象的生命週期不一樣,能夠採起不一樣收集方式,所以,它也支持分代的垃圾回收。爲了達到對回收時間的可預計性,G1在掃描了region之後,對其中的活躍對象的大小進行排序,首先會收集那些活躍對象小的region,以便快速回收空間(要複製的活躍對象少了),由於活躍對象小,裏面能夠認爲多數都是垃圾,因此這種方式被稱爲Garbage First(G1)的垃圾回收算法,即:垃圾優先的回收。
回收步驟:
初始標記(Initial Marking)
G1對於每一個region都保存了兩個標識用的bitmap,一個爲previous marking bitmap,一個爲next marking bitmap,bitmap中包含了一個bit的地址信息來指向對象的起始點。
開始Initial Marking以前,首先併發的清空next marking bitmap,而後中止全部應用線程,並掃描標識出每一個region中root可直接訪問到的對象,將region中top的值放入next top at mark start(TAMS)中,以後恢復全部應用線程。
觸發這個步驟執行的條件爲:
G1定義了一個JVM Heap大小的百分比的閥值,稱爲h,另外還有一個H,H的值爲(1-h)*Heap Size,目前這個h的值是固定的,後續G1也許會將其改成動態的,根據jvm的運行狀況來動態的調整,在分代方式下,G1還定義了一個u以及soft limit,soft limit的值爲H-u*Heap Size,當Heap中使用的內存超過了soft limit值時,就會在一次clean up執行完畢後在應用容許的GC暫停時間範圍內儘快的執行此步驟;
在pure方式下,G1將marking與clean up組成一個環,以便clean up能充分的使用marking的信息,當clean up開始回收時,首先回收可以帶來最多內存空間的regions,當通過屢次的clean up,回收到沒多少空間的regions時,G1從新初始化一個新的marking與clean up構成的環。
併發標記(Concurrent Marking)
按照以前Initial Marking掃描到的對象進行遍歷,以識別這些對象的下層對象的活躍狀態,對於在此期間應用線程併發修改的對象的以來關係則記錄到remembered set logs中,新建立的對象則放入比top值更高的地址區間中,這些新建立的對象默認狀態即爲活躍的,同時修改top值。
最終標記暫停(Final Marking Pause)
當應用線程的remembered set logs未滿時,是不會放入filled RS buffers中的,在這樣的狀況下,這些remebered set logs中記錄的card的修改就會被更新了,所以須要這一步,這一步要作的就是把應用線程中存在的remembered set logs的內容進行處理,並相應的修改remembered sets,這一步須要暫停應用,並行的運行。
存活對象計算及清除(Live Data Counting and Cleanup)
值得注意的是,在G1中,並非說Final Marking Pause執行完了,就確定執行Cleanup這步的,因爲這步須要暫停應用,G1爲了可以達到準實時的要求,須要根據用戶指定的最大的GC形成的暫停時間來合理的規劃何時執行Cleanup,另外還有幾種狀況也是會觸發這個步驟的執行的:
G1採用的是複製方法來進行收集,必須保證每次的」to space」的空間都是夠的,所以G1採起的策略是當已經使用的內存空間達到了H時,就執行Cleanup這個步驟;
對於full-young和partially-young的分代模式的G1而言,則還有狀況會觸發Cleanup的執行,full-young模式下,G1根據應用可接受的暫停時間、回收young regions須要消耗的時間來估算出一個yound regions的數量值,當JVM中分配對象的young regions的數量達到此值時,Cleanup就會執行;partially-young模式下,則會盡可能頻繁的在應用可接受的暫停時間範圍內執行Cleanup,並最大限度的去執行non-young regions的Cleanup。
之後JVM的調優或許跟多須要針對G1算法進行調優了。
主要有Jconsole,jProfile,VisualVM。
Jconsole : jdk自帶,功能簡單,可是能夠在系統有必定負荷的狀況下使用。對垃圾回收算法有很詳細的跟蹤。詳細說明參考這裏
JProfiler:商業軟件,須要付費。功能強大。詳細說明參考這裏
VisualVM:JDK自帶,功能強大,與JProfiler相似。推薦。
觀察內存釋放狀況、集合類檢查、對象樹
上面這些調優工具都提供了強大的功能,可是總的來講通常分爲如下幾類功能
圖2 查看堆信息
可查看堆空間大小分配(年輕代、年老代、持久代分配)。
提供即時的垃圾回收功能。
垃圾監控(長時間監控回收狀況)。
查看堆內類、對象信息查看:數量、類型等。
圖4 對象引用狀況
對象引用狀況查看。
有了堆信息查看方面的功能,咱們通常能夠順利解決如下問題:
-- 年老代年輕代大小劃分是否合理
-- 內存泄漏
-- 垃圾回收算法設置是否合理
圖5 線程監控信息
線程信息監控:系統線程數量。
線程狀態監控:各個線程都處在什麼樣的狀態下。
圖6 線程轉儲信息
Dump線程詳細信息:查看線程內部運行狀況。
死鎖檢查 。
圖7 熱點分析
CPU熱點:檢查系統哪些方法佔用的大量CPU時間。
內存熱點:檢查哪些對象在系統中數量最大(必定時間內存活對象和銷燬對象一塊兒統計)。
這兩個東西對於系統優化頗有幫助。咱們能夠根據找到的熱點,有針對性的進行系統的瓶頸查找和進行系統優化,而不是漫無目的的進行全部代碼的優化。
快照是系統運行到某一時刻的一個定格。在咱們進行調優的時候,不可能用眼睛去跟蹤全部系統變化,依賴快照功能,咱們就能夠進行系統兩個不一樣運行時刻,對象(或類、線程等)的不一樣,以便快速找到問題。
舉例說,我要檢查系統進行垃圾回收之後,是否還有該收回的對象被遺漏下來的了。那麼,我能夠在進行垃圾回收先後,分別進行一次堆狀況的快照,而後對比兩次快照的對象狀況。
內存泄漏是比較常見的問題,並且解決方法也比較通用,這裏能夠重點說一下,而線程、熱點方面的問題則是具體問題具體分析了。
內存泄漏通常能夠理解爲系統資源(各方面的資源,堆、棧、線程等)在錯誤使用的狀況下,致使使用完畢的資源沒法回收(或沒有回收),從而致使新的資源分配請求沒法完成,引發系統錯誤。
內存泄漏對系統危害比較大,由於他能夠直接致使系統的崩潰。
須要區別一下,內存泄漏和系統超負荷二者是有區別的,雖然可能致使的最終結果是同樣的。內存泄漏是用完的資源沒有回收引發錯誤,而系統超負荷則是系統確實沒有那麼多資源能夠分配了(其餘的資源都在使用)。
異常: java.lang.OutOfMemoryError: Java heap space
說明:
圖8 堆空間慢慢消耗盡
這是最典型的內存泄漏方式,簡單說就是全部堆空間都被沒法回收的垃圾對象佔滿,虛擬機沒法再在分配新空間。
如上圖所示,這是很是典型的內存泄漏的垃圾回收狀況圖。全部峯值部分都是一次垃圾回收點,全部谷底部分表示是一次垃圾回收後剩餘的內存。鏈接全部谷底的點,能夠發現一條由底到高的線,這說明,隨時間的推移,系統的堆空間被不斷佔滿,最終會佔滿整個堆空間。所以能夠初步認爲系統內部可能有內存泄漏。(上面的圖僅供示例,在實際狀況下收集數據的時間須要更長,好比幾個小時或者幾天)
解決:
這種方式解決起來也比較容易,通常就是根據垃圾回收先後狀況對比,同時根據對象引用狀況(常見的集合對象引用)分析,基本均可以找到泄漏點。
異常:java.lang.OutOfMemoryError: PermGen space
說明:
Perm空間被佔滿。沒法爲新的class分配存儲空間而引起的異常。這個異常之前是沒有的,可是在Java反射大量使用的今天這個異常比較常見了。主要緣由就是大量動態反射生成的類不斷被加載,最終致使Perm區被佔滿。
更可怕的是,不一樣的classLoader即使使用了相同的類,可是都會對其進行加載,至關於同一個東西,若是有N個classLoader那麼他將會被加載N次。所以,某些狀況下,這個問題基本視爲無解。固然,存在大量classLoader和大量反射類的狀況其實也很少。
解決:
1. -XX:MaxPermSize=16m
2. 換用JDK。好比JRocket。
異常:java.lang.StackOverflowError
說明:這個就很少說了,通常就是遞歸沒返回,或者循環調用形成
異常:Fatal: Stack size too small
說明:java中一個線程的空間大小是有限制的。JDK5.0之後這個值是1M。與這個線程相關的數據將會保存在其中。可是當線程空間滿了之後,將會出現上面異常。
解決:增長線程棧大小。-Xss2m。但這個配置沒法解決根本問題,還要看代碼部分是否有形成泄漏的部分。
異常:java.lang.OutOfMemoryError: unable to create new native thread
說明:
這個異常是因爲操做系統沒有足夠的資源來產生這個線程形成的。系統建立線程時,除了要在Java堆中分配內存外,操做系統自己也須要分配資源來建立線程。所以,當線程數量大到必定程度之後,堆中或許還有空間,可是操做系統分配不出資源來了,就出現這個異常了。
分配給Java虛擬機的內存愈多,系統剩餘的資源就越少,所以,當系統內存固定時,分配給Java虛擬機的內存越多,那麼,系統總共可以產生的線程也就越少,二者成反比的關係。同時,能夠經過修改-Xss來減小分配給單個線程的空間,也能夠增長系統總共內生產的線程數。
解決:
1. 從新設計系統減小線程數量。
2. 線程數量不能減小的狀況下,經過-Xss減少單個線程大小。以便能生產更多的線程。
所謂「成也蕭何敗蕭何」。Java的垃圾回收確實帶來了不少好處,爲開發帶來了便利。可是在一些高性能、高併發的狀況下,垃圾回收確成爲了制約Java應用的瓶頸。目前JDK的垃圾回收算法,始終沒法解決垃圾回收時的暫停問題,由於這個暫停嚴重影響了程序的相應時間,形成擁塞或堆積。這也是後續JDK增長G1算法的一個重要緣由。
固然,上面是從技術角度出發解決垃圾回收帶來的問題,可是從系統設計方面咱們就須要問一下了:
1. 咱們須要分配如此大的內存空間給應用嗎?
2. 咱們是否可以經過有效使用內存而不是經過擴大內存的方式來設計咱們的系統呢?
內存中須要放什麼呢?我的認爲,內存中須要放的是你的應用須要在不久的未來再次用到到的東西。想一想看,若是你在未來不用這些東西,何須放內存呢?放文件、數據庫不是更好?這些東西通常包括:
1. 系統運行時業務相關的數據。好比web應用中的session、即時消息的session等。這些數據通常在一個用戶訪問週期或者一個使用過程當中都須要存在。
2. 緩存。緩存就比較多了,你所要快速訪問的均可以放這裏面。其實上面的業務數據也能夠理解爲一種緩存。
3. 線程。
所以,咱們是否是能夠這麼認爲,若是咱們不把業務數據和緩存放在JVM中,或者把他們獨立出來,那麼Java應用使用時所需的內存將會大大減小,同時垃圾回收時間也會相應減小。
我認爲這是可能的。
把全部數據都放入數據庫或者文件系統,這是一種最爲簡單的方式。在這種方式下,Java應用的內存基本上等於處理一次峯值併發請求所需的內存。數據的獲取都在每次請求時從數據庫和文件系統中獲取。也能夠理解爲,一次業務訪問之後,全部對象均可以進行回收了。
這是一種內存使用最有效的方式,可是從應用角度來講,這種方式很低效。
上面的問題是由於咱們使用了文件系統帶來了低效。可是若是咱們不是讀寫硬盤,而是寫內存的話效率將會提升不少。
數據庫和文件系統都是實實在在進行了持久化,可是當咱們並不須要這樣持久化的時候,咱們能夠作一些變通——把內存當硬盤使。
內存-硬盤映射很好很強大,既用了緩存又對Java應用的內存使用又沒有影響。Java應用仍是Java應用,他只知道讀寫的仍是文件,可是其實是內存。
這種方式兼得的Java應用與緩存兩方面的好處。memcached的普遍使用也正是這一類的表明。
這也是一種很好的方式,能夠分爲縱拆和橫拆。縱拆能夠理解爲把Java應用劃分爲不一樣模塊,各個模塊使用一個獨立的Java進程。而橫拆則是一樣功能的應用部署多個JVM。
經過部署多個JVM,能夠把每一個JVM的一個垃圾回收控制在能夠忍受的範圍內便可。可是這至關於進行了分佈式的處理,其額外帶來的複雜性也是須要評估的。另外,也有支持分佈式的這種JVM能夠考慮,不要要錢哦:)
這種方式是理想當中的方式,目前的虛擬機尚未,純屬假設。即:考慮由編程方式配置哪些對象在垃圾收集過程當中能夠直接跳過,減小垃圾回收線程遍歷標記的時間。
這種方式至關於在編程的時候告訴虛擬機某些對象你能夠在某個時間後再進行收集或者由代碼標識能夠收集了(相似C、C++),在這以前你即使去遍歷他也是沒有效果的,他確定是還在被引用的。
這種方式若是JVM能夠實現,我的認爲將是一個飛躍,Java即有了垃圾回收的優點,又有了C、C++對內存的可控性。
Java的阻塞式的線程模型基本上能夠拋棄了,目前成熟的NIO框架也比較多了。阻塞式IO帶來的問題是線程數量的線性增加,而NIO則能夠轉換成爲常數線程。所以,對於服務端的應用而言,NIO仍是惟一選擇。不過,JDK7中爲咱們帶來的AIO是否能讓人眼前一亮呢?咱們拭目以待。
本文說的都是Sun的JDK,目前常見的JDK還有JRocket和IBM的JDK。其中JRocket在IO方面比Sun的高不少,不過Sun JDK6.0之後提升也很大。並且JRocket在垃圾回收方面,也具備優點,其可設置垃圾回收的最大暫停時間也是很吸引人的。不過,Sun的G1實現之後,在這方面會有一個質的飛躍。
能整理出上面一些東西,也是由於站在巨人的肩上。下面是一些參考資料,供你們學習,你們有更好的,能夠繼續完善:)
· Java SE 6 HotSpot[tm] Virtual Machine Garbage Collection Tuning
· Hotspot memory management whitepaper
· Diagnosing a Garbage Collection problem
· Garbage-First Garbage Collection
· Frequently Asked Questions about Garbage Collection in the HotspotTM JavaTM Virtual Machine
· 《深刻Java虛擬機》。雖然過去了不少年,但這本書依舊是經典。