副标题#e#
在没有垃圾收集的语言中,好比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 指针),哪些引用可以用来修改被引用的工具的状态。当两个类都(错误 地)认为本身生存有对给定工具的惟一可写的引用时,就会呈现悬空指针。产生 这种环境时,假如工具的状态被意外地变动,这两个类中的一个或两者将会发生 夹杂。
#p#副标题#e#
一个贴切的例子
#p#分页标题#e#
思量清单 2 中的代码,个中的 UI 组件生存有一个 Point 工具,用于暗示 它的位置。当挪用 MathUtil.calculateDistance 来计较工具移动了多远时,我 们依赖于一个隐含而微妙的假设――即 calculateDistance 不会改变通报给它 的 Point 工具的状态,可能环境更坏,维护着对那些 Point 工具的一个引用( 好比通过将它们生存在荟萃中可能将它们通报到另一个线程),然后这个引用将 用于在 calculateDistance 返回后变动Point 工具的状态。在 calculateDistance的例子中,为这种行为担忧好像有些好笑,因为这明明是一 个可骇的违背老例的环境。可是,假如要说将一个可变的工具通报给一个要领, 之后工具还可以或许毫发无损地返返来,而且未来对付工具的状态也不会有不行预料 的副浸染(好比该要领与另一个线程共享引用,该线程大概会期待5分钟,然后 变动工具的状态),那么这只不外是一厢情愿的想法罢了。
清单 2. 将可变工具通报给外部要领是不行取的
private Point initialLocation, currentLocation;
public Widget(Point initialLocation) {
this.initialLocation = initialLocation;
this.currentLocation = initialLocation;
}
public double getDistanceMoved() {
return MathUtil.calculateDistance(initialLocation, currentLocation);
}
. . .
// The ill-behaved utility class MathUtil
public static double calculateDistance(Point p1,
Point p2) {
double distance = Math.sqrt((p2.x - p1.x) ^ 2
+ (p2.y - p1.y) ^ 2);
p2.x = p1.x;
p2.y = p1.y;
return distance;
}
一个愚蠢的例子
各人对该例子明明而普遍的回响就是――这是一个愚蠢的例子――只是强调 了这样一个事实,即工具所有权的观念在 Java 措施中依然存在,并且存在得很 好,只是没有说明罢了。calculateDistance 要领不该该改变它的参数的状态, 因为它并不“拥有”它们――虽然,挪用要领拥有它们。因此说不消思量工具所 有权。
下面是一个越发实用的例子,它说明白不知道谁拥有工具就有大概会引起混 淆。再次思量一个以Point 属性 来暗示其位置的 UI组件。清单 3 显示了实现 存取器要领 setLocation 和 getLocation的三种方法。第一种方法是最懒散的 ,而且提供了最好的机能,可是对付蓄意进攻和无意识的失误,它有几个单薄环 节。
清单 3. getters 和 setters的值语义以及引用语义
public class Widget {
private Point location;
// Version 1: No copying -- getter and setter implement reference
// semantics
// This approach effectively assumes that we are transferring
// ownership of the Point from the caller to the Widget, but this
// assumption is rarely explicitly documented.
public void setLocation(Point p) {
this.location = p;
}
public Point getLocation() {
return location;
}
// Version 2: Defensive copy on setter, implementing value
// semantics for the setter
// This approach effectively assumes that callers of
// getLocation will respect the assumption that the Widget
// owns the Point, but this assumption is rarely documented.
public void setLocation(Point p) {
this.location = new Point(p.x, p.y);
}
public Point getLocation() {
return location;
}
// Version 3: Defensive copy on getter and setter, implementing
// true value semantics, at a performance cost
public void setLocation(Point p) {
this.location = new Point(p.x, p.y);
}
public Point getLocation() {
return (Point) location.clone();
}
}
此刻来思量 setLocation 看起来是无意的利用 :
Widget w1, w2;
. . .
Point p = new Point();
p.x = p.y = 1;
w1.setLocation(p);
p.x = p.y = 2;
w2.setLocation(p);
可能是:
w2.setLocation(w1.getLocation());
#p#分页标题#e#
在setLocation/getLocation存取器实现的版本 1 之下,大概看起来仿佛第 一个Widget的 位置是 (1, 1) ,第二个Widget的位置是 (2, 2),而事实上,二 者都是 (2, 2)。这大概对付挪用者(因为第一个Widget意外地移动了)和 Widget 类(因为它的位置改变了,而与Widget代码无关)来说城市发生夹杂。 在第二个例子中,您大概认为本身只是将Widget w2移动到 Widget w1当前地址 的位置 ,可是实际上您这样做便划定了每次w1 移动时w2都跟从w1 。
防止性副本
setLocation 的版本 2 做得更好:它建设了通报给它的参数的一个副本,以 确保不存在可以意外改变其状态的 Point的别名。可是它也并非无可挑剔,因为 下面的代码也将具有一个很大概不但愿呈现的结果,即Widget在不知情的环境下 被移动了:
Point p = w1.getLocation();
. . .
p.x = 0;
getLocation 和 setLocation 的版本 3 对付别名引用的恶意或无意利用是 完全安详的。这一安详是以一些机能为价钱换来的:每次挪用一个 getter 或 setter 城市建设一个新工具。
getLocation 和 setLocation 的差异版本具有差异的语义,凡是这些语义被 称作值语义(版本 1)和引用语义(版本 3)。不幸的是,凡是没有说明实现者 应该利用的是哪种语义。功效,这个类的利用者并不清楚这一点,从而作出了更 差的假设(即选择了不是最符合的语义)。
getLocation 和 setLocation 的版本 3 所利用的技能叫做防止性复制( defensive copying),尽量存在着明明的机能上的价钱,您也应该养成这样的 习惯,即险些每次返回和存储对可变工具或数组的引用时都利用这一技能,尤其 是在您编写一个通用的大概被不是您本身编写的代码挪用(事实上这很常见)的 东西时更是如此。有别名的可变工具被意外修改的环境会以很多微妙且令人诧异 的方法溘然呈现,而且调试起来相当坚苦。
并且环境还会变得更坏。假设您是Widget类的一个利用者,您并不知道存取 器具有值语义照旧引用语义。审慎的做法是,在挪用存取器要领时也利用防止性 副本。所以,假如您想要将 w2 移动到 w1 的当前位置,您应该这样去做:
Point p = w1.getLocation();
w2.setLocation(new Point(p.x, p.y));
假如 Widget 像在版本 2 或 3 中一样实现其存取器,那么我们将为每个调 用建设两个姑且工具 ――一个在 setLocation 挪用的外面,一个在内里。
文档说明存取器语义
getLocation 和 setLocation 的版本 1 的真正问题不是它们易受夹杂别名 副浸染的不良影响(确实是这样),而是它们的语义没有清楚的说明。假如存取 器被清楚地说明为具有引用语义(而不是像凡是那样被假设为值语义),那么调 用者将更大概认识到,在它们挪用setLocation时,它们是将Point工具的所有权 转移给另一个实体,而且也不大大概仍然认为它们还拥有Point工具的所有权, 因而还可以或许再次利用它。
操作不行改变性办理以上问题
假如一开始就使得Point 成为不行变的,那么这些与 Point 有关的问题早就 迎刃而解了。不行变工具上没有副浸染,而且缓存不行变工具的引用老是安详的 ,不会呈现别名问题。假如 Point是不行变的,那么与setLocation 和 getLocation存取器的语义有关的所有问题都长短常确定的 。不行变属性的存取 器将老是具有值引用,因而挪用的任何一方都不需要防止性复制,这使得它们效 率更高。
那么为什么不在一开始就使得Point 成为不行变的呢?这大概是出于机能上 的原因,因为早期的 JVM具有不太有效的垃圾收集器。当时,每当一个工具(甚 至是鼠标)在屏幕上移动就建设一个新的Point的工具建设开销大概有些让人生 畏,而建设防止性副本的开销则不在话下。
依后见之明,使Point成为可变的这个抉择被证明对付措施清晰性和机能是昂 贵的价钱。Point类的可变性使得每一个接管Point作为参数可能要返回一个 Point的要领背上了编写文档说明的极重承担。也就是说,它得说明它是要改变 Point,照旧在返回之后保存对Point的一个引用。因为很少有类真正包括这样的 文档,所以在挪用一个没有用文档说明其挪用语义或副浸染行为的要领时,安详 的计策是在通报它到任何这样的要领之前建设一份防止副本。
#p#分页标题#e#
有嘲讽意味的是,使 Point成为可变的这个抉择所带来的机能优势被由于 Point的可变性而需要举办的防止性复制给抵消了。由于缺乏清晰的文档说明( 可能缺少信任),在要领挪用的双方都需要建设防止副本 ――挪用者需要这样 做是因为它不知道被挪用者是否会粗暴地改变 Point,而被挪用者需要这样做是 因为它不知道是否保存了对 Point 的引用。
一个现实的例子
下面是悬空别名问题的另一个例子,该例子很是雷同于我最近在一个处事器 应用中所看到的。该应用在内部利用了宣布-订阅式动静通报方法,以将事件和 状态更新转到达处事器内的其他署理。这些署理可以订阅任何一个它们感乐趣的 动静流。一旦宣布之后,通报到其他署理的动静就大概在未来某个时候在一个不 同的线程中被处理惩罚。
清单 4 显示了一个典范的动静通报事件(即宣布拍卖系统中一个新的高投标 通知)和发生该事件的代码。不幸的是,动静通报事件实现和挪用者实现的交互 合起来建设了一个悬空别名。通过简朴地复制而不是克隆数组引用,动静和发生 动静的类都生存了前一投标数组的主副本的一个引用。假如动静宣布时的时间和 消费时的时间有任何延迟,那么订阅者看到的 previous5Bids 数组的值将差异 于动静宣布时的时间,而且多个订阅者看到的前面投标的值大概会互不沟通。在 这个例子中,订阅者将看到当前投标的汗青值和前面投标的更靠近此刻的值,从 而形成了这样的错觉,认为前面投标比当前投标的值要高。不难设想这将如何引 起问题――这还不算,当应用在很大的负载下时,这样一个问题则更是袒露无遗 。使得动静类不行变并在结构时克隆像数组这样的可变引用,就可以防备该问题 。
清单 4. 宣布-订阅式动静通报代码中的悬空数组别名
public interface MessagingEvent { ... }
public class CurrentBidEvent implements MessagingEvent {
public final int currentBid;
public final int[] previous5Bids;
public CurrentBidEvent(int currentBid, int[] previousBids) {
this.currentBid = currentBid;
// Danger -- copying array reference instead of values
this.previous5Bids = previous5Bids;
}
...
}
// Now, somewhere in the bid-processing code, we create a
// CurrentBidEvent and publish it.
public void newBid(int newBid) {
if (newBid > currentBid) {
for (int i=1; i<5; i++)
previous5Bids[i] = previous5Bids[i-1];
previous5Bids[0] = currentBid;
currentBid = newBid;
messagingTopic.publish(new CurrentBidEvent(currentBid, previousBids));
}
}
}
可变工具的指导
假如您要建设一个可变类 M,那么您应该筹备编写比 M 是不行变的环境下多 得多的文档说明,以说明奈何处理惩罚 M 的引用。首先,您必需选择以 M 为参数或 返回 M 工具的要领是利用值语义照旧引用语义,并筹备在每一个在其接口内使 用 M 的其他类中清晰地文档说明这一点 。假如接管或返回 M 工具的任何要领 隐式地假设 M 的所有权被转移,那么您必需也文档说明这一点。您还要筹备着 接管在须要时建设防止副本的机能开销。
一个必需处理惩罚工具所有权问题的非凡环境是数组,因为数组不行以是不行变 的。当通报一个数组引用到另一个类时,大概有建设防止副本的价钱,除非您能 确保其他类要么建设了它本身的副本,要么只在挪用期间生存引用,不然您大概 需要在通报数组之前建设副本。别的,您可以容易地竣事这样一种景象,即挪用 的双方的类都隐式地假设它们拥有数组,只是这样会有不行预知的功效呈现。
竣事语
处理惩罚可变的类比处理惩罚不行变的类需要更细心。当在要领之间通报对可变工具 的引用时,您需要清楚地文档说明哪些环境下工具的所有权被转移。而缺乏清楚 的文档说明时,您必需在要领挪用的双方都建设防止副本。认为可变性更公道是 基于机能方面的思量,因为不需要在每次状态改变时都建设一个新工具,然而, 由防止性复制所招致的机能价钱能垂手可得地抵消掉因为淘汰了工具建设而节减 下来的机能。