在没有垃圾收集的语言中,比如C++,必须特别关注内存管理。对于每个动态 对象,必须要么实现引用计数以模拟 垃圾收集效果,要么管理每个对象的“所 有权”――确定哪个类负责删除一个对象。通常,对这种所有权的维护并没有什 么成文的规则,而是按照约定(通常是不成文的)进行维护。尽管垃圾收集意味 着Java开发者不必太多地担心内存 泄漏,有时我们仍然需要担心对象所有权, 以防止数据争用(data races)和不必要的副作用。在这篇文章中,Brian Goetz 指出了一些这样的情况,即Java开发者必须注意对象所有权。
如果您是在1997年之前开始学习编程,那么可能您学习的第一种编程语言没 有提供透明的垃圾收集。每一个new 操作必须有相应的delete操作 ,否则您的 程序就会泄漏内存,最终内存分配器(memory allocator )就会出故障,而您 的程序就会崩溃。每当利用 new 分配一个对象时,您就得问自己,谁将删除该 对象?何时删除?
别名, 也叫做 ...
内存管理复杂性的主要原因是别名使用:同一块内存或对象具有 多个指针或 引用。别名在任何时候都会很自然地出现。例如,在清单 1 中,在 makeSomething 的第一行创建的 Something 对象至少有四个引用:
something 引用。
集合 c1 中至少有一个引用。
当 something 被作为参数传递给 registerSomething 时,会创建临时 aSomething 引用。
集合 c2 中至少有一个引用。
清单 1. 典型代码中的别名
Collection c1, c2;
public void makeSomething {
Something something = new Something();
c1.add(something);
registerSomething(something);
}
private void registerSomething(Something aSomething) {
c2.add(aSomething);
}
在非垃圾收集语言中需要避免两个主要的内存管理危险:内存泄漏和悬空指 针。为了防止内存泄漏,必须确保每个分配了内存的对象最终都会被删除。为了 避免悬空指针(一种危险的情况,即一块内存已经被释放了,而一个指针还在引 用它),必须在最后的引用释放之后才删除对象。为满足这两条约束,采用一定 的策略是很重要的。
为内存管理而管理对象所有权
除了垃圾收集之外,通常还有其他两种方法用于处理别名问题: 引用计数和 所有权管理。引用计数(reference counting)是对一个给定的对象当前有多少 指向它的引用保留有一个计数,然后当最后一个引用被释放时自动删除该对象。 在 C和20世纪90年代中期之前的多数 C++ 版本中,这是不可能自动完成的。标 准模板库(Standard Template Library,STL)允许创建“灵巧”指针,而不能 自动实现引用计数(要查看一些例子,请参见开放源代码 Boost 库中的 shared_ptr 类,或者参见STL中的更加简单的 auto_ptr 类)。
所有权管理(ownership management) 是这样一个过程,该过程指明一个指 针是“拥有”指针("owning" pointer),而 所有其他别名只是临时的二类副 本( temporary second-class copies),并且只在所拥有的指针被释放时才删 除对象。在有些情况下,所有权可以从一个指针“转移”到另一个指针,比如一 个这样的方法,它以一个缓冲区作为参数,该方法用于向一个套接字写数据,并 且在写操作完成时删除这个缓冲区。这样的方法通常叫做接收器 (sinks)。在 这个例子中,缓冲区的所有权已经被有效地转移,因而进行调用的代码必须假设 在被调用方法返回时缓冲区已经被删除。(通过确保所有的别名指针都具有与调 用堆栈(比如方法参数或局部变量)一致的作用域(scope ),可以进一步简化 所有权管理,如果引用将由非堆栈作用域的变量保存,则通过复制对象来进行简 化。)
那么,怎么着?
此时,您可能正纳闷,为什么我还要讨论内存管理、别名和对象所有权。毕 竟,垃圾收集是 Java语言的核心特性之一,而内存管理是已经过时的一件麻烦 事。就让垃圾收集器来处理这件事吧,这正是它的工作。那些从内存管理的麻烦 中解脱出来的人不愿意再回到过去,而那些从未处理过内存管理的人则根本无法 想象在过去倒霉的日子里――比如1996年――程序员的编程是多么可怕。
提防悬空别名
那么这意味着我们可以与对象所有权的概念说再见了吗?可以说是,也可以 说不是。大多数情况下,垃圾收集确实消除了显式资源存储单元分配(explicit resource deallocation)的必要(在以后的专栏中我将讨论一些例外)。但是 ,有一个区域中,所有权管理仍然是Java 程序中的一个问题,而这就是悬空别 名(dangling aliases)问题。Java 开发者通常依赖于这样一个隐含的假设, 即假设由对象所有权来确定哪些引用应该被看作是只读的 (在C++中就是一个 const 指针),哪些引用可以用来修改被引用的对象的状态。当两个类都(错误 地)认为自己保存有对给定对象的惟一可写的引用时,就会出现悬空指针。发生 这种情况时,如果对象的状态被意外地更改,这两个类中的一个或两者将会产生 混淆。