Java中最容易导致内存泄漏的方法

41

你是在寻找一个人为制造的例子还是一个非常常见的编程错误? - mikerobi
一个人造的例子会是最好的。 - Paul McKenzie
每当一个不打算使用的对象有引用时,就会创建一个内存泄漏。几乎任何可以编写的程序都可能是内存泄漏的人为例子。 - OrangeDog
请查看'使用Java创建内存泄漏',以了解与“最简单”的不同的其他方法。 - Alberto
9个回答

35

在Java中,除非你:

  • 对字符串进行intern操作
  • 生成类
  • JNI调用的本地代码中泄漏内存
  • 保留对不需要的东西的引用在某个被遗忘或模糊的地方。

我认为你对最后一种情况感兴趣。常见的场景有:

  • 监听器,特别是使用内部类完成的
  • 缓存。

一个很好的例子是:

  • 构建一个Swing GUI,它可以启动无限数量的模态窗口;
  • 在初始化过程中,让模态窗口执行以下操作:
    StaticGuiHelper.getMainApplicationFrame().getOneOfTheButtons().addActionListener(new ActionListener(){
    public void actionPerformed(ActionEvent e){
    // do nothing...
    }
    })
    

注册的操作什么也不做,但它将导致模态窗口永远停留在内存中,即使在关闭后,也会导致泄漏 - 因为监听器从未被注销,并且每个匿名内部类对象都持有对其外部对象的引用(不可见)。更重要的是 - 从模态窗口引用的任何对象也有可能泄漏。

这就是为什么EventBus等库默认使用弱引用的原因。

除了监听器,其他典型的例子是缓存,但我想不出一个好的例子。


4
内部化字符串并不真正造成内存泄漏,它们也可以被垃圾回收。问题只在于它们(通常实现中)位于特殊的内存区域(PermGen),该区域比其余内存更小,因此更容易填满。 - Paŭlo Ebermann
你是对的。Interned Strings 不是一个“真正”的泄漏(再说了,“真正的泄漏”在 jvm 中是不可能的)。然而,只有当所有其他方法都失败并且其内容经过主要收集后 perm 才会被收集,因此它是少数几个真正内存问题的来源之一。此外,interned strings 占用空间,即使它们没有从程序中引用。从这个意义上讲,它们是最接近泄漏的东西。 - fdreger

16

首先,我们需要就什么是内存泄漏达成一致。

维基百科曾经这样描述过内存泄漏:在这个链接里

计算机科学中的内存泄漏(或泄露)指的是当计算机程序消耗了内存但无法将其释放回操作系统时发生的情况。

然而这个定义已经多次更改了,当前(02/2023),维基百科上这样描述:

在计算机科学中,内存泄漏是一种资源泄漏类型,它会发生在计算机程序错误地管理内存分配时,不再需要的内存未被释放的情况下。

取决于上下文,您需要更准确地指定您所要查找的内容。

无法访问的动态分配内存

首先,让我们快速看一个没有自动内存管理的语言的例子:在C语言中,您可以使用malloc()来分配一些内存。此函数返回指向已分配内存的指针。您必须使用正好该指针调用free()以将内存释放回操作系统。但是,如果指针在多个地方被使用,谁来负责调用free()?如果您过早释放内存,则仍在使用该内存的应用程序的某些部分将会崩溃。如果您不释放内存,则会出现泄漏。如果所有分配的内存的指针都丢失(被覆盖或生命周期结束),则您的应用程序将无法将内存释放回操作系统。这符合维基百科2011年对内存泄漏的旧定义。为了避免这种情况,您需要一种规约,用于定义谁负责释放已分配的内存。这需要文档,必须被正确阅读、理解和遵循,可能有很多人创建各种机会出错。

自动内存管理(Java具备)使您摆脱了这种危险。在Java中,您可以使用关键字new来分配内存,但是没有freenew返回一个“引用”,在这个上下文中类似于指针。当所有对已分配内存的引用都丢失(被覆盖或生命周期结束)时,这会自动检测到并将内存返回给操作系统。

在Java中,仅在垃圾收集器中存在错误、泄漏内存的JNI模块或类似情况下才可能出现此类内存泄漏,但至少从理论上讲您是安全的。

其他编程错误

