Effective Java 3rd 条目7 消除过期对象引用

如果你是从手动内存管理的语言(比如C或者C++)切换到垃圾回收语言(比如Java),作为程序员你的工作会变得更容易,因为当你用完了对象时会被自动回收。当你第一次经历的时候,这好像是魔法一样。这可能容易导致这种印象:你不必要考虑内存管理,但是这不完全正确的。

考虑如下栈实现的例子:

// 你能指出“内存泄漏”吗?
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]; 
    }

    /** 
    * 保证至少有一个以上的元素的空间,每次队列需要增长时大约使容量加倍
    * */ 
    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1); 
    }
}

这个程序没有明显的错误(但是参考条目29的泛型版本)。你可以彻底测试它,而且它会成功通过每项测试,但是有个潜在的问题。大约地讲,这个程序有个“内存泄漏”,当由于垃圾回收器活动增加和内存占用增加而性能降低时,会悄悄的显露出来。在极端情况下,这样的内存泄漏可以导致磁盘分页,甚至OutOfMemoryError的程序失败,但是这样的失败是相当稀少的。

那么内存泄漏在哪里呢?如果栈增长然后收缩,从栈弹出的对象不会被垃圾回收,即使程序已经没有对它们的引用。这是因为栈维持着对这些对象的过期引用(obsolete reference)。过期引用仅仅是一个没有再次解引用的引用。从这个情况下,在元素队列中的“有效区域”之外的任何引用都是过期的。有效区域包含索引小于大小(size)的元素。

垃圾回收的语言中内存泄漏(叫做无意对象留存(unintentional object retention)更合适)是潜隐的。如果一个对象引用无意留存了,不仅是对象被垃圾回收排除之外,而且由这个对象引用的任何其他对象也是如此(诸如此类)。即使只有一些对象引用无意存留,许许多多的对象也被阻止垃圾回收,这对性能可能有很大影响。

这种问题的解决方案很简单:当引用过期后置空。在我们的Stack类的情形中,对一个项的引用当被弹出栈时,会成为过期。pop方法的纠正版本就像下面:

public Object pop() { 
    if (size == 0) 
        throw new EmptyStackException(); 
    Object result = elements[--size]; 
    elements[size] = null; // 消除过期引用 
    return result; 
}

置空过期引用的额外好处是,如果后来它们被错误地被解引用,这个程序立即以NullPointerException方式失败,而不是悄悄地做错误的事情。尽快检测到程序错误,这总是有益的。

当程序员第一次被这个问题困扰时,他们可能过度补偿:一旦程序完成使用,就置空每个对象引用。这既不必须也不合适;这不必要地把代码凌乱了。置空对象引用应该是特例而不是规范。消除过期引用的最佳方式是让包含引用的变量掉出作用域。如果你在尽可能窄的域中定义每个变量,自然而然就会发生(条目57)。

那么你什么时候置空一个引用呢?Stack类的什么方面使得内存泄漏容易呢?简单来讲,它自己管理自己的内存(manages its own memory)存储池(storage pool)包含元素队列的元素(对象引用格(cell),而不是对象本身)。队列有效区域的元素是被分配的,而队列其他的元素是空闲的。垃圾回收不会知道这些;对于垃圾回收器来说,元素队列里面的所有对象引用是同样有效。只有程序员知道队列的无效部分是不重要的。一旦队列元素变为不有效部分的一部分,就手动地置空队列元素,通过这种方式,程序员有效地和垃圾回收器沟通这个事实。

通常来说,无任何时一个类管理自己的内存,程序员应该警惕内存泄漏。无任何时一个元素被释放,一个元素内含的对象引用应该被置空。

另外一个内存泄漏的来源是缓存。一旦你把对象引用放入到缓存时,容易忘记它还在那里,在它变得不相关的时候让它久存在缓存中。这个问题有几个解决方案。只要在缓存之外键值对(entry)的的键有引用,键值对就有意义,如果你足够幸运实现这样的缓存,可以用WeakHashMap呈现这个缓存;在键值对过期之后,它们可以自动的被移除。记住,只有在缓存键值对的期望生命周期是由键的外部引用决定,而不是值的时候,WeakHashMap才是有用的。

更普遍地,随着时间键值对变得越来越没有价值,缓存键值对的有用生命周期也没有很好的定义。在这些情况下,缓存应该不定期清理不再用的键值对。这个可以用后台线程(或许是一个ScheduledThreadPoolExecutor)完成,或者可以作为添加新的键值对到缓存的副作用。通过它的removeEldestEntry方法,LinkedHashMap类使得后面这种方法更加容易。对于更复杂的缓存,你可以直接用java.lang.ref。

第三个内存泄漏的来源是监听器(listener)和其他的回调(callback)。如果你实现了一个API,客户端可以注册回调,但是不会显式地反注册,除非你采取一些行动,否则它们将积累。一个保证回调立即被垃圾回收的方式是,只存储对它们的虚引用,比如,仅仅把他们作为WeakHashMap的键来存储。

因为内存泄漏通常不会像明显的失败来显现,它们可能在一个系统中存在数年。它们通常仅仅通过仔细的代码检查,或者在调试工具(叫做heap profiler)的帮助下被发现。所以,这是非常值得的,学会在这样的问题发生之前预见到它们,并阻止它们发生。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • 本文出自 Eddy Wiki ,转载请注明出处:http://eddy.wiki/interview-java.h...
    eddy_wiki阅读 1,151评论 0 16
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,182评论 11 349
  • 关爱是一缕春风,温暖着每一个人的心。 关爱是一把火炬,照亮了每个人的行程。 关爱是一个港湾,安抚着每个远航的人……...
    晚巷清风阅读 239评论 1 5
  • 躺在地上那刻真爽,和队友们聊着天,累并兴奋着,想睡觉,但总有和队友们说不完的话。尤其当有后到队友,给他们鼓掌,精神...
    红糖二姐阅读 272评论 0 0
  • 我们都知道盲人摸象的故事,但是在你不同的人生阶段,对这个故事的理解会多么的不同。以前我只会嘲笑那些盲人目...
    孤石刘琳锋阅读 170评论 0 0