定位和优化隐藏的内存问题

来源:互联网 发布:淘宝怎么删除差评 编辑:程序博客网 时间:2024/04/30 19:44
定位和优化隐藏的内存问题

问题阐述

现象、问题描述

Java程序除了内存泄漏外,也会存在其他一些内存问题,比如: 假内存泄漏,随机内存泄漏,垃圾对象产生过多过快,大对象的生成等.

内存泄漏问题主要表现在随时间增加(有时候可能需要上月上年的时间)而内存使用增加;而假内存泄漏也可能出现上面的现象,最终出现 OutOfMemoryException,但是当增大堆内存后,又不一定会出现;随即内存泄漏又可以称为隐藏内存泄漏,其产生的时机, 以及其危害成都都是不确定的.

垃圾对象产生过多过快,并没有衡量标准,但是和预计的值相差过大.比如一个流程呼叫产生60K的对象就不正常了.大对象的生成也跟此相关,但是主要还是程序运行不稳定, GC频繁, AF GC过多.

上面这些问题, UC项目中都出现过,而且较难定位.

问题检测、根本原因分析

这些问题, 难点在与定位, 解决办法一般都很简单.因此掌握定位方法是解决上述问题的关键.这里我们通过一步一步的介绍来解决上面的问题.

,垃圾对象问题

首先我们需要解决垃圾对象生成过多过快的问题.由于内存泄漏一般都会在高压力情况下才能较快出现,所以就无法有效的使用 OptimizeIT工具;在低呼叫量下,垃圾对象又会对其产生消极影响.于是我们必须先解决一些不必要的对象问题,降低对象相互影响因素, 保证在低呼叫量的情况下也可以使用该工具进行内存分析.对于这个问题,我们可以按照如下步骤操作:

1,使用 OptimizeIT工具的 Profile模式启动系统, 然后连接上 OptimizeIT工具.

2,连接上一个运行的系统后, OptimizeIT会检测系统运行的堆内存使用状况, 按照Diff.排序就可以清晰的看到哪些对象增加过快了.

3,观测看那个对象增长比较快,然后查看该对象的分配情况,本例子中, String对象分配比较快,数据增加也比较多,于是先观测此对象:

注意: 根据经验, 一般来说都是由于 String对象增加过多导致的系统堆内存消耗过快,因此建议一开始就对 String 对象进行分析.

4,在对象分配图中,首先找到系统本身的类,比如这里就是TestMemoryOne.call():

注意: 这里首先选择的是系统本身的类, 而不是所在比例最好的类.因为只能优化本身类,所以建议先通过对象分配比例排序,然后一个一个的找出该系统的类,然后进行分析.

5,找到本身系统类的分配情况后,查看是哪个方法分配的,然后定位到该方法, 分析源代码,确保该方法中,对象使用没有问题:

public void call()

    {

        log.debug("a" + "b" + "c" + this.toString());

    }

本案例就是因为没有判断日志级别的可用性,而导致垃圾对象生成.作如下修改后, String对象就变得很少了. UC项目就是因为在代码中没有添加日志级别的判断,导致发送消息的方法中生成大量垃圾对象,而影响性能.

public void call()

    {

        if(log.isDebugEnabled())

        {

            log.debug("a" + "b" + "c" + this.toString());

        }

    }

另外我们还发现 HeapCharBuffer对象也分配过快,于是查看 main()方法,我们就可以找到在一个循环中,有调用System.out.println()的地方.

6,反复进行3-5步的操作,对所有增加过快的对象都进行检测.避免大部分垃圾对象的生成.

大对象问题

大对象问题比较容易定位,其方法跟先前定位垃圾对象差不多.对于大对象问题, 一般来说都是找方法规避,并不能真正解决.

1.        首先还在对内存使用视图中,通过size diff…”进行排序,找到内存大小增加较大的而对象数量较小的一些对象进行分析.

2,确定一个对象后,查看其分配的情况.跟分析垃圾对象一样,找到相关的代码进行分析,确保大对象的分配是必须的. 如果存在不必要的操作,获取可以规避的操作,则可进行优化改经.

注意: 一般来说, 大对象的生成情况比较少用,所以需要仔细分析每个地方是否必要,要避免比如大对象拷贝等(UC项目就有把一个大消息组装成 String对象打印到日志中的操作).

 

2.        反复进行上一步的操作,对所有大对象操作都进行优化。

具体介绍

内存泄漏

分析内存泄漏, 不管是什么情况下导致的, 其分析方法都一样.由于系统的类和第三方类以及JDK的类都有可能存在内存泄漏的问题,所有都需要分析,一般来说会分别进行(通过类过滤器,过滤出分析部分).

首先我们分析自己系统的类,通过在类filter中输入test.*来过滤掉其他的类:

先执行一次GC,然后马上标记当前的对象实例数:

 

然后继续执行GC,看看其diff是否恢复到 0或者负数.多观测几次,如果确定其不能恢复到 0或者负数,则说明其有可能存在内存泄漏问题.

注意: 这里只是说明其可能存在问题, 并不一定时问题,需要通过代码分析才能最后下结论.具体还需要参考后面的假内存泄漏和隐藏内存泄漏问题的处理.

通过查看其分配情况,找到产生该对象的类和方法,通过分析代码, 最终确定是否有内存泄漏问题.

最后在对其他类进行同样的分析,找出所有的可疑点.

假内存泄漏

在分析完代码后,可以确定的是不存在内存泄漏,那么就有可能是假内存泄漏了. 主要有一下几种情况:

1,过大的队列,导致队列中的数据还没有装满,就出现 OutOfMemoryException异常. 此时在 OptimizeIT中也可能会看到对象实例不断的增加,但是实际上代码是没有问题(队列有大小限制).

2,过多的线程(长期运行),导致内存耗尽.这个在 suse8上更是厉害, suse8上使用进程模拟线程, 每个线程都要分配2M的空间,如此计算, 1000个线程就需要2G的堆内存.

3IO数据读取,通过临时对象保存,也可能会出现这个问题.

隐藏内存泄漏

如果某个情况, 在剔除假内存泄漏的情况下, 那么就有可能属于隐藏内存泄漏问题了.这个主要是由于跟该段代码相关的代码问题导致的,非常难定位.

比如 Tomcat曾经出现过一个关于 session对象无法回收的问题. 对于每个 session对象,当有一个 request进来时,其会有一个标记 + 1来记录当前 request数目. request完成时,该标记会 -1.然后在 Tomcat forward一个 request,如果出现异常,则该 -1操作就不会被执行到,导致该标记永远 >0,于是该 session将永远不被回收.

定位这种问题, 主要是通过调试代码, 找到问题点,然后分析问题点的相关代码.没有太多捷径可询, 一般都是通过分析相关调用来定位.

注意: 对于这个情况, 根据经验分析,大部分都是由于异常处理不当导致的,所以需要对一些异常处理的地方进行分析.首先保证是否有异常处理; 然后确定异常处理是否合理;最后确定那些资源释放操作是否都放到了 finally块中.

经验总结、预防措施和规范建议

对于内存泄漏, 只有需要细心的一个个类的分析才能找出问题所在, 有时候还可能需要对同一个类进行多次分析. 但是这些分析是有价值的, 不但可以找出内存的问题,还可以对系统性能的提升有帮助.

对于小部分的内存泄漏问题,通过上面的方式分析后,还不能定位, 则需要寻找有经验的人员,了解系统设计,从系统架构上着手,逐步确定问题范围,最后在通过工具确定.这个只有通过不断的学习和实践才能达到,所以需要多对系统分析,掌握规律, 提高分析能力和问题认识广度.