尽管如此,在有和没有自动内存管理的情况下,都有可能主动维护不需要的引用。假设以下类:

class Demo {
    private static final LinkedList<Integer> history = new LinkedList<>(Collections.singleton(0));

    public static int plusPrevious(int value) {
        int result = history.getLast() + value;
        history.add(value);
        return result;
    }
}
每次调用plusPrevious时,history列表都会增长。但是为什么?只需要一个值,而不是整个历史记录。这个类保存了不必要的内存,这符合维基百科对内存泄漏的定义。 在这种情况下,错误是显而易见的。但在更复杂的情况下,可能不那么容易确定什么仍然是“需要”的,什么不是。 无论如何,把东西放到static变量中是开始麻烦的“好”做法。如果在上面的示例中history不是static,那么该类的用户最终可能会释放对Demo实例的引用,从而释放内存。但由于它是静态的,所以直到整个应用程序终止,历史将一直存在。

2
但是在任何时候,您仍然可以取消引用所创建的对象,从而释放内存。我不同意。类实现者可以隐藏外部世界的对象句柄。 - Thomas Eding
@trinithis:如果你有一个对象通过分配大量内存来私下浪费内存,那么你无法强制该对象释放内存而不丢弃该对象。但在这种情况下,它仅仅是浪费内存而不是泄漏。这块内存是可以被释放的。一旦不再引用浪费内存的对象,该内存将会被释放。或者我理解错了吗? - yankee
我觉得我误解了你所说的“dereference”的意思。我一直在想C语言中这个词的含义。 - Thomas Eding
“维基百科”上该术语的定义已经改变。现在它是这样的:“在计算机科学中,内存泄漏是一种资源泄漏类型,当计算机程序以不正确的方式管理内存分配时,不再需要的内存不会被释放。” 这是几乎所有海报使用的定义,但不包括此海报。 - Lii
1
@Lii:我重新写了整个答案。 - yankee

12

这里有一个简单的例子

public class Finalizer {
    @Override
    protected void finalize() throws Throwable {
        while (true) {
            Thread.yield();
        }
    }

    public static void main(String[] args) {
        while (true) {
            for (int i = 0; i < 100000; i++) {
                Finalizer f = new Finalizer();
            }

            System.out.println("" + Runtime.getRuntime().freeMemory() + " bytes free!");
        }
    }
}

2
你能解释一下在这个例子中你是如何实现内存泄漏的吗? - TheBlueNotebook
1
不确定,但这段代码似乎可以工作,至少它让我的电脑崩溃了,而且即使在关闭eclipse后,进程仍然在后台运行。 - pescamillam
5
@TheBlueNotebook他重写的finalize方法是Java通常在要释放对象内存之前调用的方法。在他的main方法中,他创建了100K个终结器,然后告诉JVM释放所有内存。JVM会礼貌地执行此操作,在实际释放内存之前调用finalize方法。它调用的finalize方法永远不会返回,因此对象永远不会被删除,但主循环会继续执行,从而创建另外100K个永远不会被删除的对象,再创建另外更多的对象... - Jeutnarg

9

使用:

public static List<byte[]> list = new ArrayList<byte[]>();

在不删除它们的情况下添加(大)数组。在某些点上,您会意识到自己已经用完了内存,而不怀疑它。(您可以对任何对象执行此操作,但是对于大型、完整的数组,您会更快地用完内存。)

在Java中,如果您取消引用一个对象(它超出范围),它将被垃圾回收。因此,您必须保持对它的引用,以便出现内存问题。


1
这会导致你的内存耗尽,但如果你从未执行任何破坏对象引用的操作,那么你怎么可能会有泄漏呢? - mikerobi
7
内存泄漏是指在不清理(也不使用)的情况下“占用”一些内存。但是,如果取消引用对象,它将被垃圾回收。 - Bozho
1
我理解这一点,但我不认为在每种情况下都会出现内存泄漏。如果您错误地将类变量设置为静态变量,则肯定会出现内存泄漏;如果您在长时间运行的进程中将其用作全局值,则可能会出现内存泄漏。如果您的意图是使数据持续到程序终止,那么这不是内存泄漏。无限循环耗尽内存与这是否是内存泄漏无关。许多内存泄漏不会被注意到,除非它们不断分配新数据,但拥有一块固定的孤立内存仍然是内存泄漏。 - mikerobi
@mikerobi - 我没有提到循环 ;) 我同意静态集合的使用决定了它是否是泄漏。但这就是Java中发生的情况 - 你不能有所谓的孤立内存,即你已经分配了它,但却忘记了它。这由垃圾回收器处理。 - Bozho
1
这不是内存泄漏。 - Aykut Kllic

