使用jmap和MAT分析JVM堆記憶體
我的一臺生產環境機器每次執行幾天之後就會莫名其妙的宕機,分析日誌之後發現在tomcat剛啟動的時候記憶體佔用比較少,但是執行個幾天之後記憶體佔用越來越大,通過jmap命令可以查詢到一些大物件引用沒有被及時GC,這裡就要求解決記憶體洩露的問題。
Java的記憶體洩露多半是因為物件存在無效的引用,物件得不到釋放,如果發現Java應用程式佔用的記憶體出現了洩露的跡象,那麼我們一般採用下面的步驟分析: 1. 用工具生成java應用程式的heap dump(如jmap) 2. 使用Java heap分析工具(如MAT),找出記憶體佔用超出預期的嫌疑物件 3. 根據情況,分析嫌疑物件和其他物件的引用關係。 4. 分析程式的原始碼,找出嫌疑物件數量過多的原因。
以下一步步的按照專案例項來操作,去解決記憶體洩露的問題。
1. 登入linux伺服器,獲取tomcat的pid,命令:
ps -ef|grep java
2. 利用jmap初步分析記憶體對映,命令:
jmap -histo:live 3514 | head -7
第2行是我們業務系統的物件,通過這個物件的引用可以初步分析出到底是哪裡出現了引用未被垃圾回收收集,通知開發人員優化相關程式碼。
3. 如果上面一步還無法定位到關鍵資訊,那麼需要拿到heap dump,生成離線檔案,做進一步分析,命令:
jmap -dump:live,format=b,file=heap.hprof 3514
4. 拿到heap dump檔案,利用eclipse外掛MAT來分析heap profile。
a. 安裝MAT外掛
b. 在eclipse裡切換到Memory Analysis檢視
c. 用MAT開啟heap profile檔案。
直接看到下面Action視窗,有4種Action來分析heap profile,介紹其中最常用的2種:
- Histogram:這個使用的最多,跟上面的jmap -histo 命令類似,只是在MAT裡面可以用GUI來展示應用系統各個類產生的例項。
Shllow Heap排序後發現 Cms_Organization 這個類佔用的記憶體比較多(沒有得到及時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); } } } } } 分析原始碼,定時任務定時呼叫,每次呼叫生成10個執行緒處理,而它又使用了非執行緒安全的List物件,導致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.快取基礎教育編目樹"); 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.快取職業教育編目樹"); 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("======= 快取編目樹結束 ======="); } catch(Exception ex) { LOGGER.error(ex.getMessage(), ex); LOGGER.info("======= 快取編目樹出錯 ======="); } } } 這裡的HashMap也有問題:居然使用定時任務,在容器啟動之後定時將資料放到Map裡面做快取?這裡修改這部分程式碼,替換為使用memcached快取即可。
記憶體洩漏的原因分析,總結出來只有一條:存在無效的引用!良好的編碼規範以及合理使用設計模式有助於解決此類問題。