Effective第三版中英 | 第二章 创建和销毁对象 | 清除过期对象的引用

文章讨论了在垃圾回收机制的语言中,如Java,虽然对象自动回收简化了内存管理,但程序员仍需关注内存泄漏问题。文中通过一个简单的栈实现示例,说明了当对象被弹出栈后,过期引用导致的内存泄漏问题。解决方法是及时清空过期引用,防止无意识的对象保留影响性能。此外,文章还提到了缓存和监听器作为内存泄漏的常见来源,并给出了相应的处理策略。
摘要由CSDN通过智能技术生成

Effective第三版

前言

大家好,这里是 Rocky 编程日记 ,喜欢后端架构及中间件源码,目前正在阅读 effective-java 书籍。同时也把自己学习该书时的笔记,代码分享出来,供大家学习交流,如若笔记中有不对的地方,那一定是当时我的理解还不够,希望你能及时提出。

如果对于该笔记存在很多疑惑,欢迎和我交流讨论,最后也感谢您的阅读,点赞,关注,收藏~

前人述备矣,我只是知识的搬运工。

effective 书籍源码均在开源项目 java-diary 中的 code-effective-third 模块中,源代码仓库地址: https://gitee.com/Rocky-BCRJ/java-diary.git

第二章 创建和销毁对象

清除过期对象的引用

  If you switched from a language with manual memory management, such as C or C++, to a garbage-collected language such as Java, your job as a programmer was made much easier by the fact that your objects are automatically reclaimed when you’re through with them. It seems almost like magic when you first experience it. It can easily lead to the impression that you don’t have to think about memory management, but this isn’t quite true.

  当你从手动管理内存的语言(比如 C 或者 C++)转换到具有垃圾回收功能的语言的时候,程序猿的工作就会变得更加容易,因为当你用完了对象之后,他们就会被自动回收。当你第一次经历对象回收功能的时候,会觉得这简直有点不可思议。这很容易给你留下这样的印象,认为自己不再需要考虑内存管理的事情了,其实不然。

  Consider the following simple stack implementation:

  考虑下面这个简单的栈实现的例子:

// Can you spot the "memory leak"?
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }
    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }
    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        return elements[--size];
    }
    /**
     * Ensure space for at least one more element, roughly
     * doubling the capacity each time the array needs to grow.
     */
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

  There’s nothing obviously wrong with this program (but see Item 29 for a generic version). You could test it exhaustively, and it would pass every test with flying colors, but there’s a problem lurking. Loosely speaking, the program has a “memory leak,” which can silently manifest itself as reduced performance due to increased garbage collector activity or increased memory footprint. In extreme cases, such memory leaks can cause disk paging and even program failure with an OutOfMemoryError, but such failures are relatively rare.

  这个程序没有明显的错误(它的通用版本请见 29 项)。无论如何测试,它都会成功地通过每一项测试,但是这个程序中隐藏着一个问题。简而言之,该程序存在“内存泄漏”,由于垃圾收集器的活动增加或者内存占用增加,程序性能的降低会逐渐表现出来。在极端的情况下,这种内存泄漏会导致磁盘分页(Disk Paging),甚至导致程序失败并出现 OutOfMemoryError,但这种失败情形相对比较少见。

  So where is the memory leak? If a stack grows and then shrinks, the objects that were popped off the stack will not be garbage collected, even if the program using the stack has no more references to them. This is because the stack maintains obsolete references to these objects. An obsolete reference is simply a reference that will never be dereferenced again. In this case, any references outside of the “active portion” of the element array are obsolete. The active portion consists of the elements whose index is less than size.

  那么,程序中哪里发生了内存泄漏呢?如果一个栈先是增长,然后再收缩,那么,从栈中弹出来的对象将不会被当做垃圾回收,即使使用栈的程序不再引用这些对象,它们也不会回收,因为,栈内部维护着对这些对象的过期引用(obsolete references),所谓的过期引用,是指永远也不会再被解除的引用。在本例中,凡是在 element 数组的“活动部分”(active portion)之外的任何引用都是过期的。活动部分是指 element 中下标小于 size 的那些元素。

Memory leaks in garbage-collected languages (more properly known as unintentional object retentions) are insidious. If an object reference is unintentionally retained, not only is that object excluded from garbage collection, but so too are any objects referenced by that object, and so on. Even if only a few object references are unintentionally retained, many, many objects may be prevented from being garbage collected, with potentially large effects on performance.

  具有垃圾收集功能的编程语言中的内存泄漏(更恰当地称为无意识的对象保留)是隐蔽的。如果无意中保留了对象引用,则不仅将该对象从垃圾回收中排除,而且该对象引用的任何对象也是如此,以此类推。即使无意中保留了少量对象引用,也会阻止许多对象被垃圾回收器收集,对性能可能产生很大影响。

The fix for this sort of problem is simple: null out references once they become obsolete. In the case of our Stack class, the reference to an item becomes obsolete as soon as it’s popped off the stack. The corrected version of the pop method looks like this:

  这类问题的修复方法很简单:一旦对象引用已经过期,只需要清空这些引用即可。对于上述例子中的 Stack 类而言,只要一个单元被弹出栈,指向它的引用就过期了,pop 方法的修订版本如下所示:

public Object pop() {
    if (size == 0)
        throw new EmptyStackException();
    Object result = elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}

