深刻理解JVM——案例實戰(一)

 本文概述
  1. 排查Full Gc的套路是什麼,這裏用一個電商案例來進行說明。
  2. spilt()方法是如何形成內存泄露的?如何經過可視化圖形分析出問題。以及如何從源代碼層面發現根本問題
思惟導圖:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

電商案例-排查Full GC套路

主要業務:

​ 在平常場景進行發郵箱,短信以及APP 的推送消息一些特別活動。java

​ 這種業務的特色是短期以內會有大量的用戶進入APP進行參與,這時候系統的壓力會忽然增長。面試

問題:

​ 在業務流量高峯的時候,CPU的使用率十分十分高,而且直接致使系統卡死,沒法進行任何請求的處理,在系統重啓以後會好一段時間,可是後面又會立刻卡死。算法

初步排查:

  • 首先咱們須要排查是否爲 線程建立過多:線程過多而且併發執行差,因此CPU的上下文切換十分頻繁,壓力很大
  • 頻繁的FULL GC致使系統卡頓

​ 經過這種思路排查,結果果真發現FULL GC的頻率十分高,竟然一分鐘一次FULL GC,頻率實在是過高了。數組

初步排查FULL GC的套路有哪些:(重點)

  1. 內存分配不合理,對象頻繁進入老年代,引起頻繁FULL GC
  2. 內存泄露問題,內存駐留大量的老年代對象,一有對象就會觸發FULL GC,好比以前提到的全表查詢引起海量對象
  3. 永久代的類太多,觸發 FULL GC

繼續排查:

​ 繼續排查發現使用jstat發現並不存在內存不合理的狀況,而且對象也是正常進入老年代,同時永久代的內存竟然也是正常的。緩存

​ 這時候又會考慮一個問題,一分鐘一次FULL GC,證實老年代空間是不夠的,雖然新生代進入老年代是正常的,可是若是老年代 自己對象就很是多,會不會也會出現問題呢?按照這個思路繼續排查,果真發現老年代GC以後 竟然還有那麼多對象存活併發

​ 真相大白,緣由就是老年代被大量對象佔滿了,很容易觸發FULL GC,咱們可使用Jmap的工具排查這裏面的內容,固然,也可使用mat(memory anaylyze tool)進行排查,可是本文不涉及工具的使用介紹,大體介紹一下mat的處理流程:jvm

MAT的排查進程:

jmap -dump:format=b,file=文件名[服務進程ID]

1. 首先內存快照,能夠看到當前內存狀況
2. 其次發現內存泄露
3. 建立的對象佔比量過大
4. 發現緣由是jvm緩存沒有及時進行清理,致使內存愈來愈大
5. 排查結果是本地內存沒有進行限制,同時沒有按期淘汰算法
6. 解決辦法使用一些EHCACASH的緩存便可

解決方式:

  1. 使用JSTAT和JMAP找到讓對象大量建立的緣由
  2. 使用MAT 軟件進行分析
    1. jmap -dump:format=b,file=文件名[服務進程ID]
    2. 使用jhat等可視化圖形工具進行分析。
  3. 解決代碼層面短期大量建立對象的問題。

總結:

​ 其實按照排查思路進行一步步排查,要找到問題其實並非很難。ide

String.split是如何形成內存泄露的

主要業務:

​ 業務就直接跳過,這裏重點關注問題分析和解決流程。工具

問題分析:

  1. 發現也是CPU忽然爆高,可是能夠看到新生代和老年代竟然同時有10G的內存大小
  2. 發現每兩分鐘就會有一次FULL GC同時伴隨着系統的資源高度佔用
  3. 不是簡單的改一下JVM參數就能夠解決的事情,排查發現代碼出了問題。

​ 不用說,標題已經暴露了一切,可是到底是如何分析出來的?這裏也不兜圈子,直接給一張圖,:測試

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

Problem Suspect 1

​ 從這裏看到java.lang.Thread的主線程main 線程,局部變量竟然佔用了**24.97%**的內存的對象。這裏告訴你問題出如今java.lang.Object[]數組,這個數組佔用大量的內存。

​ 在1的下面有一行藍色的 Details,進入以後能夠看到下面的內容:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

Problem Supspect 3裏面也能夠看到這裏面佔用了大量的String對象。

​ 從這裏能夠看到在main線程裏面,有一個arrayList集合佔用了幾乎全部的內存,這個List顯然也是Object[]的數組,而且在內容裏面存在Demo1$Data的對象實例。

​ 從這個分析咱們知道了如何分析出內存佔用的問題,其實大膽猜想加上實用工具測試能夠基本均可以驗證出問題。

trance鏈路追蹤:

​ 知道了佔用是由於Object[]數組的問題,接着來看下鏈路追蹤的狀況:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

​ 如上圖所示,咱們點擊statictrace進入到具體的代碼界面:

​ 答案在最下面的圖:

watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=

​ 咱們能夠明顯的看到是String的問題,經過代碼搜索發現有一個String.split多是產生問題的緣由。

爲何String.split()會形成內存泄露

​ 這裏就涉及一個JDK源代碼的問題了:

​ 在JDK6的版本,一個字符串的底層是基於下面的形式進行存儲的,好比"yes yes yes yes"使用空格切分是以下的形式:

["yes","yes","yes","yes"]

​ 可是到了Jdk7,他給每一個切分出來的字符串都建立了一個新的數組,意思就是說每次切分都切分出一個新的數組,這裏可能無法理解,因此咱們給出代碼:

if (xxxxx)// 一大堆判斷,不用管,總之大部分狀況你都會進這個If判斷
{    
    return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);

​ 這個sublist毫無疑問就是罪魁禍首了,致使JDK版本升級了以後內存佔用爆高也是這個代碼,這個代碼幹了啥呢?

public List<E> subList(int fromIndex, int toIndex) {
    subListRangeCheck(fromIndex, toIndex, size);
    return new SubList(this, 0, fromIndex, toIndex);
}

​ 這個也是典型的面試題,可用看到返回了當前List的視圖,同時這個視圖會隨着數組的改變而改變,關於這個對象細節百度一大堆,這裏不討論,這裏須要關注的是這個new

​ 到這裏相信讀者也清楚爲何split()方法會致使大量的Object[]數組被構建出來,SubList底層依然是一個數組!

解決方式:

​ 說白了仍是代碼的質量問題,不用想能夠知道須要從代碼層面修復問題,解決fot循環裏面的split()方法。

​ 因此字符串的操做尤爲須要謹慎,由於字符串天生的不可變的特性,使用頻率很是高的同時也很容易出現問題。

總結:

​ 這篇文章內容很少,主要爲下面兩個點:

  1. 經過可視化工具以及代碼排查,能夠從分析圖表裏面看到根本的代碼問題點
  2. 關於FULL GC的常見排查討論
 

 

相關文章
相關標籤/搜索