内存泄漏是用户不感兴趣的任何内存使用
定义1:用户角度内存泄漏
这个定义可能有点过于宽泛,特别是,它将包括从未使用过的缓存,内存泄漏是困扰开发人员和用户数代人的一大问题。不过,术语本身并不像看上去那么明显,所以我们将从一开始就开始:应该如何定义内存泄漏?
在开发人员(和计算机科学)界,与以下定义2类似的定义(也称为“语法内存泄漏”)非常流行:
内存泄漏是指任何无法访问的内存。
定义2:语法内存泄漏
这里的“reachable”是一个递归定义,“reachable memory”是一个指向它的可访问指针或堆栈的内存,而“reachable pointer”是驻留在可访问内存中的指针。
让我们考虑以下Java程序:
//Program 1
Vector bufs = new Vector();
while( true ) {
String in = System.console.readLine( "..." );
if( in == "*" )
break;
byte buf[] = new byte[ 1000000 ];
bufs.add( buf );
// do something with buf
}
//bufs is not used after this point
根据定义2,Java中不存在可能的内存泄漏(垃圾收集器负责处理无法访问的对象)。不过,根据定义1,仍然存在内存泄漏。它说明了定义1和定义2并不是严格等价的:至少,定义1包含的元素不是定义2的成员(参见图1)。
值得一提的是,显然,程序1只显示了一个简单的示例,而且这种行为的更复杂的示例是可能的(例如,代码可能会为响应某些事件而分配大型对象,而忘记清理它们,直到稍后的某个事件中,这些对象将被简单地丢弃而不读取它们)。
定义3:调试器的透视图
进一步深入形式主义,让我们考虑一种由许多程序(从visualstudio到Valgrind)部署的非常流行的内存泄漏检测方法。这些程序倾向于跟踪所有的分配和释放(在堆内,或者在其他地方),并将在程序出口处没有释放的任何内容报告为内存泄漏。这就引出了定义3:
内存泄漏是指尚未在程序出口释放的内存。
很明显,根据这个定义3,程序1不受内存泄漏的影响,因此定义3不等同于定义1,并且定义1描述为泄漏的一些情况不是定义3的泄漏。但我们能说所有被定义为泄漏的情况,都是定义为1的泄漏吗?显然,我们不能。让我们考虑另一个程序(程序2),它在一开始就分配一个4K的缓冲区,在程序的整个生命周期中使用它,并且从不释放它,它依赖于操作系统在程序终止后进行清理。是内存泄漏吗?根据定义1(假设我们的程序2在正确执行清理的操作系统下运行),它不是;根据定义3,它是。它引导我们找到定义1和定义3之间的关系,如图2所示。
哪个定义更好?
到目前为止,我们还没有问自己哪个定义更好,在什么情况下更好。我们只是想证明他们之间存在着实质性的差异。现在是时候做出选择了。
我们认为唯一正确的定义是来自用户的定义;这并不是为了降低Valgrind等工具的价值,而是为了帮助处理对某个行为是否泄漏存在分歧的情况。
前段时间,我对某个项目进行了一场激烈的辩论。这个程序确实在一开始就分配了大约4K的内存(出于一个很好的理由,没有任何争论),而且根本没有费心去释放它。显然,visual studio已经将它报告为一个漏洞,显然有虔诚的开发人员将visual studio的泄漏报告视为福音,并认为这是一个必须修复的bug。然而,由于修复是非常琐碎的(在多线程环境中,处理释放全局变量一点也不简单),它可能会给最终用户带来真正的问题,因此我反对该修复。现在,这个难题的答案确实相当明显:如果内存泄漏的各种定义之间存在任何分歧,则定义1(语义内存泄漏),而不是任何其他定义,该定义应用于确定程序行为是否符合泄漏。
再深入一点,我们可以扪心自问——在计划结束时,所有这些交易的目的到底是什么?为什么不在完成所有必要的磁盘工作并关闭所有句柄后简单地调用exitprocesss()或exit()?当然,为了简单性(也就是可靠性),有时最好直接调用所有的析构函数,但另一方面,如果我是用户,为什么要把CPU周期花在执行不必要的清理工作上呢?更糟糕的是,如果程序使用大量内存,那么其中的大部分很可能已被交换到磁盘上的虚拟内存中。因此,为了执行不必要的释放,需要将其交换到主内存中,给用户带来极大的不便(如果您曾经想知道为什么关闭一个web浏览器需要几分钟——这是您的罪魁祸首)。总结一下我们对程序结束时的释放问题的感受-我们并不认为exitprocesss()或等效方法是处理该问题的唯一方法,但我们认为这是一种可能的方法,至少在某些情况下具有一定的价值(特别是如果至少在某些情况下仍在执行全面的释放)测试运行以检测实际内存泄漏)。从我们的观点来看,一个合理的解决方案是尝试将所有的析构函数和释放放在适当的位置,并在调试模式下运行所有工具,同时在release中求助于exitProcess()或等效方法;虽然存在一个缺点,即发布模式与调试模式不太等效,但在许多情况下,它仍然可以被正确地测试(尤其是QA测试发布版本时)。
形式主义导致近似
如果从稍微不同(也不太实际)的角度来看,内存泄漏的多个定义的整个故事是相当有趣的。很明显,我们在上面看到的,形式上的近似定义并不像我们看到的那样精确。
此外,我们可以把定义3看作是定义2的更进一步、更正式的近似值,而且这仍然是一个近似值,而且它也不是100%精确。这就引出了一个有趣但又非常普遍的问题:是否有必要增加更多的形式主义导致丧失初衷?
原文链接:http://javakk.com/1085.html
如果觉得本文对你有帮助,可以关注一下我公众号,回复关键字【面试】即可得到一份Java核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,大家一起学习进步!