自从公司开始将java作为主要开发语言后,C++与java的混合应用日趋增多。 java与C++的通信主要也是使用JNI来完成,这并没有什么问题。对于这样的混合应用项目来说,最大的噩梦莫过于memory leak诊断了。由于Java的内存管理模式与C++有很大的区别,所以对这样的项目进行调试时,首先要区分是Java代码的memory leak还是C++代码的memory leak。对于内存诊断来说,我们需要先了解一些指标含义和工具的使用,这样才能做到有理有据。
指标:
memory(working set): MSDN的说明-The working set of a process is the set of pages in the virtual address space of the process that are currently resident in physical memory. The working set contains only pageable memory allocations; nonpageable memory allocations such as Address Windowing Extensions (AWE) or large page allocations are not included in the working set.
一个进程的专用工作集指的是当前进程常驻于物理内存的虚拟内存页面集。它只包含可以被分页的内存区;那些不能被分页的内存区如AWE(一种应用程序可以直接操纵大于4G物理内存的技术)或是LPA(主要用于服务器的大物理内存上,一般对于64位的系统比较有用)不会被包括在专用工作集中。
virtual bytes: 当前进程所使用的虚拟内存大小。这个指标包含所有的内存页面文件,如在磁盘交换区中的页面文件,加载的库文件等。
private bytes:当前进程已经分配的私有内存的大小,不包含共享给其他进程使用的内存。
在一般情况下,如果你的应用程序需要的内存不多,并且比较活跃,内存泄漏比较明显,那么通过监视working set就可以看出是否有内存泄漏。但是如果应用程序比较复杂,模块较多而且需要的内存在不同时刻变化比较大,那么单纯根据working set是看不出来问题的,因为一些页面文件会在某一时刻被交换的磁盘缓冲区中。那么,我们就需要去分析virtual bytes和private bytes这两个指标。但这也不是绝对准确的,因为有些时候比如内存碎片比较多,而应用程序经常请求大块连续的内存,也会造成virtual bytes增加的情况。所以,在实际环境中我们还需要了解应用程序的内存使用特点来确定是否有memory leak问题。
介绍完了指标,下面介绍一下工具:
对于Java程序来说,比较好的监视内存的工具是jvirsualVM。这个工具是java自带的,它可以监视本地或远程的java应用程序,也可以监视系统服务这样的程序。你可以在JDK的bin目录下找到。其他的还有比如jconsole, eclipse的MAT等。
对于C++的程序来说,那工具可就多了。这里我主要用的是IIS Debug Diagnostics Tool,这个原本是用于IIS应用程序的诊断工具,在监视系统服务这样的应用上还是很方便的。同时它可以进行自动的memory leak分析并生成报表。对于memory leak的诊断很有帮助。当然,我还用到了vmmap和rammap两个应用程序。这两个程序原先是system internal那个作者开发的,现在已经收归微软门下了。这两个文件一个用于查看进程的虚拟内存分配情况,而另一个拥有查看物理内存的时候情况。最后一个工具就是process hacker,它能帮助我们更详细的了解进程的内存分配,句柄分配,模块加载,线程数目等。当然processexplorer也可以做相同的工作,但是如果你要查看内存块的内容时,还需要windbg的配合。
基本上我们需要的东西已经都有了,那么接下来就是真正的调试之旅了。这里我先介绍一下我要调试的程序是基于tomcat的企业级备份服务,java的工作是基于c++模块上来做的统计和管理工作。对于这样一个应用,memory leak是一个很头疼的问题,因为基本上不能通过调试来解决。那么如果出现了memory leak,我们先要区分出来是java代码还是c++代码。因为java是具有垃圾回收机制的语言,所以memory leak比较不好检查。那么对于java来说什么样的情况才是memory leak呢? 某个对象不能被回收,就是一个leak,比如object被放在了一个singleton的列表中,只要这个singleton没有被释放,那么这个object永远存在于内存中。对于tomcat程序来说,我们需要先配置几个参数用于jvisualvm的监视:
-Dcom.sun.management.jmxremote.port=8086
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
这样就可以在jvisualvm里面建立一个JMX的连接用于tomcat,然后我们需要在不同的时间点去打heap dump,然后通过比对两个时期的对象变化来检查是否发生memory leak。这里需要你对你的应用程序对象分配比较了解才行。在java这块对于内存来说需要关注的主要是heap和PermGen,heap是new对象要用的,而PermGen是存放class和meta data的内容。
经过不懈的努力,最终发现我们程序中对http connection没有设置超时,从而导致在持久化连接模式下,很多访问web service的线程会hang住。整完了Java部分的memory leak问题,就该整C++模块的问题了。通过一段时间的观察发现,working set的大小会在某一时刻降下来,但是virtual bytes和private bytes是阶段性的上涨。这说明可能是某段代码请求大块内存而产生的,但是这并不能证明是leak。而且这段代码也不一定是在C++中。为了找到问题是在C++中还是Java中,我需要对java的内存使用情况进行跟踪(这里使用到了process hacker的内存检查功能)。
最终我发现java部分的虚拟内存没有变化,一直维持在我们设定的最大内存大小之内。这样我们就可以从C++这边开始工作了。先用vmmap查看一下内存的使用情况,发现在一个地方内存读写量非常大,这证明有程序非常的密集访问某块内存。接下来就是使用Debug Diagnostics Tool对Tomcat的service进行跟踪了. 先创建一个memory leak的规则,然后开始跟踪。在一段时间后,内存出现比较大的变化后,选择memory dump并进行自动分析。你会得到一个大概的内存分析报告,并且报告会给出可能的memory leak模块。根据这个信息你就有选择性的去检查某个模块的代码来确定是否真的发现leak了。
最终发现问题是在加密解密函数出来问题,在一个函数中malloc了一块内存用于存储字符串,然后将它加密完后,拷贝到新的加密完的内存中就直接返回了,没有去free它。而是在返回的代码后面去free。这样就造成了每一次成功的加密就会泄漏一块内存,因为加密函数只在特定的时候被调用,所以内存成阶段性的上涨。
到这里,整个诊断memory leak的过程也就结束了。我的感受是搞这种问题既要有技术也要有运气^_^ ! (以上的图是现找的,不说明真实情况,所以大家看看知道个样子就可以了)
涉及到的工具通过google都可以找到。