An added benefit of nulling out obsolete references is that if they are subsequently dereferenced by mistake, the program will immediately fail with a NullPointerException, rather than quietly doing the wrong thing. It is always beneficial to detect programming errors as quickly as possible.

  清空过期引用的另一个好处是,如果它们以后又被错误地解除引用,程序就会立即抛出 NullPointerException 异常,而不是悄悄地错误运行下去。尽快检测出程序中的错误总是有益的。

When programmers are first stung by this problem, they may overcompensate by nulling out every object reference as soon as the program is finished using it. This is neither necessary nor desirable; it clutters up the program unnecessarily. Nulling out object references should be the exception rather than the norm. The best way to eliminate an obsolete reference is to let the variable that contained the reference fall out of scope. This occurs naturally if you define each variable in the narrowest possible scope (Item 57).

  当程序猿第一次被这种类似的问题困扰的时候,它们往往会过分小心:对于每一个对象的引用,一旦程序不再用到它,就把它清空。其实这样做即没必要,也不是我们所期望的,因为这样做会把程序代码弄得很乱。清空对象引用应该是一种例外,而不是一种规范行为。消除过期引用最好的方法是让包含该引用的变量结束其生命周期。如果你是在最紧凑的作用域范围内定义每一个变量(第 57 项),这种情形就会自然而然地发生。

So when should you null out a reference? What aspect of the Stack class makes it susceptible to memory leaks? Simply put, it manages its own memory. The storage pool consists of the elements of the elements array (the object reference cells, not the objects themselves). The elements in the active portion of the array (as defined earlier) are allocated, and those in the remainder of the array are free. The garbage collector has no way of knowing this; to the garbage collector, all of the object references in the elements array are equally valid. Only the programmer knows that the inactive portion of the array is unimportant. The programmer effectively communicates this fact to the garbage collector by manually nulling out array elements as soon as they become part of the inactive portion.

  那么,何时应该清空引用呢?Stack 类的哪方面特性使它易于遭受内存泄漏的影响呢?简而言之,问题在于,Stack 类自己管理内存(manage its own memory)、存储池(storage pool)包含了 elements 数组(对象引用单元,而不是对象本身)的元素。数组活动区域(同前面的定义)中的元素是已分配的(allocated),而数组其余部分的元素则是自由的(free)。但是垃圾回收器无法知道这一点;对于垃圾回收器而言,elements 数组中的所有对象引用都同等有效。只有程序猿知道数组的非活动部分是不重要的。程序猿可以把这个情况告知垃圾回收器,做法很简单:一旦数组元素变成了非活动部分的一部分,程序猿就手动清空这些数组元素。

Generally speaking, whenever a class manages its own memory, the programmer should be alert for memory leaks. Whenever an element is freed, any object references contained in the element should be nulled out.

  通常来说,只要类是自己管理内存,程序猿就应该警惕内存泄漏问题。一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。

Another common source of memory leaks is caches. Once you put an object reference into a cache, it’s easy to forget that it’s there and leave it in the cache long after it becomes irrelevant. There are several solutions to this problem. If you’re lucky enough to implement a cache for which an entry is relevant exactly so long as there are references to its key outside of the cache, represent the cache as a WeakHashMap; entries will be removed automatically after they become obsolete. Remember that WeakHashMap is useful only if the desired lifetime of cache entries is determined by external references to the key, not the value.

  内存泄漏的另一个常见来源是缓存。一旦你把对象引用放到缓存中,它就很容易被遗忘掉,从而使得它在很长一段时间没有使用,但是却仍然留在缓存中。对于这个问题,这里有好几种解决方案。如果你正好要实现这样的缓存,只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用 WeakHashMap 代表缓存,当缓存中的项过期之后,它们就会自动被删除。记住只有当所要的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap 才有用处。

More commonly, the useful lifetime of a cache entry is less well defined, with entries becoming less valuable over time. Under these circumstances, the cache should occasionally be cleansed of entries that have fallen into disuse. This can be done by a background thread (perhaps a ScheduledThreadPoolExecutor) or as a side effect of adding new entries to the cache.

  更为常见的情形则是,“缓存项的生命周期是否有意义”并不是很容易确定,随着时间的推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该时不时地清除掉没用的项。这项清除工作可以由一个后台线程(可能是 Timer 或者 ScheduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新项的时候顺便进行清理。LinkedHashMap 类利用它的 removeEldestEntry 方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用 java.lang.ref。

  A third common source of memory leaks is listeners and other callbacks. If you implement an API where clients register callbacks but don’t deregister them explicitly, they will accumulate unless you take some action. One way to ensure that callbacks are garbage collected promptly is to store only weak references to them, for instance, by storing them only as keys in a WeakHashMap.

  内存泄漏的第三个常见来源是监听器和其他回调。如果你实现了一个 API,客户端在这个 API 中注册回调,却没有显示地取消注册,那么除非你采取某些动作,否则他们就会积累下来。确保回调立即被当做垃圾回收的最佳方法是只保存他们的弱引用(weak reference),例如,只将它们保存成 WeakHashMap 中的键。

  Because memory leaks typically do not manifest themselves as obvious failures, they may remain present in a system for years. They are typically discovered only as a result of careful code inspection or with the aid of a debugging tool known as a heap profiler. Therefore, it is very desirable to learn to anticipate problems like this before they occur and prevent them from happening.

  由于内存泄漏通常不会表现出明显的失败迹象,所以他们可以在一个系统中存在很多年。往往只有通过仔细检查代码,或者借助于 Heap 剖析工具(Heap Profiler)才能发现内存泄漏问题。因此,如果能在内存泄漏发生之前就知道如何预测此类问题,并阻止它们发生,那是最好不过的了。

  • 8
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值