3
  1. 在类范围内创建一个对象集合
  2. 定期向集合中添加新的对象
  3. 不要丢弃持有集合的类实例的引用

由于集合和拥有集合的对象实例始终存在引用,因此垃圾收集器永远不会清理该内存,随着时间的推移可能会导致"泄漏"。


3
根据最受欢迎的答案所述,您很可能在询问类似C语言的内存泄漏。由于存在垃圾回收机制,您无法分配一个对象,丢失其所有引用,并使其仍然占用内存 - 这将是一个严重的JVM错误。
另一方面,您可能会出现线程泄漏 - 这当然会导致这种状态,因为您可能有一些线程正在运行并具有对对象的引用,而您可能会失去线程的引用。您仍然可以通过API获取线程引用 - 请参见http://www.exampledepot.com/egs/java.lang/ListThreads.html

链接已经(实际上)失效: "域名exampledepot.com可能出售。" (是的,拼写方式有误) - Peter Mortensen

1
下面这个极为牵强的 Box 类在使用时会泄露内存。被 put 进入该类的对象最终(确切地说是在另一个对 put 的调用之后......前提是没有重新将相同的对象put进去)无法通过该类从外部引用。它们不能通过该类取消引用,但该类确保它们不能被收回。这是一个真正的内存泄漏。我知道这非常牵强,但类似的意外情况是有可能发生的。
import java.util.ArrayList;
import java.util.Collection;
import java.util.Stack;

public class Box <E> {
    private final Collection<Box<?>> createdBoxes = new ArrayList<Box<?>>();
    private final Stack<E> stack = new Stack<E>();

    public Box () {
        createdBoxes.add(this);
    }

    public void put (E e) {
        stack.push(e);
    }

    public E get () {
        if (stack.isEmpty()) {
            return null;
        }
        return stack.peek();
    }
}

0

尝试使用这个简单的类:

public class Memory {
    private Map<String, List<Object>> dontGarbageMe = new HashMap<String, List<Object>>();

    public Memory() {
        dontGarbageMe.put("map", new ArrayList<Object>());
    }

    public void useMemInMB(long size) {
        System.out.println("Before=" + getFreeMemInMB() + " MB");

        long before = getFreeMemInMB();
        while ((before  - getFreeMemInMB()) < size) {
            dontGarbageMe.get("map").add("aaaaaaaaaaaaaaaaaaaaaa");
        }

        dontGarbageMe.put("map", null);

        System.out.println("After=" + getFreeMemInMB() + " MB");
    }

    private long getFreeMemInMB() {
        return Runtime.getRuntime().freeMemory() / (1024 * 1024);
    }

    public static void main(String[] args) {
        Memory m = new Memory();
        m.useMemInMB(15);  // put here apropriate huge value
    }
}

1
这是这里最复杂的简单示例。 ;) - Peter Lawrey
1
漏洞在哪里?GC后列表不是被释放了吗? - Aykut Kllic

0

看起来大多数答案都不是C风格的内存泄漏。

我想举一个库类的例子,它有一个错误会导致你得到一个内存不足异常。虽然这不是真正的内存泄漏,但它是一个意料之外的内存耗尽的例子。

public class Scratch {
    public static void main(String[] args) throws Exception {
        long lastOut = System.currentTimeMillis();
        File file = new File("deleteme.txt");

        ObjectOutputStream out;
        try {
            out = new ObjectOutputStream(
                    new FileOutputStream("deleteme.txt"));

            while (true) {
                out.writeUnshared(new LittleObject());
                if ((System.currentTimeMillis() - lastOut) > 2000) {
                    lastOut = System.currentTimeMillis();
                    System.out.println("Size " + file.length());
                    // out.reset();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class LittleObject implements Serializable {
    int x = 0;
}

您可以在JDK-4363937: ObjectOutputStream is creating a memory leak找到原始代码和错误描述。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接