从一个栈引出的内存泄露问题

简介:

我记得在有一次面试中,面试官问我自己实现的一个栈中会不会有内存泄露的问题,我努力搜索可能的问题,就是感受不到可能出现的问题。当时忽然意识到,内存泄露这个问题一直被我忽略,因为用的是java/C#,这些语言中都有内存自动回收的机制,我突然发现自己对这个问题竟然一无所知。面试中的栈就是下面这个:

复制代码
// 你能检查出"内存泄露"吗?
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);
    }
}
复制代码

这段程序不管你怎么测试都是没有问题的,但是他确实可能引起“内存泄露”。定位到pop()函数,在return语句中,当我们弹出一个元素时,只是简单的让栈顶指针(size)-1。逻辑上,栈中的这个元素已经弹出,已经没有用了。但是事实上,被弹出的元素依然存在于elements数组中,它依然被elements数组所引用,GC是无法回收被引用着的对象的。也许你期望等这整个栈失去引用(将被GC回收时),栈内的elements数组一起被GC回收。但是实际的使用过程中,又有谁能够预料到这个栈会存活多长时间。为了保险起见,我们需要在弹出一个元素的时候,就让这个元素失去引用,便于GC回收。我们只需要让Pop()函数弹出时,同时解除对弹出元素的引用即可。

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

从上面的例子中,我们可以发现当类中持有过期的元素的引用时,就有可能造成内存泄露问题。而且通常这种内存泄露问题都是我们无意识造成的,上面的栈中,逻辑上我们认为弹出的元素就应该被GC回收掉,但事实上GC没有办法回收,因此elements数组依然持有它。这种问题很隐蔽,通常只要类自己管理内存(如类中有一个Array或List型的结构),那么我们就应该警惕内存泄露的问题。

 

内存泄露来源及解决

    内存泄露可能来源于缓存。我们为了让下次的程序的处理速度更快,常常需要将一些信息缓存在内存中,但是这些过期的缓存又很容易被遗忘,从而使得它不再有用之后很长一段时间内仍然留在缓存中。例如像一个要显示图片墙的程序,我们需要缓存图片和相关的信息,为了方便GC回收过期的缓存,我们可以使用WeakHashMap来实现缓存,当界面显示图片的时候,界面持有相关图片的引用,这些引用同时也存在于WeakHashMap中。而其他不被界面持有的过期缓存,则WeakHashMap会自动将这些剔除。

    总的说来,只要在缓存之外存在对某个项的键的引用,该项就有意义,那么就可以用WeakHashMap代表缓存;当缓存中的项过期之后,它们就自动被删除。记住只有当所有的缓存项的生命周期是由该键的外部引用而不是由值决定时,WeakHashMap才有用处。

    更为常见的情景则是,"缓存项的生命周期是否有意义"并不是非常容易确定,随着时间推移,其中的项会变得越来越没有价值。在这种情况下,缓存应该是不是地清除掉没有的项。这项清除工作可以由一个后台线程(可能是Timer或者ScheduledThreadPoolExecutor)来完成,或者也可以在给缓存添加新条目的时候顺便进行清理。LinkedHashMap类利用它的removeEldestEntry方法可以很容易地实现后一种方案。对于更加复杂的缓存,必须直接使用java.lang.ref. 
    内存泄露的第三个常见来源是监听器和其他回调。如果你实现了一个API,客户端在这个API中注册回调,却没有显式地取消注册,那么除非你采取某些动作,否则他们就会积聚。确保回调立即被当成垃圾回收的最佳方法是只保存它们的弱引用(weak reference),例如,只将它们保存成WeakHashMap中的键。

 

部分文字直接截取自《Effective Java》

本文转自陈哈哈博客园博客,原文链接http://www.cnblogs.com/kissazi2/p/3618464.html如需转载请自行联系原作者

kissazi2
相关文章
|
9月前
|
存储 IDE Java
java设置栈内存大小
在Java应用中合理设置栈内存大小是确保程序稳定性和性能的重要措施。通过JVM参数 `-Xss`,可以灵活调整栈内存大小,以适应不同的应用场景。本文介绍了设置栈内存大小的方法、应用场景和注意事项,希望能帮助开发者更好地管理Java应用的内存资源。
457 4
|
存储 算法 Java
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
Java面试题:深入探究Java内存模型与垃圾回收机制,解释JVM中堆内存和栈内存的主要区别,谈谈对Java垃圾回收机制的理解,Java中的内存泄漏及其产生原因,如何检测和解决内存泄漏问题
170 0
|
11月前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
262 5
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
670 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
12月前
|
存储
栈内存
栈内存归属于单个线程,也就是每创建一个线程都会分配一块栈内存,而栈中存储的东西只有本线程可见,属于线程私有。 栈的生命周期与线程一致,一旦线程结束,栈内存也就被回收。 栈中存放的内容主要包括:8大基本类型 + 对象的引用 + 实例的方法
138 1
|
存储 安全 Java
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程是什么,JDK、JRE、JVM的联系与区别;什么是程序计数器,堆,虚拟机栈,栈内存溢出,堆栈的区别是什么,方法区,直接内存
JVM常见面试题(二):JVM是什么、由哪些部分组成、运行流程,JDK、JRE、JVM关系;程序计数器,堆,虚拟机栈,堆栈的区别是什么,方法区,直接内存
|
存储 程序员 编译器
堆和栈内存的区别是什么
【8月更文挑战第23天】堆和栈内存的区别是什么
1158 4
|
存储 Java
深入理解Java中的堆内存与栈内存分配
深入理解Java中的堆内存与栈内存分配
|
存储 监控 Java
深入剖析堆和栈的区别及其在内存管理中的影响
深入剖析堆和栈的区别及其在内存管理中的影响
|
存储 Java C++
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据
Java虚拟机(JVM)管理内存划分为多个区域:程序计数器记录线程执行位置;虚拟机栈存储线程私有数据,如局部变量和操作数;本地方法栈支持native方法;堆存放所有线程的对象实例,由垃圾回收管理;方法区(在Java 8后变为元空间)存储类信息和常量;运行时常量池是方法区一部分,保存符号引用和常量;直接内存非JVM规范定义,手动管理,通过Buffer类使用。Java 8后,永久代被元空间取代,G1成为默认GC。
160 2

热门文章

最新文章