我刚刚接受了一次采访,被要求用Java创建内存泄漏。

不用说,我觉得自己很傻,不知道如何开始创作。

什么样的例子?


当前回答

什么是内存泄漏:

这是由错误或不良设计引起的。这是在浪费记忆。随着时间的推移,情况会变得更糟。垃圾收集器无法清理它。

典型示例:

对象缓存是一个很好的起点,可以让事情变得一团糟。

private static final Map<String, Info> myCache = new HashMap<>();

public void getInfo(String key)
{
    // uses cache
    Info info = myCache.get(key);
    if (info != null) return info;

    // if it's not in cache, then fetch it from the database
    info = Database.fetch(key);
    if (info == null) return null;

    // and store it in the cache
    myCache.put(key, info);
    return info;
}

您的缓存不断增长。很快整个数据库就被吸进了内存。更好的设计使用LRUMap(仅将最近使用的对象保存在缓存中)。

当然,你可以让事情变得更加复杂:

使用ThreadLocal构造。添加更复杂的参考树。或由第三方库引起的泄漏。

经常发生的情况:

如果此Info对象引用了其他对象,则这些对象也引用了其他的对象。在某种程度上,您也可以认为这是某种内存泄漏(由糟糕的设计导致)。

其他回答

我认为还没有人说过这一点:你可以通过重写finalize()方法来复活一个对象,这样finalize)就可以在某个地方存储对它的引用。垃圾回收器只会在对象上调用一次,因此在此之后,对象将永远不会被销毁。

在具有自己生命周期的类中随意使用非静态内部类。

在Java中,非静态内部类和匿名类对其外部类具有隐式引用。另一方面,静态内部类则不然。

下面是一个常见的Android内存泄漏示例,但这并不明显:

public class SampleActivity extends Activity {

  private final Handler mLeakyHandler = new Handler() { // Non-static inner class, holds the reference to the SampleActivity outer class
    @Override
    public void handleMessage(Message msg) {
      // ...
    }
  }

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for a long time.
    mLeakyHandler.postDelayed(new Runnable() {//here, the anonymous inner class holds the reference to the SampleActivity class too
      @Override
      public void run() {
     //....
      }
    }, SOME_TOME_TIME);

    // Go back to the previous Activity.
    finish();
  }}

这将防止活动上下文被垃圾收集。

任何时候,只要您保留对不再需要的对象的引用,就会出现内存泄漏。请参阅处理Java程序中的内存泄漏,以了解内存泄漏如何在Java中表现出来以及您可以如何处理它。

我最近遇到了一种更微妙的资源泄漏。我们通过类加载器的getResourceAsStream打开资源,但碰巧输入流句柄没有关闭。

嗯,你可能会说,真是个白痴。

嗯,有趣的是:通过这种方式,您可以泄漏底层进程的堆内存,而不是JVM的堆内存。

您只需要一个jar文件,其中包含一个将从Java代码中引用的文件。jar文件越大,分配内存的速度越快。

您可以使用以下类轻松创建这样的jar:

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

public class BigJarCreator {
    public static void main(String[] args) throws IOException {
        ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
        zos.putNextEntry(new ZipEntry("resource.txt"));
        zos.write("not too much in here".getBytes());
        zos.closeEntry();
        zos.putNextEntry(new ZipEntry("largeFile.out"));
        for (int i=0 ; i<10000000 ; i++) {
            zos.write((int) (Math.round(Math.random()*100)+20));
        }
        zos.closeEntry();
        zos.close();
    }
}

只需粘贴到名为BigJarCreator.java的文件中,从命令行编译并运行它:

javac BigJarCreator.java
java -cp . BigJarCreator

等等:您在当前工作目录中找到一个jar存档,其中包含两个文件。

让我们创建第二个类:

public class MemLeak {
    public static void main(String[] args) throws InterruptedException {
        int ITERATIONS=100000;
        for (int i=0 ; i<ITERATIONS ; i++) {
            MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
        }
        System.out.println("finished creation of streams, now waiting to be killed");

        Thread.sleep(Long.MAX_VALUE);
    }

}

这个类基本上什么都不做,只创建未引用的InputStream对象。这些对象将立即被垃圾收集,因此不会影响堆大小。对于我们的示例来说,从jar文件加载现有资源很重要,这里的大小很重要!

如果您有疑问,请尝试编译并启动上面的类,但确保选择了合适的堆大小(2MB):

javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak

在这里您不会遇到OOM错误,因为没有保留引用,所以无论您在上面的示例中选择了多大的ITERATIONS,应用程序都将继续运行。除非应用程序执行wait命令,否则进程的内存消耗(在顶部(RES/RSS)或进程资源管理器中可见)会增加。在上面的设置中,它将分配大约150 MB的内存。

如果希望应用程序安全运行,请在创建输入流的位置关闭输入流:

MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();

并且您的进程不会超过35MB,与迭代计数无关。

非常简单和令人惊讶。

在Java中,“内存泄漏”主要是因为您使用了太多内存,这与在C中不同,在C中,您不再使用内存,而是忘记返回(释放)内存。当面试官询问Java内存泄漏时,他们询问的是JVM内存使用情况,但似乎一直在增加,他们认为定期重新启动JVM是最好的解决方案(除非面试官非常精通技术)。

所以,回答这个问题,就像他们问JVM内存使用量随时间增长的原因一样。好的答案是在HttpSessions中存储太多数据,超时时间过长,或者内存缓存(singleton)实现不佳,从不刷新旧条目。另一个可能的答案是拥有大量JSP或动态生成的类。类被加载到一个名为PermGen的内存区域,该区域通常很小,大多数JVM不实现类卸载。