jvm內存分配策略和性能監控

概述

本篇旨在講清楚jvm的內存分配策略,gc日誌閱讀,一些常見名詞和jdk提供的一些性能監控工具。廢話很少說,開始上貨。java

GC日誌閱讀

在開發的世界裏,閱讀日誌是最基礎的能力,也是解決問題重要的工具。一樣閱讀gc日誌也是解決虛擬機內存的基礎技能,經過配置參數-XX:+PrintGCDetails就能夠打印gc日誌,建議加上參數-Xloggc指定gc日誌目錄,避免gc日誌和console控制檯日誌混亂形成的閱讀困難。
每一種收集器的日誌都會略有不一樣,但會維持必定的共性,如下面一段日誌爲例:程序員

0.332: [GC (Allocation Failure) [PSYoungGen: 6120K->504K(6144K)] 12535K->12549K(19968K), 0.0066909 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.339: [Full GC (Ergonomics) [PSYoungGen: 504K->0K(6144K)] [ParOldGen: 12045K->10615K(13824K)] 12549K->10615K(19968K), [Metaspace: 3473K->3473K(1056768K)], 0.1372999 secs] [Times: user=0.28 sys=0.00, real=0.14 secs]

最前面的0.332和0.339表明了gc的發生的時間,它的含義是表達虛擬機啓動到發生gc的秒數。後面的GC和Full GC表明着垃圾收集的停頓類型,若是是GC表明的是新生代的GC,也稱ygc和minor gc,fullgc表明的是對整堆的一個gc。後面括號裏的Allocation Failure和Ergonomics表明的是發生gc的緣由,分別是eden區域空間不夠和parOldGen空間不夠致使的gc和fullgc問題。以Full GC爲例,接下來的[PSYoungGen、[ParOldGen、[Metaspace表明gc發生的區域,分別是年輕代、老年代、元空間,其名字也是由所使用的gc收集器密切相關,大體以下:算法

收集器                    顯示區域
serial                   DefNew
ParNew                   ParNew
Parallel Scavenge        PSYoungGen
serial old               Tenured
parallel old             ParOldGen
CMS                      CMS

後面方括號內部的 504K->0K(6144K)表明着該區域GC前使用容量-》GC後該區域所使用容量(該區域總容量),方括號以外的12549K->10615K(19968K)則表明gc以前堆中使用容量-》gc後堆中使用容量(堆總容量)。0.1372999 secs這個很簡單,表明gc佔用時間,單位是秒。小程序

內存分配與回收策略

  • 對象優先在Eden分配
    大多數狀況下,對象優先在新生代Eden區中分配。當Eden區域沒有足夠空間進行分配時,將發生一次Minor GC。虛擬機提供了-XX:+PrintGCDetails用來輸出gc日誌,此日誌會告訴咱們垃圾收集行爲時的內存日誌,並在進程結束後輸出當前內存各區域的分配狀況。上例子:微信小程序

    public class TestAllocation {
    
        private static final int _1MB=1024*1024;
    
        public static void main(String[] args) {
            byte[] a1,a2,a3,a4;
            a1=new byte[2*_1MB];
            a2=new byte[2*_1MB];
            a3=new byte[2*_1MB];
            a4=new byte[4*_1MB];
        }
    }

    gc日誌以下所示:數組

"C:\Program Files\Java\jdk1.8.0_151\bin\java" -XX:+PrintGCDetails -Xmx20m -Xms20m -Xmn10m 
[GC (Allocation Failure) [PSYoungGen: 6294K->808K(9216K)] 6294K->4912K(19456K), 0.0023349 secs] [Times: user=0.09 sys=0.00, real=0.02 secs] 
Heap
 PSYoungGen      total 9216K, used 7273K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 78% used [0x00000000ff600000,0x00000000ffc50670,0x00000000ffe00000)
  from space 1024K, 78% used [0x00000000ffe00000,0x00000000ffeca020,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 ParOldGen       total 10240K, used 4104K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 40% used [0x00000000fec00000,0x00000000ff002020,0x00000000ff600000)
 Metaspace       used 3473K, capacity 4496K, committed 4864K, reserved 1056768K
  class space    used 381K, capacity 388K, committed 512K, reserved 1048576K

其中vm配置參數從第一行即可知。新生代分爲eden區域和兩塊survior區域,默認比例爲8:1:1,從圖中eden:from:to=8192:1024:1024能夠獲得驗證。下來咱們就根據gc日誌分析下在執行這段程序時,jvm究竟都作了哪些事。
圖片描述
從這張圖片可知,在jdk1.8.0_151中,即便跑一個空的main函數,新生代就要佔2362k,這個是虛擬機的初始內存佔用,好奇寶寶能夠經過jmap命令看看到底堆裏裝了什麼。如今讓咱們回到最開始代碼中,會將a一、a2分配在新生代的eden區,此時eden區域爲2048k+2048K+2362k=6458k,由於eden區域空間不夠,不足以將a3裝入,此時觸發minor gc,又由於此時a一、a2對象還存活,suivor區域只有1024k,故將a一、a2分配擔保到老年代。從日誌中可知,經歷過一次minor gc新生代還有808k的存活對象,由於a一、a2已經擔保到老年代,故這是初始內存中通過gc存活的對象,經過複製算法轉移到survivor中。此時eden區域是0k,其中一塊survivor是初始內存,老年代存放着a一、a2對象,此時開始繼續分配對象內存,因a3+a4<eden區域,故所有分配在eden區域。瀏覽器

  • 大對象直接進入老年代
    哪怕你歷來沒有學習過jvm知識,你或許也據說過江湖上流傳着大對象直接進入老年代這個傳聞。不少人都知道這個知識點,但恐怕大多數人並不能準確的去描述這個分配策略。
    1.何謂大對象?
    所謂大對象就是須要大量連續內存空間的對象,上個例子中的byte數組就是典型的大對象。
    2.參數-XX:PretenureSizeThreshold的做用?
    虛擬機提供了-XX:PretenureSizeThreshold,令大於這個值得對象直接在老年代分配。hotspot能夠在年輕代手機內存的收集器有Serial、ParNew、Parallel Scavenge以及G1(G1劃份內存區域比較特殊暫不考慮)。其中只有Serial和ParNew收集器能夠識別這個參數,Parallel Scavenge是不識別這個參數的,但並非大對象直接進入老年代分配策略對其就是無效的,在Parallel Scavenge中自有它的實現,大約等於Eden區域一半的對象會被認成大對象。感興趣的能夠來這看看,傳送門:連接描述.若是想要使用-XX:PretenureSizeThreshold參數,能夠考慮使用ParNew+CMS的組合。給你們展現一個例子:安全

    public class BigObject {
    
        public static void main(String[] args) {
            byte[] test = new byte[4*1024*1024];
        }
    }

    圖片描述
    從gc日誌,咱們很容易得出大對象進入老年代這個結論。對了,還須要注意的是XX:PretenureSizeThreshold的單位是k,不能像-Xmx3mb這樣直接指定。
    3.爲何大對象要進入老年代?
    在搞清楚這個問題以前,咱們首先要去揣摩大師們設計分代算法的意圖。設計師們但願新生代的對象多數是朝生夕滅的,故新生代採用複製算法最合適。複製算法的優勢是簡單,速度快(在存活對象少的狀況下),缺點是佔內存要發生內存複製。這樣作的目的就是避免在eden區和兩個survivor區之間發生大量的內存複製。服務器

  • 長期存活的對象將進入老年代
    虛擬機給每一個對象定義了一個對象年齡計數器,保存在對象頭中的Mark word部分。若是對象在eden出生,經歷過一次minor gc仍然活着,而且能被survivor區容納,將會被移動到survivor區域,而且將gc年齡設置爲1,這種對象沒經歷一次Minor gc ,年齡就增長一歲,當它的年齡增長到必定程度(默認是15歲),就會被晉升到老年代,這個年齡閾值能夠經過參數-XX:MaxTenuringThresold來設置.
  • 動態對象年齡斷定
    虛擬機並非永遠要求對象的年齡永遠達到MaxTenuringThresold才能晉升老年代,若是在survivor空間中相同年齡全部的對象的大小總和大於survivor空間的一半,年齡大於或等於改年齡的對象直接進入老年代,無須等到MaxTenuringThresold要求的年齡。
  • 空間分配擔保
    在發生minor gc之間,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代全部對象總空間,若是條件成立,則表明此次gc是安全的。若是不成立,jdk1.7和jdk1.8的版本中會繼續檢查老年代最大的可用空間是否大於歷次晉升到老年代對象的平均大小,若是小於則進行一次full gc,若是大於則嘗試進行一次minor gc,若是出現老年代擔保失敗的狀況則會進行一次full gc。

性能監控與故障處理工具

給一個系統定位問題的時候,知識、經驗是關鍵基礎,數據是依據,工具是運用知識處理數據的手段。java開發程序員應該都知道jdk的bin目錄下有java.exe和javac.exe,但其實bin下面還有不少的小工具都極其實用,是咱們排查問題的關鍵。工具不少,咱們挑幾個最經常使用的來講明:微信

  • jstat

能夠用來監視虛擬機各類運行時狀態信息的命令行工具。jstat格式命令爲:

jstat [ option vmid [interval[s|ms] [count]] ]

option表明咱們想要查詢的虛擬機信息,vmid就是咱們的進程id,interval和count表明查詢間隔(默認單位是ms)和次數。jstat工具主要選項以下圖所示:
圖片描述
gccause是最經常使用的一個option之一,假如咱們須要每250ms查詢一次進程5888的垃圾收集狀況,一共查詢20次,命令應該爲:

圖片描述

S0和S1對應的是新生代的兩塊suivor區域,E對應的是Eden,O對應的是老年代,M對應的是Klass Metaspace以及Noklass Metaspace二者總共的使用率,CCS對應的是NoKlass Metaspace的使用率,YGC表示的是新生代gc發生的次數,YGCT表示的是新生代gc總共的stop the world的時間,FGC表示的是Full gc的次數,FGCT GCT同YGCT同樣就很少說了,LGCC表示上次gc的緣由,GCC表示這次gc的緣由。須要額外注意的是,當使用CMS做爲老年代收集器的時候,每執行一次Old GC,FGC就會增長兩次。

  • jinfo

:能夠用來實時查看和調整虛擬機的各項參數。
使用jps -v pid能夠查看虛擬機啓動時顯示指定的參數,前提是你要開rmi。若是想要查看未被指定參數的默認值,除了查文檔就只能經過jinfo -flag 選項進行查詢,固然若是你使用java -XX:+PrintFlagsFinal查看默認值也是一個很好的選擇(此命令會打印出全部的默認值),同時你還能夠經過jinfo -flag [+|-] name=value或者-flag name=value修改一部分運行期可寫的虛擬機參數,若是想要查看那些事運行期可寫的參數,能夠經過命令java -XX:+PrintFlagsFinal |grep manageable查詢。
圖片描述

  • jmap

jmap命令主要用於生成堆轉儲快照,通常稱爲dump文件。固然它的做用不只僅只有這個,還有查詢java堆和方法區的詳細信息.生成dump文件。其使用方法以下:
圖片描述
生成堆轉儲快照: jmap -dump:format=b,file=gc.bin pid(線上慎用,若是堆文件較大,會比較耗時,由於要保證dump的準確性,會發生stop the world)
查看堆詳細信息,使用回收期、參數配置和分代情況等:jmap -heap pid
查看對象統計信息:jmap -histo pid(若是加上參數:live,jmap -histo:live pid會率先執行一次gc,線上慎用)

  • jhat

jhat命令可與jmap搭配使用,用來分析dump文件,其內置了一個http服務器,生成分析結果,能夠在瀏覽器查看。不過通常都不會用jhat來分析,緣由有兩點1:不會直接在部署服務器上分析,由於分析工做是一個耗時且消耗硬件資源的過程。2:由於jhat分析確實比較簡陋,目前eclispe的mat是分析dump文件最專業的工具,能夠到eclispe官網下載插件版,無須安裝eclispe。

  • jstack

jstack命令用於生成虛擬機當前時刻的線程快照,通常稱爲threaddump或javacore文件。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,主要用來定位線程出現長時間停頓的緣由,如線程間死鎖、死循環、請求外部資源長時間等待等都是致使線程長時間停頓的主要緣由。
圖片描述

在jdk1.5中, Thread類新增了一個getAllStackTraces()方法用於獲取虛擬機中全部的StackTraceElement對象,使用這個方法能夠完成jstack的大部分功能,能夠在實際的項目中用這個方法作個管理頁面,來隨時監控線程堆棧。

  • VisualVM

VisualVmM是jdk發佈到目前爲止,最強大的運行監視和故障處理數據,他有一個很大的優勢不須要被監視的程序基於特殊的agent運行,所以它對應用程序地實際性能影響很小,使得它能夠直接應用到生產環境中。它上面有許多好玩的插件,好比Btrace,它但是線上調試的神奇,能夠在不中止目標程序運行的前提下,經過hotspot的hotswap技術動態加入本來不存在的調試代碼。這個插件等之後有機會了再具體介紹下。

總結

基本上,hotspot系列jvm的理論知識和經常使用性能監控工具已經介紹完畢,但這些知識只是jvm世界的一點皮毛,若是你們想要學習更多的知識,能夠去關注R大,被稱爲jvm源碼化身的男人。傳送門:https://www.zhihu.com/people/...。另外阿里的你假笨同窗開發了兩個特別實用的程序,微信小程序JVMPocket和網站:http://xxfox.perfma.com/,至於它們有什麼功效,你們本身去體驗吧^_^。

相關文章
相關標籤/搜索