使用jmap和MAT分析JVM堆内存

来源:互联网 发布:南京水科院怎么样知乎 编辑:程序博客网 时间:2024/06/07 06:43

我的一台生产环境机器每次运行几天之后就会莫名其妙的宕机,分析日志之后发现在tomcat刚启动的时候内存占用比较少,但是运行个几天之后内存占用越来越大,通过jmap命令可以查询到一些大对象引用没有被及时GC,这里就要求解决内存泄露的问题。


Java的内存泄露多半是因为对象存在无效的引用,对象得不到释放,如果发现Java应用程序占用的内存出现了泄露的迹象,那么我们一般采用下面的步骤分析:
1. 用工具生成java应用程序的heap dump(如jmap)
2. 使用Java heap分析工具(如MAT),找出内存占用超出预期的嫌疑对象
3. 根据情况,分析嫌疑对象和其他对象的引用关系。
4. 分析程序的源代码,找出嫌疑对象数量过多的原因。


以下一步步的按照项目实例来操作,去解决内存泄露的问题。

1. 登录linux服务器,获取tomcat的pid,命令:

[html] view plain copy
  1. ps -ef|grep java  


2. 利用jmap初步分析内存映射,命令:

[html] view plain copy
  1. jmap -histo:live 3514 | head -7  


第2行是我们业务系统的对象,通过这个对象的引用可以初步分析出到底是哪里出现了引用未被垃圾回收收集,通知开发人员优化相关代码。


3. 如果上面一步还无法定位到关键信息,那么需要拿到heap dump,生成离线文件,做进一步分析,命令:

[html] view plain copy
  1. 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),查看引用:


分析引用栈,找到无效引用,打开源码:


有问题的源码如下:

[java] view plain copy
  1. public class RefreshCmsOrganizationStruts implements Runnable{  
  2.   
  3.     private final static Logger logger = Logger.getLogger(RefreshCmsOrganizationStruts.class);  
  4.       
  5.     private List<Cms_Organization> organizations;  
  6.   
  7.     private OrganizationDao organizationDao = (OrganizationDao) WebContentBean  
  8.             .getInstance().getBean("organizationDao");  
  9.     public RefreshCmsOrganizationStruts(List<Cms_Organization> organizations) {  
  10.         this.organizations = organizations;  
  11.     }  
  12.   
  13.     public void run() {  
  14.         Iterator<Cms_Organization> iter = organizations.iterator();  
  15.         Cms_Organization organization = null;  
  16.         while (iter.hasNext()) {  
  17.             organization = iter.next();  
  18.             synchronized (organization) {  
  19.                 try {  
  20.                     organizationDao.refreshCmsOrganizationStrutsInfo(organization.getOrgaId());  
  21.                     organizationDao.refreshCmsOrganizationResourceInfo(organization.getOrgaId());  
  22.                     organizationDao.sleep();  
  23.                 } catch (Exception e) {  
  24.                     logger.debug("RefreshCmsOrganizationStruts organization = " + organization.getOrgaId(), e);  
  25.                 }  
  26.             }  
  27.         }  
  28.     }  
  29.   
  30. }  
分析源码,定时任务定时调用,每次调用生成10个线程处理,而它又使用了非线程安全的List对象,导致List对象无法被GC收集,所以这里将List替换为CopyOnWriteArrayList 。

- Dominator Tree:这个使用的也比较多,显示大对象的占用率。

同样的打开源码:

[java] view plain copy
  1. public class CategoryCacheJob extends QuartzJobBean implements StatefulJob {  
  2.       
  3.     private static final Logger LOGGER = Logger.getLogger(CategoryCacheJob.class);  
  4.       
  5.     public static Map<String,List<Cms_Category>> cacheMap = new java.util.HashMap<String,List<Cms_Category>>();  
  6.   
  7.     @Override  
  8.     protected void executeInternal(JobExecutionContext ctx) throws JobExecutionException {  
  9.         try {  
  10.             //LOGGER.info("======= 缓存编目树开始 =======");  
  11.             MongoBaseDao mongoBaseDao = (MongoBaseDao) BeanLocator.getInstance().getBean("mongoBaseDao");  
  12.             MongoOperations mongoOperations = mongoBaseDao.getMongoOperations();  
  13.               
  14.             /* 
  15.             LOGGER.info("1.缓存基础教育编目树"); 
  16.             Query query = Query.query(Criteria.where("isDel").is("0").and("categoryType").is("F")); 
  17.             query.sort().on("orderNo", Order.ASCENDING); 
  18.             List<Cms_Category> list = mongoOperations.find(query, Cms_Category.class); 
  19.             String key = query.toString().replaceAll("\\{|\\}|\\p{Cntrl}|\\p{Space}", ""); 
  20.             key += "_CategoryCacheJob"; 
  21.             cacheMap.put(key, list); 
  22.             */  
  23.               
  24.             //LOGGER.info("2.缓存职业教育编目树");  
  25.             Query query2 = Query.query(Criteria.where("isDel").is("0").and("categoryType").in("JMP","JHP"));  
  26.             query2.sort().on("orderNo", Order.ASCENDING);  
  27.             List<Cms_Category> list2 = mongoOperations.find(query2, Cms_Category.class);  
  28.             String key2 = query2.toString().replaceAll("\\{|\\}|\\p{Cntrl}|\\p{Space}""");  
  29.             key2 += "_CategoryCacheJob";  
  30.             cacheMap.put(key2, list2);  
  31.               
  32.             //LOGGER.info("3.缓存专题教育编目树");  
  33.             Query query3 = Query.query(Criteria.where("isDel").is("0").and("categoryType").is("JS"));  
  34.             query3.sort().on("orderNo", Order.ASCENDING);  
  35.             List<Cms_Category> list3 = mongoOperations.find(query3, Cms_Category.class);  
  36.             String key3 = query3.toString().replaceAll("\\{|\\}|\\p{Cntrl}|\\p{Space}""");  
  37.             key3 += "_CategoryCacheJob";  
  38.             cacheMap.put(key3, list3);  
  39.               
  40.             //LOGGER.info("======= 缓存编目树结束 =======");  
  41.         } catch(Exception ex) {  
  42.             LOGGER.error(ex.getMessage(), ex);  
  43.             LOGGER.info("======= 缓存编目树出错 =======");  
  44.         }  
  45.     }  
  46.   
  47. }  
这里的HashMap也有问题:居然使用定时任务,在容器启动之后定时将数据放到Map里面做缓存?这里修改这部分代码,替换为使用memcached缓存即可。


内存泄漏的原因分析,总结出来只有一条:存在无效的引用!良好的编码规范以及合理使用设计模式有助于解决此类问题。