<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          java內(nèi)存泄露排查總結(jié)

          共 14803字,需瀏覽 30分鐘

           ·

          2021-05-11 18:00

          點擊上方藍色字體,選擇“標(biāo)星公眾號”

          優(yōu)質(zhì)文章,第一時間送達

            作者 |  那個少年~

          來源 |  urlify.cn/FvQVzi

          76套java從入門到精通實戰(zhàn)課程分享

          1.內(nèi)存溢出和內(nèi)存泄露

          一種通俗的說法:

          • 內(nèi)存溢出:你申請了10個字節(jié)的空間,但是你在這個空間寫入了11個或者以上字節(jié)的數(shù)據(jù),則出現(xiàn)溢出

          • 內(nèi)存泄露:你用new申請了一塊內(nèi)存,后來很長時間都不使用了,但是因為一直被某個或者某些實例所持有導(dǎo)致GC不能回收掉,也就是該釋放的對象沒有釋放,則出現(xiàn)泄露。

          1.1 內(nèi)存溢出

          java.lang.OutOfMemoryError:是指程序在申請內(nèi)存時,沒有足夠的內(nèi)存空間供其使用,出現(xiàn)OutOfMemoryError。

          產(chǎn)生的原因

          • JVM內(nèi)存過小

          • 程序不嚴(yán)密,產(chǎn)生了過多的垃圾

          程序提現(xiàn)

          • 內(nèi)存中加載的數(shù)據(jù)量過大,如一次性從數(shù)據(jù)庫取出過多數(shù)據(jù)

          • 集合類中有對對象的引用,使用完后沒有清空,是jvm不能回收

          • 代碼中存在死循環(huán)或循環(huán)中產(chǎn)生過多重復(fù)的對象實體

          • 使用第三方軟件中的bug

          • 啟動參數(shù)內(nèi)存值設(shè)定過小

          常見錯誤提示

          • tomcat:java.lang.OutOfMemoryError: PermGen space

          • tomcat:java.lang.OutOfMemoryError: Java heap space

          • weblogic:Root cause of ServletException java.lang.OutOfMemoryError

          • resin:java.lang.OutOfMemoryError

          • java:java.lang.OutOfMemoryError

          解決方法

          • 增加JVM的內(nèi)存大小:對于tomcat容器,找到tomcat在電腦中的安裝目錄,進入這個目錄,然后進入bin目錄中,在window環(huán)境下找到bin目錄中的catalina.bat,在linux環(huán)境下找到catalina.sh。編輯catalina.bat文件,找到JAVA_OPTS(具體來說是 set "JAVA_OPTS=%JAVA_OPTS% %LOGGING_MANAGER%")這個選項的位置,這個參數(shù)是Java啟動的時候,需要的啟動參數(shù)。也可以在操作系統(tǒng)的環(huán)境變量中對JAVA_OPTS進行設(shè)置,因為tomcat在啟動的時候,也會讀取操作系統(tǒng)中的環(huán)境變量的值,進行加載。如果是修改了操作系統(tǒng)的環(huán)境變量,需要重啟機器,再重啟tomcat,如果修改的是tomcat配置文件,需要將配置文件保存,然后重啟tomcat,設(shè)置就能生效了。

          • 優(yōu)化程序,釋放垃圾:主要思路就是避免程序體現(xiàn)上出現(xiàn)的情況。避免死循環(huán),防止一次載入太多的數(shù)據(jù),提高程序健壯型及時釋放。因此,從根本上解決Java內(nèi)存溢出的唯一方法就是修改程序,及時地釋放沒用的對象,釋放內(nèi)存空間。

          1.2 內(nèi)存泄露

          Memory Leak,是指程序在申請內(nèi)存后,無法釋放已申請的內(nèi)存空間,一次內(nèi)存泄露危害可以忽略,但內(nèi)存泄露堆積后果很嚴(yán)重,無論多少內(nèi)存,遲早會被占光。

          在Java中,內(nèi)存泄漏就是存在一些被分配的對象,這些對象有下面兩個特點:

          • 首先,這些對象是可達的,即在有向圖中,存在通路可以與其相連;

          • 其次,這些對象是無用的,即程序以后不會再使用這些對象。

          如果對象滿足這兩個條件,這些對象就可以判定為Java中的內(nèi)存泄漏,這些對象不會被GC所回收,然而它卻占用內(nèi)存。

          關(guān)于內(nèi)存泄露的處理頁就是提高程序的健壯型,因為內(nèi)存泄露是純代碼層面的問題。

          1.3 內(nèi)存溢出和內(nèi)存泄露的聯(lián)系

          內(nèi)存泄露會最終會導(dǎo)致內(nèi)存溢出。
          相同點:都會導(dǎo)致應(yīng)用程序運行出現(xiàn)問題,性能下降或掛起。
          不同點:1) 內(nèi)存泄露是導(dǎo)致內(nèi)存溢出的原因之一,內(nèi)存泄露積累起來將導(dǎo)致內(nèi)存溢出。2) 內(nèi)存泄露可以通過完善代碼來避免,內(nèi)存溢出可以通過調(diào)整配置來減少發(fā)生頻率,但無法徹底避免。

          2、一個Java內(nèi)存泄漏的排查案例

          2.1 確定頻繁的Full GC現(xiàn)象

          首先通過“虛擬機進程狀況工具:jps”找出正在運行的虛擬機進程,最主要是找出這個進程在本地虛擬機的唯一ID(LVMID,Local Virtual Machine Identifier),因為在后面的排查過程中都是需要這個LVMID來確定要監(jiān)控的是哪一個虛擬機進程。
          同時,對于本地虛擬機進程來說,LVMID與操作系統(tǒng)的進程ID(PID,Process Identifier)是一致的,使用Windows的任務(wù)管理器或Unix的ps命令也可以查詢到虛擬機進程的LVMID。
          jps命令格式為:
          jps [ options ] [ hostid ]
          使用命令如下:
          使用jps:
          jps -l
          使用ps:ps aux | grep tomat

          找到你需要監(jiān)控的ID(假設(shè)為20954),再利用“虛擬機統(tǒng)計信息監(jiān)視工具:jstat”監(jiān)視虛擬機各種運行狀態(tài)信息。
          jstat命令格式為:
          jstat [ option vmid [interval[s|ms] [count]] ]
          使用命令如下:
          jstat -gcutil 20954 1000
          意思是每1000毫秒查詢一次,一直查。gcutil的意思是已使用空間站總空間的百分比。
          結(jié)果如下圖:

          查詢結(jié)果表明:這臺服務(wù)器的新生代Eden區(qū)(E,表示Eden)使用了28.30%(最后)的空間,兩個Survivor區(qū)(S0、S1,表示Survivor0、Survivor1)分別是0和8.93%,老年代(O,表示Old)使用了87.33%。程序運行以來共發(fā)生Minor GC(YGC,表示Young GC)101次,總耗時1.961秒,發(fā)生Full GC(FGC,表示Full GC)7次,F(xiàn)ull GC總耗時3.022秒,總的耗時(GCT,表示GC Time)為4.983秒。

          2.2 找出頻繁Full GC的原因

          分析方法通常有兩種:

          • 把堆dump下來在用MAT等工具進行分析,但是dump堆要花較長時間,并且文件巨大,再從服務(wù)器上拖回本地導(dǎo)入工具,這個過程有些折騰,不到萬不得已最好別這么干。

          • 更輕量級的在線分析,使用jmap(java內(nèi)存影響工具)生成堆轉(zhuǎn)存快照(一般稱為headdump或者dump文件)

          jmap命令格式:
          jmap [ option ] vmid
          使用命令如下:
          jmap -histo:live 20954
          查看存活的對象情況,如下圖所示:

          按照一位IT友的說法,數(shù)據(jù)不正常,十有八九就是泄露的。在我這個圖上對象還是挺正常的。

          我在網(wǎng)上找了一位博友的不正常數(shù)據(jù),如下:

          可以看出HashTable中的元素有5000多萬,占用內(nèi)存大約1.5G的樣子。這肯定不正常。

          2.3 定位到代碼

          定位帶代碼,有很多種方法,比如前面提到的通過MAT查看Histogram即可找出是哪塊代碼。——我以前是使用這個方法。也可以使用BTrace,我沒有使用過。

          舉例

          一臺生產(chǎn)環(huán)境機器每次運行幾天之后就會莫名其妙的宕機,分析日志之后發(fā)現(xiàn)在tomcat剛啟動的時候內(nèi)存占用比較少,但是運行個幾天之后內(nèi)存占用越來越大,通過jmap命令可以查詢到一些大對象引用沒有被及時GC,這里就要求解決內(nèi)存泄露的問題。

          Java的內(nèi)存泄露多半是因為對象存在無效的引用,對象得不到釋放,如果發(fā)現(xiàn)Java應(yīng)用程序占用的內(nèi)存出現(xiàn)了泄露的跡象,那么我們一般采用下面的步驟分析:

          1.  用工具生成java應(yīng)用程序的heap dump(如jmap)

          2. 使用Java heap分析工具(如MAT),找出內(nèi)存占用超出預(yù)期的嫌疑對象

          3. 根據(jù)情況,分析嫌疑對象和其他對象的引用關(guān)系。

          4. 分析程序的源代碼,找出嫌疑對象數(shù)量過多的原因。

          以下一步步的按照項目實例來操作,去解決內(nèi)存泄露的問題。

          1. 登錄linux服務(wù)器,獲取tomcat的pid,命令:

          ps -ef|grep java

          2.利用jmap初步分析內(nèi)存映射,命令:

          jmap -histo:live 進程號 | head -7  

          3. 如果上面一步還無法定位到關(guān)鍵信息,那么需要拿到heap dump,生成離線文件,做進一步分析,命令:

          jmap -dump:live,format=b,file=heap.hprof 3514 

          4.拿到heap dump文件,利用eclipse插件MAT來分析heap profile。

          1. 安裝MAT插件

          2. 在eclipse里切換到Memory Analysis視圖

          3. 用MAT打開heap profile文件。

          Memory Analyzer插件下載

          直接看到下面Action窗口,有4種Action來分析heap profile,介紹其中最常用的2種:

          Histogram:這個使用的最多,跟上面的jmap -histo 命令類似,只是在MAT里面可以用GUI來展示應(yīng)用系統(tǒng)各個類產(chǎn)生的實例。

          Shllow Heap排序后發(fā)現(xiàn) Cms_Organization 這個類占用的內(nèi)存比較多(沒有得到及時GC),查看引用:

          分析引用棧,找到無效引用,打開源碼:

          有問題的源碼如下:

          public class RefreshCmsOrganizationStruts implements Runnable{  
            
              private final static Logger logger = Logger.getLogger(RefreshCmsOrganizationStruts.class);  
                
              private List<Cms_Organization> organizations;  
            
              private OrganizationDao organizationDao = (OrganizationDao) WebContentBean  
                      .getInstance().getBean("organizationDao");  
              public RefreshCmsOrganizationStruts(List<Cms_Organization> organizations) {  
                  this.organizations = organizations;  
              }  
            
              public void run() {  
                  Iterator<Cms_Organization> iter = organizations.iterator();  
                  Cms_Organization organization = null;  
                  while (iter.hasNext()) {  
                      organization = iter.next();  
                      synchronized (organization) {  
                          try {  
                              organizationDao.refreshCmsOrganizationStrutsInfo(organization.getOrgaId());  
                              organizationDao.refreshCmsOrganizationResourceInfo(organization.getOrgaId());  
                              organizationDao.sleep();  
                          } catch (Exception e) {  
                              logger.debug("RefreshCmsOrganizationStruts organization = " + organization.getOrgaId(), e);  
                          }  
                      }  
                  }  
              }  
            
          }  

          分析源碼,定時任務(wù)定時調(diào)用,每次調(diào)用生成10個線程處理,而它又使用了非線程安全的List對象,導(dǎo)致List對象無法被GC收集,所以這里將List替換為CopyOnWriteArrayList 。

          Dominator Tree:這個使用的也比較多,顯示大對象的占用率。

          同樣的打開源碼:

          public class CategoryCacheJob extends QuartzJobBean implements StatefulJob {  
                
              private static final Logger LOGGER = Logger.getLogger(CategoryCacheJob.class);  
                
              public static Map<String,List<Cms_Category>> cacheMap = new java.util.HashMap<String,List<Cms_Category>>();  
            
              @Override  
              protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {  
                  try {  
                      //LOGGER.info("======= 緩存編目樹開始 =======");  
                      MongoBaseDao mongoBaseDao = (MongoBaseDao) BeanLocator.getInstance().getBean("mongoBaseDao");  
                      MongoOperations mongoOperations = mongoBaseDao.getMongoOperations();  
                        
                      /* 
                      LOGGER.info("1.緩存基礎(chǔ)教育編目樹"); 
                      Query query = Query.query(Criteria.where("isDel").is("0").and("categoryType").is("F")); 
                      query.sort().on("orderNo", Order.ASCENDING); 
                      List<Cms_Category> list = mongoOperations.find(query, Cms_Category.class); 
                      String key = query.toString().replaceAll("\\{|\\}|\\p{Cntrl}|\\p{Space}"""); 
                      key += "_CategoryCacheJob"
                      cacheMap.put(key, list); 
                      */  
                        
                      //LOGGER.info("2.緩存職業(yè)教育編目樹");  
                      Query query2 = Query.query(Criteria.where("isDel").is("0").and("categoryType").in("JMP","JHP"));  
                      query2.sort().on("orderNo", Order.ASCENDING);  
                      List<Cms_Category> list2 = mongoOperations.find(query2, Cms_Category.class);  
                      String key2 = query2.toString().replaceAll("\\{|\\}|\\p{Cntrl}|\\p{Space}""");  
                      key2 += "_CategoryCacheJob";  
                      cacheMap.put(key2, list2);  
                        
                      //LOGGER.info("3.緩存專題教育編目樹");  
                      Query query3 = Query.query(Criteria.where("isDel").is("0").and("categoryType").is("JS"));  
                      query3.sort().on("orderNo", Order.ASCENDING);  
                      List<Cms_Category> list3 = mongoOperations.find(query3, Cms_Category.class);  
                      String key3 = query3.toString().replaceAll("\\{|\\}|\\p{Cntrl}|\\p{Space}""");  
                      key3 += "_CategoryCacheJob";  
                      cacheMap.put(key3, list3);  
                        
                      //LOGGER.info("======= 緩存編目樹結(jié)束 =======");  
                  } catch(Exception ex) {  
                      LOGGER.error(ex.getMessage(), ex);  
                      LOGGER.info("======= 緩存編目樹出錯 =======");  
                  }  
              }  
            
          }  

          這里的HashMap也有問題:居然使用定時任務(wù),在容器啟動之后定時將數(shù)據(jù)放到Map里面做緩存?這里修改這部分代碼,替換為使用memcached緩存即可。

          內(nèi)存泄漏的原因分析,總結(jié)出來只有一條:存在無效的引用!良好的編碼規(guī)范以及合理使用設(shè)計模式有助于解決此類問題。

          線程安全的CopyOnWriteArrayList介紹

          CopyOnWriteArrayList使用了一種叫寫時復(fù)制的方法,當(dāng)有新元素添加到CopyOnWriteArrayList時,先從原有的數(shù)組中拷貝一份出來,然后在新的數(shù)組做寫操作,寫完之后,再將原來的數(shù)組引用指向到新數(shù)組。

          當(dāng)有新元素加入的時候,如下圖,創(chuàng)建新數(shù)組,并往新數(shù)組中加入一個新元素,這個時候,array這個引用仍然是指向原數(shù)組的。

          當(dāng)元素在新數(shù)組添加成功后,將array這個引用指向新數(shù)組。

          CopyOnWriteArrayList的整個add操作都是在鎖的保護下進行的。 
          這樣做是為了避免在多線程并發(fā)add的時候,復(fù)制出多個副本出來,把數(shù)據(jù)搞亂了,導(dǎo)致最終的數(shù)組數(shù)據(jù)不是我們期望的。

          CopyOnWriteArrayListadd操作的源代碼如下:

           public boolean add(E e) {
              //1、先加鎖
              final ReentrantLock lock = this.lock;
              lock.lock();
              try {
                  Object[] elements = getArray();
                  int len = elements.length;
                  //2、拷貝數(shù)組
                  Object[] newElements = Arrays.copyOf(elements, len + 1);
                  //3、將元素加入到新數(shù)組中
                  newElements[len] = e;
                  //4、將array引用指向到新數(shù)組
                  setArray(newElements);
                  return true;
              } finally {
                 //5、解鎖
                  lock.unlock();
              }
          }

          由于所有的寫操作都是在新數(shù)組進行的,這個時候如果有線程并發(fā)的寫,則通過鎖來控制,如果有線程并發(fā)的讀,則分幾種情況: 
          1、如果寫操作未完成,那么直接讀取原數(shù)組的數(shù)據(jù); 
          2、如果寫操作完成,但是引用還未指向新數(shù)組,那么也是讀取原數(shù)組數(shù)據(jù); 
          3、如果寫操作完成,并且引用已經(jīng)指向了新的數(shù)組,那么直接從新數(shù)組中讀取數(shù)據(jù)。

          可見,CopyOnWriteArrayList的讀操作是可以不用加鎖的。

          CopyOnWriteArrayList的使用場景

          通過上面的分析,CopyOnWriteArrayList有幾個缺點:

          1. 由于寫操作的時候,需要拷貝數(shù)組,會消耗內(nèi)存,如果原數(shù)組的內(nèi)容比較多的情況下,可能導(dǎo)致yong gc或者full gc

          2. 不能用于實時讀的場景,像拷貝數(shù)組,新增元素都需要時間,所以調(diào)用一個set操作后,讀取到的數(shù)據(jù)可能還是舊的,雖然CopyOnWriteArrayList 能做到最終一致性,但是還無法滿足實時性的要求

           

          CopyOnWriteArrayList 合適讀多寫少的場景,不過這類慎用 ,因為誰也沒法保證CopyOnWriteArrayList 到底要放置多少數(shù)據(jù),萬一數(shù)據(jù)稍微有點多,每次add/set都要重新復(fù)制數(shù)組,這個代價實在太高昂了。在高性能的互聯(lián)網(wǎng)應(yīng)用中,這種操作分分鐘引起故障。





          鋒哥最新SpringCloud分布式電商秒殺課程發(fā)布

          ??????

          ??長按上方微信二維碼 2 秒





          感謝點贊支持下哈 

          瀏覽 70
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人AV十八 亚洲二区 | 一区二区三区四区高清无码 | www.99在线视频 | a在线级电影网站 | 激情乱伦视频 |