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

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

什么样的例子?


当前回答

JDK 1.7之前内存泄漏的实时示例:

假设您读取了一个包含1000行文本的文件,并将其保存在String对象中:

String fileText = 1000 characters from file
fileText = fileText.subString(900, fileText.length());

在上面的代码中,我最初读取了1000个字符,然后执行了子字符串,只获得最后100个字符。现在,fileText应该只引用100个字符,所有其他字符都应该被垃圾收集,因为我丢失了引用,但是在JDK1.7之前,substring函数间接引用了最后100个字符的原始字符串,并阻止了整个字符串的垃圾收集,而整个1000个字符将一直保存在内存中,直到您丢失了对子字符串的引用。

您可以创建一个类似于上述的内存泄漏示例。

其他回答

这里有一个在纯Java中创建真正的内存泄漏(运行代码无法访问但仍存储在内存中的对象)的好方法:

应用程序创建一个长时间运行的线程(或者使用线程池更快地泄漏)。线程通过(可选的自定义)ClassLoader加载类。该类分配一大块内存(例如新字节[10000000]),在静态字段中存储对它的强引用,然后在ThreadLocal中存储对自身的引用。分配额外的内存是可选的(泄漏类实例就足够了),但这会使泄漏工作得更快。应用程序清除对自定义类或从中加载该类的ClassLoader的所有引用。重复

由于ThreadLocal在Oracle的JDK中的实现方式,这会造成内存泄漏:

每个线程都有一个私有字段threadLocals,它实际上存储线程本地值。此映射中的每个键都是对ThreadLocal对象的弱引用,因此在ThreadLocal对象被垃圾收集后,其条目将从映射中删除。但每个值都是一个强引用,因此当一个值(直接或间接)指向作为其键的ThreadLocal对象时,只要线程存在,该对象既不会被垃圾收集,也不会从映射中删除。

在本例中,强引用链如下所示:

线程对象→ threadLocals映射→ 示例类的实例→ 示例类→ 静态ThreadLocal字段→ ThreadLocal对象。

(ClassLoader在创建泄漏时并没有真正发挥作用,它只是因为这个额外的引用链而使泄漏变得更糟:example类→ 类加载器→ 它加载的所有类。在许多JVM实现中,尤其是在Java7之前,情况更糟,因为类和ClassLoader被直接分配到permagen中,根本不会被垃圾收集。)

这种模式的一个变体是,如果您经常重新部署碰巧使用ThreadLocal的应用程序,而这些应用程序在某种程度上指向自己,那么应用程序容器(如Tomcat)会像筛子一样泄漏内存。这种情况可能有许多微妙的原因,并且通常很难调试和/或修复。

更新:由于很多人一直在要求它,这里有一些示例代码显示了这种行为。

我最近遇到了由log4j引起的内存泄漏情况。

Log4j有一种称为嵌套诊断上下文(NDC)的机制,它是一种区分不同来源的交织日志输出的工具。NDC工作的粒度是线程,因此它区分不同线程的日志输出。

为了存储线程特定的标记,log4j的NDC类使用一个Hashtable,该Hashtable由thread对象本身(而不是线程id)键控,因此直到NDC标记保留在内存中,挂在线程对象上的所有对象也保留在内存。在我们的web应用程序中,我们使用NDC标记带有请求id的登录,以将日志与单个请求区分开来。将NDC标记与线程关联的容器在返回请求响应时也会将其删除。在处理请求的过程中,产生了一个子线程,类似于以下代码:

pubclic class RequestProcessor {
    private static final Logger logger = Logger.getLogger(RequestProcessor.class);
    public void doSomething()  {
        ....
        final List<String> hugeList = new ArrayList<String>(10000);
        new Thread() {
           public void run() {
               logger.info("Child thread spawned")
               for(String s:hugeList) {
                   ....
               }
           }
        }.start();
    }
}    

因此,NDC上下文与派生的内联线程相关联。这个NDC上下文的关键线程对象是一个内联线程,它挂着hugeList对象。因此,即使线程完成了它正在做的事情,对hugeList的引用也会被NDC上下文Hastable保持活动状态,从而导致内存泄漏。

您可以使用sun.misc.Unsafe类进行内存泄漏。事实上,这个服务类用于不同的标准类(例如java.nio类)。不能直接创建此类的实例,但可以使用反射来获取实例。

代码不会在EclipseIDE中编译-使用javac命令编译代码(编译期间,您会收到警告)

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import sun.misc.Unsafe;

public class TestUnsafe {

    public static void main(String[] args) throws Exception{
        Class unsafeClass = Class.forName("sun.misc.Unsafe");
        Field f = unsafeClass.getDeclaredField("theUnsafe");
        f.setAccessible(true);
        Unsafe unsafe = (Unsafe) f.get(null);
        System.out.print("4..3..2..1...");
        try
        {
            for(;;)
                unsafe.allocateMemory(1024*1024);
        } catch(Error e) {
            System.out.println("Boom :)");
            e.printStackTrace();
        }
    }
}

面试官可能在寻找一个循环引用,比如下面的代码(顺便说一下,这只会在使用引用计数的非常旧的JVM中泄漏内存,而现在情况已经不是这样了)。但这是一个非常模糊的问题,因此这是展示您对JVM内存管理理解的绝佳机会。

class A {
    B bRef;
}

class B {
    A aRef;
}

public class Main {
    public static void main(String args[]) {
        A myA = new A();
        B myB = new B();
        myA.bRef = myB;
        myB.aRef = myA;
        myA=null;
        myB=null;
        /* at this point, there is no access to the myA and myB objects, */
        /* even though both objects still have active references. */
    } /* main */
}

然后您可以解释,使用引用计数,上面的代码会泄漏内存。但大多数现代JVM不再使用引用计数。大多数都使用一个清理垃圾收集器,它实际上会收集这些内存。

接下来,您可能会解释创建一个具有底层本机资源的Object,如下所示:

public class Main {
    public static void main(String args[]) {
        Socket s = new Socket(InetAddress.getByName("google.com"),80);
        s=null;
        /* at this point, because you didn't close the socket properly, */
        /* you have a leak of a native descriptor, which uses memory. */
    }
}

然后您可以解释这在技术上是内存泄漏,但实际上泄漏是由JVM中的本机代码分配底层本机资源造成的,而Java代码没有释放这些资源。

最后,对于现代JVM,您需要编写一些Java代码来分配JVM感知范围之外的本地资源。

面试官可能一直在寻找一个循环参考解决方案:

    public static void main(String[] args) {
        while (true) {
            Element first = new Element();
            first.next = new Element();
            first.next.next = first;
        }
    }

这是引用计数垃圾收集器的典型问题。然后,您可以礼貌地解释JVM使用了一种更复杂的算法,它没有这种限制。