Java理论与实践

在没有垃圾收集的语言中,比如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 指针),哪些引用可以用来修改被引用的对象的状态。当两个类都(错误 地)认为自己保存有对给定对象的惟一可写的引用时,就会出现悬空指针。发生 这种情况时,如果对象的状态被意外地更改,这两个类中的一个或两者将会产生 混淆。

时间: 2024-07-30 10:30:07

Java理论与实践的相关文章

Java 理论与实践:变还是不变?

不变对象具有许多能更方便地使用它们的特性,包括不严格的同步需求和不必考虑数据讹误就能自由地共享和高速缓存对象引用.尽管不变性可能未必对于所有类都有意义,但大多数程序中至少有一些类将受益于不可变.在本月的 Java 理论与实践中,Brian Goetz 说明了不变性的一些长处和构造不变类的一些准则.请在附带的论坛中与作者和其他读者分享您关于本文的心得.(也可以单击文章顶部或底部的"讨论"来访问论坛.) 不变对象是指在实例化后其外部可见状态无法更改的对象.Java 类库中的 String.

Java 理论与实践: JDK 5.0 中更灵活、更具可伸缩性的锁定机制

伸缩 内容: synchronized 快速回顾 对 synchronized 的改进 比较 ReentrantLock 和 synchronized 的可伸缩性 条件变量 这不公平 结束语 参考资料 关于作者 对本文的评价 相关内容: Java 理论与实践 系列 Synchronization is not the enemy Reducing contention IBM developer kits for the Java platform (downloads) 订阅: develop

Java理论与实践: 您的小数点到哪里去了?

许多程序员在其整个开发生涯中都不曾使用定点或浮点数,可能的例外是, 偶尔在计时测试或基准测试程序中会用到.Java语言和类库支持两类非整数类型 ― IEEE 754 浮点( float 和 double ,包装类(wrapper class)为 Float 和 Double ),以及任意精度的小数( java.math.BigDecimal ).在本月的 Java 理论和实践中,Brian Goetz 探讨了在 Java 程序中使用非整数类型时一 些常碰到的陷阱和"gotcha". 虽

Java理论与实践: 垃圾收集简史

Java 语言可能是使用最广泛的依赖于垃圾收集的编程语言,但是它并不是第 一个.垃圾收集已经成为了包括 Lisp.Smalltalk.Eiffel.Haskell.ML. Scheme和 Modula-3 在内的许多编程语言的一个集成部分,并且从 20 世纪 60 年代早期就开始使用了.在 Java 理论与实践的本篇文章中,Brian Goetz 描述 了垃圾收集最常用的技术. 垃圾收集的好处是无可争辩的 ―― 可靠性提高.使内存管理与类接口设计 分离,并使开发者减少了跟踪内存管理错误的时间.著

Java理论与实践: 构建一个更好的HashMap

ConcurrentHashMap 是 Doug Lea的 util.concurrent 包的一部分,它提供 比Hashtable 或者 synchronizedMap 更高程度的并发性.而且,对于大多数成 功的 get() 操作它会设法避免完全锁定,其结果就是使得并发应用程序有着非 常好的吞吐量.这个月,BrianGoetz 仔细分析了 ConcurrentHashMap的代码, 并探讨 Doug Lea 是如何在不损失线程安全的情况下取得这么骄人成绩的. 在7月份的那期 Java理论与实践

Java理论与实践: 消除bug

很多有关编程风格的建议都是为了创建高质量.可维护的代码,这很合理, 因为最容易修复 bug 的时间就是在产生 bug 之前(少量的预防措施--).遗 憾的是,只预防往往是不够的,虽然有一些精巧的工具可以帮助您创建好的代码 ,但是很少有工具可以帮助您分析.维护或提高现有代码的质量. 写线程安全的类很难,而分析现有类的线程安全性更难,增强类使其仍然保 持线程安全也很难.以隐含假定.不变式以及预期用例(虽然在开发人员的头脑 中很清晰,但是没有以设计笔记.注释或者文档的方式记录下来)的方式编写完 类之后

Java理论与实践: 修复Java内存模型,第1部分

活跃了将近三年的 JSR 133,近期发布了关于如何修复 Java 内存模型 (Java Memory Model, JMM)的公开建议.原始 JMM 中有几个严重缺陷,这导 致了一些难度高得惊人的概念语义,这些概念原来被认为很简单,如 volatile .final 以及 synchronized.在这一期的 Java 理论与实践 中,Brian Goetz 展示了如何加强 volatile 和 final 的语义,以修复 JMM.这些更改有些已经 集成在 JDK 1.4 中:而另一些将会包含

Java理论与实践:做个好的(事件)侦听器

观察者模式在 Swing 开发中很常见,在 GUI 应用程序以外的场景中,它对 于消除组件的耦合性也非常有用.但是,仍然存在一些侦听器登记和调用方面的 常见缺陷.在 Java 理论与实践 的这一期中,Java 专家 Brian Goetz 就如何 做一个好的侦听器,以及如何对您的侦听器也友好,提供了一些感觉很好的建议 .请在相应的 讨论论坛 上与作者和其他读者分享您对这篇文章的想法.(您也 可以单击本文顶部或底部的 讨论 访问论坛.) Swing 框架以事件侦听器的形式广泛利用了观察者模式(也称

Java理论与实践专题

Java理论与实践: JDK 5.0中更灵活.更具可伸缩性的锁定机制 Java理论和实践: 一个有缺陷的微基准的剖析 Java理论和实践: 理解JTS ― 平衡安全性和性能 Java理论和实践: 理解JTS ― 幕后魔术 Java理论和实践: 安全构造技术 Java理论与实践: 平衡测试,第3部分:用方面检验设计约束 Java理论与实践:平衡测试,第2部分:编写和优化bug检测器 Java理论与实践:平衡测试,第1部分:不要仅编写测试,还要编写bu Java理论与实践: 您的小数点到哪里去了?

Java理论和实践: 安全构造技术

Java 语言提供了灵活的.看上去很简单的线程功能,使得您很容易在您的应用程序中使用多线程.然而,Java应用程序中的并发编程比看上去要复杂:在 Java 程序中,有一些微妙(也许并不是那么微妙)方式会造成数据争用(data race)以及并发问题.在这篇 Java 理论和实践中,Brian探讨了一个常见的线程方面的危险:在构造过程中,允许 this 引用逃脱(escape).这个看上去没有什么危害的做法可以在 Java 程序中造成无法可预料和不期望的结果. 测试和调试多线程程序是极其困难的,因