提问



我刚接受采访,并被要求用Java创建内存泄漏。
毋庸置疑,我觉得自己很傻,甚至不知道如何开始创建一个。


一个例子是什么?

最佳参考


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



  1. 应用程序创建一个长时间运行的线程(或者使用线程池来更快地泄漏)。

  2. 线程通过(可选自定义)ClassLoader加载一个类。

  3. 该类分配大块内存(例如new byte[1000000]),在静态字段中存储对它的强引用,然后在ThreadLocal中存储对自身的引用。分配额外的内存是可选的(泄漏Class实例就足够了),但它会使泄漏工作更快。

  4. 该线程清除对自定义类或从中加载的ClassLoader的所有引用。

  5. 重复。



这是有效的,因为ThreadLocal保留对该对象的引用,该对象保持对其Class的引用,而Class又保持对其ClassLoader的引用。反过来,ClassLoader保持对它已加载的所有类的引用。


(在许多JVM实现中,尤其是在Java 7之前,情况更糟,因为Classes和ClassLoader直接分配到permgen并且从来都不是GC。但是,无论JVM如何处理类卸载,ThreadLocal仍然会阻止被回收的类对象。)


此模式的一个变体是,如果您经常重新部署碰巧以任何方式使用ThreadLocals的应用程序,那么应用程序容器(如Tomcat)可能会像筛子那样泄漏内存。 (由于应用程序容器使用了所描述的线程,每次重新部署应用程序时都会使用新的ClassLoader。)


更新:由于很多人不断要求它,这里有一些示例代码可以显示这种行为。[85]



其它参考1


静态字段保持对象引用[[esp final field]]


class MemorableClass {
    static final ArrayList list = new ArrayList(100);
}


在冗长的字符串上调用String.intern() [86]


String str=readString(); // read lengthy string any source db,textbox/jsp etc..
// This will place the string in memory pool from which you can't remove
str.intern();


(未关闭)开放流(文件,网络等......)


try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}


未关闭的连接


try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}


无法从JVM的垃圾收集器访问的区域,例如通过本机方法分配的内存


在Web应用程序中,某些对象存储在应用程序范围中,直到明确停止或删除应用程序。


getServletContext().setAttribute("SOME_MAP", map);


不正确或不恰当的JVM选项,例如IBM JDK上的noclassgc选项,可防止未使用的类垃圾回收


请参阅IBM jdk设置。[87]

其它参考2


一个简单的事情是使用不正确(或不存在)hashCode()equals()的HashSet,然后继续添加重复。而不是忽略重复,它只会增长,你将无法删除它们。


如果你想让这些坏键/元素闲逛,你可以使用静态字段


class BadKey {
   // no hashCode or equals();
   public final String key;
   public BadKey(String key) { this.key = key; }
}

Map map = System.getProperties();
map.put(new BadKey("key"), "value"); // Memory leak even if your threads die.

其它参考3


下面将有一个非显而易见的案例,其中Java泄漏,除了被遗忘的侦听器的标准情况,静态引用,哈希映射中的虚假/可修改键,或者只是没有任何机会结束其生命周期的线程。



  • File.deleteOnExit() - 总是泄漏字符串,如果字符串是子字符串,泄漏情况更糟(底层char [[]]也泄漏) - 在Java中  7子串也复制char[],所以后者不适用; @Daniel,不需要投票。



我将专注于线程,以显示大多数未管理线程的危险,甚至不希望触摸摆动。



  • Runtime.addShutdownHook并且不删除...然后使用removeShutdownHook由于ThreadGroup类中有关未启动线程的错误而无法收集它,有效地泄漏了ThreadGroup。 JGroup在GossipRouter中有漏洞。

  • 创建,但不是开始,Thread与上面的类别相同。

  • 创建一个线程继承ContextClassLoaderAccessControlContext,再加上ThreadGroup和任何InheritedThreadLocal,所有这些引用都是潜在的泄漏,以及由classloader和所有静态引用,以及ja-ja。整个j.u.c.Executor框架具有超级简单的ThreadFactory界面,但效果特别明显,但大多数开发人员都不知道潜伏的危险。此外,许多库都会根据请求启动线程(太多行业流行的库)。

  • ThreadLocal缓存;那些在许多情况下是邪恶的。我确信每个人都已经看到了很多基于ThreadLocal的简单缓存,这也是坏消息:如果线程持续超过预期生命的上下文ClassLoader,那么这是一个纯粹的小漏洞。除非确实需要,否则不要使用ThreadLocal缓存。

  • 当ThreadGroup本身没有线程时调用ThreadGroup.destroy(),但它仍然保留子ThreadGroups。一个错误的泄漏将阻止ThreadGroup从其父级中删除,但所有子级都变为不可枚举。

  • 使用WeakHashMap和值(in)直接引用键。没有堆转储,这是一个很难找到的。这适用于所有扩展Weak/SoftReference,可能会将一个硬引用保留回受保护对象。

  • 使用java.net.URL和HTTP(S)协议并从(!)加载资源。这个是特殊的,KeepAliveCache在系统ThreadGroup中创建一个新线程,它泄漏当前线程的上下文类加载器。当没有Activity线程存在时,线程是在第一个请求时创建的,所以要么你可能运气好,要么只是泄漏。泄漏已经在Java 7中修复了,创建线程的代码正确地删除了上下文类加载器。还有更多的情况(像ImageFetcher ,还修复了)创建类似线程的问题。

  • 使用InflaterInputStream在构造函数中传递new java.util.zip.Inflater()(例如PNGImageDecoder)而不调用inflater的end()。好吧,如果你只使用new传入构造函数,那就没有机会......是的,如果将它作为构造函数参数手动传递,则在流上调用close()不会关闭inflater。这是不是真正的泄漏,因为它会被终结者释放......当它认为有必要时。直到那一刻它吃掉本机内存如此糟糕,它可能导致Linux oom_killer肆无忌惮地杀死进程。主要问题是Java的最终确定非常不可靠,G1在7.0.2之前变得更糟。故事的道德:尽快释放本地资源;终结者太穷了。

  • java.util.zip.Deflater相同的情况。这个更糟糕,因为Deflater在Java中需要内存,即总是使用15位(最大)和8位内存级别(最大值为9)分配几百KB的本机内存。幸运的是,Deflater并没有被广泛使用,据我所知,JDK没有滥用。如果手动创建DeflaterInflater,请务必调用end()。最后两个中最好的部分:你无法通过正常的分析工具找到它们。



(我可以根据要求添加更多时间浪费。)


祝你好运,保持安全;泄漏是邪恶的!

其它参考4


这里的大多数例子都过于复杂。他们是边缘案件。有了这些例子,程序员犯了一个错误(比如没有重新定义equals/hashcode),或者被JVM/JAVA的一个角落案例(带有静态的类的负载......)咬了。我认为不是面试官想要的例子类型,甚至是最常见的案例。


但是内存泄漏的情况确实比较简单。垃圾收集器只释放不再引用的内容。我们作为Java开发人员并不关心内存。我们在需要时分配它并让它自动释放。很好。


但任何长期存在的应用程序都倾向于共享状态。它可以是任何东西,静力学,单身......通常非平凡的应用程序往往会制作复杂的对象图。只是忘记设置null或更多的引用经常忘记从集合中删除一个对象足以导致内存泄漏。


当然,如果处理不当,所有类型的侦听器(如UI侦听器),缓存或任何长期共享状态都会产生内存泄漏。应该理解的是,这不是Java角落案例,也不是垃圾收集器的问题。这是一个设计问题。我们设计我们为一个长期存在的对象添加一个监听器,但是当不再需要时我们不会删除监听器。我们缓存对象,但我们没有策略将它们从缓存中删除。


我们可能有一个复杂的图形来存储计算所需的先前状态。但是之前的状态本身与之前的状态有关,依此类推。


就像我们必须关闭SQL连接或文件一样。我们需要设置对null的正确引用并从集合中删除元素。我们将有适当的缓存策略(最大内存大小,元素数量或计时器)。允许侦听器得到通知的所有对象都必须同时提供addListener和removeListener方法。当这些通知符不再使用时,他们必须清除他们的听众列表。


内存泄漏确实是可能的,并且是完全可预测的。无需特殊语言功能或角落案例。内存泄漏可能表明某些内容可能缺失甚至是设计问题。

其它参考5


答案完全取决于面试官的想法。


在实践中是否有可能使Java泄漏?当然是,并且在其他答案中有很多例子。


但是有多个元问题可能会被问到?



  • 理论上完美的Java实现是否容易受到泄漏?

  • 候选人是否理解理论与现实之间的区别?

  • 候选人是否了解垃圾收集的工作原理?

  • 或者垃圾收集在理想情况下应该如何工作?

  • 他们是否知道可以通过原生界面调用其他语言?

  • 他们知道用其他语言泄漏记忆吗?

  • 候选人是否知道内存管理是什么,以及Java中幕后发生了什么?



我正在阅读你的元问题我在这次采访中可以使用的答案是什么。因此,我将专注于面试技巧而不是Java。我相信你更有可能重复在面试中不知道问题答案的情况,而不是在需要知道如何使Java泄漏。所以,希望这会有所帮助。


您可以为面试开发的最重要的技能之一是学习积极倾听问题并与面试官合作以提取他们的意图。这不仅可以让你以他们想要的方式回答他们的问题,而且还表明你有一些重要的沟通技巧。当涉及到许多同样有才华的开发人员之间的选择时,我会雇用那个在每次回复之前倾听,思考和理解的人。

其它参考6


如果您不了解JDBC,以下是一个非常毫无意义的示例。或者至少JDBC希望开发人员在丢弃它们或丢失对它们的引用之前关闭ConnectionStatementResultSet实例,而不是依赖于finalize的实现。 [88]


void doWork()
{
   try
   {
       Connection conn = ConnectionFactory.getConnection();
       PreparedStatement stmt = conn.preparedStatement("some query"); // executes a valid query
       ResultSet rs = stmt.executeQuery();
       while(rs.hasNext())
       {
          ... process the result set
       }
   }
   catch(SQLException sqlEx)
   {
       log(sqlEx);
   }
}


上面的问题是Connection对象没有关闭,因此物理连接将保持打开状态,直到垃圾收集器出现并看到它无法访问。 GC将调用finalize方法,但有些JDBC驱动程序没有实现finalize,至少与实现Connection.close的方式不同。由此产生的行为是,由于收集了无法访问的对象而将回收内存,因此可能无法回收与Connection对象关联的资源(包括内存)。


Connection [[s finalize方法没有清理所有内容的情况下,实际上可能会发现与数据库服务器的物理连接将持续几个垃圾收集周期,直到数据库服务器最终计算出来连接不存在(如果存在),应该关闭。


即使JDBC驱动程序要实现finalize,也有可能在最终确定期间抛出异常。由此产生的行为是,与现在休眠对象相关联的任何内存都不会被回收,因为finalize保证只被调用一次。


在对象最终化期间遇到异常的上述情况与另一个可能导致内存泄漏的情况有关 - 对象复活。对象复活通常是通过从另一个对象创建对象的强引用来有意识地完成的。当对象复活被滥用时,它将导致内存泄漏以及其他内存泄漏源。


你可以想出更多的例子 - 比如



  • 管理List实例,您只添加到列表中而不是从中删除(尽管您应该删除不再需要的元素),或者

  • 打开Socket s或File s,但在不再需要时关闭它们(类似于上面涉及Connection类的例子)。

  • 在关闭Java EE应用程序时不卸载单例。显然,加载单例类的类加载器将保留对类的引用,因此永远不会收集单例实例。当部署应用程序的新实例时,通常会创建一个新的类加载器,并且由于单例,前一个类加载器将继续存在。


其它参考7


可能是潜在内存泄漏的最简单示例之一,以及如何避免它,是ArrayList.remove(int)的实现:


public E remove(int index) {
    RangeCheck(index);

    modCount++;
    E oldValue = (E) elementData[index];

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index + 1, elementData, index,
                numMoved);
    elementData[--size] = null; // (!) Let gc do its work

    return oldValue;
}


如果你自己实现它,你会想到清除不再使用的数组元素(elementData[--size] = null)吗?这个参考可能会让一个巨大的物体活着......

其它参考8


每当你保持对不再需要的对象的引用时,就会发生内存泄漏。请参阅Java程序中的处理内存泄漏,以获取有关内存泄漏如何在Java中表现出来的示例以及您可以采取的措施。[89]

其它参考9


您可以使用 sun.misc.Unsafe 类进行内存泄漏。实际上,此服务类用于不同的标准类(例如,在 java.nio 类中)。 您无法直接创建此类的实例,但您可以使用反射来执行此操作


代码不能在Eclipse IDE中编译 - 使用命令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();
        }
    }

}

其它参考10


我可以从这里复制我的答案:
在Java中导致内存泄漏的最简单方法?


当计算机程序消耗内存但无法将其释放回操作系统时,会发生计算机科学(或泄漏,在此上下文中)的内存泄漏。 (维基百科)


简单的答案是:你不能Java。自动内存管理,并将释放你不需要的资源。你不能阻止这种情况发生。它总是能够释放资源。在具有手动内存管理的程序中,这是不同的。你可以使用malloc()在C中获得一些内存。要释放内存,需要malloc返回的指针并在其上调用free()。但是如果你不再拥有指针(覆盖或超过生命周期),那么你很遗憾无法释放这些内存,因此你会有内存泄漏。


到目前为止,所有其他答案都在我的定义中并不是真正的内存泄漏。它们都旨在快速地用无意义的东西填充内存。但是在任何时候你仍然可以取消引用你创建的对象,从而释放内存 - >没有泄漏。 acconrad的答案非常接近,但我不得不承认,因为他的解决方案实际上只是通过强制它在无限循环中崩溃垃圾收集器。


长的答案是:您可以通过使用JNI编写Java库来获取内存泄漏,JNI可能具有手动内存管理,因此存在内存泄漏。如果你调用这个库,你的java进程将泄漏内存。或者,您可以在JVM中出现错误,以便JVM丢失内存。 JVM中可能有bug,甚至可能有一些已知的bug,因为垃圾收集并不是那么简单,但它仍然是一个bug。通过设计这是不可能的。你可能会要求一些受影响的java代码通过这样的错误。抱歉,我不知道一个,无论如何,它在下一个Java版本中可能不再是一个bug。

其它参考11


这是一个简单/险恶的,来自http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29. [92]


public class StringLeaker
{
    private final String muchSmallerString;

    public StringLeaker()
    {
        // Imagine the whole Declaration of Independence here
        String veryLongString = "We hold these truths to be self-evident...";

        // The substring here maintains a reference to the internal char[]
        // representation of the original string.
        this.muchSmallerString = veryLongString.substring(0, 1);
    }
}


因为子串指的是原始字符串的内部表示,所以原始字符串保留在内存中。因此,只要你有一个StringLeaker,你就可以在内存中拥有整个原始字符串,即使你可能认为你只是坚持使用单字符串。


避免将不需要的引用存储到原始字符串的方法是执行以下操作:


...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...


对于增加的不良,您可能.intern()子字符串:


...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...


即使在丢弃StringLeaker实例之后,这样做也会将原始长字符串和派生子字符串保留在内存中。

其它参考12


获取在任何servlet容器中运行的任何Web应用程序(Tomcat,Jetty,Glassfish,等等......)。连续重新部署应用程序10次或者20次(可能只需触摸服务器的autodeploy目录中的WAR即可。


除非有人实际对此进行了测试,否则你很可能会在经过几次重新部署后得到一个OutOfMemoryError,因为应用程序没有注意自己进行清理。你甚至可以通过这个测试找到服务器中的错误。


问题是,容器的生命周期比应用程序的生命周期长。您必须确保容器可能对应用程序的对象或类的所有引用都可以进行垃圾回收。


如果只有一个引用在您的Web应用程序取消部署后仍然存在,则相应的类加载器将导致您的Web应用程序的所有类都无法进行垃圾回收。


您的应用程序启动的线程,ThreadLocal变量,日志记录附加程序是导致类加载器泄漏的一些常见嫌疑。

其它参考13


GUI代码中的一个常见示例是创建窗口小部件/组件并向某个静态/应用程序范围对象添加侦听器,然后在窗口小部件被销毁时不删除侦听器。您不仅会遇到内存泄漏,而且还会受到性能影响,因为无论您何时正在收听火灾事件,您的所有旧听众都会被调用。

其它参考14


也许通过JNI使用外部本机代码?


使用纯Java,几乎是不可能的。


但这是关于标准类型的内存泄漏,当您无法再访问内存时,它仍然由应用程序拥有。您可以保留对未使用对象的引用,或者在不关闭它们的情况下打开流。

其它参考15


我曾经有过一次与PermGen和XML解析有关的内存泄漏。
我们使用的XML解析器(我不记得它是哪一个)在标记名称上做了一个String.intern(),以便更快地进行比较。
我们的一位客户最好不要将数据值存储在XML属性或文本中,而是存储为标记名,因此我们有一个文档,如:


<data>
   <1>bla</1>
   <2>foo</>
   ...
</data>


实际上,他们并没有使用数字,而是使用更长的文本ID(大约20个字符),这些ID是独一无二的,每天的速度为10-15百万。这样每天就会产生200亿MB的垃圾,这种垃圾再也不需要了,而且从来没有GCed(因为它是PermGen)。我们将permgen设置为512 MB,因此内存异常(OOME)需要大约两天才能到达......

其它参考16


我最近遇到了log4j造成的内存泄漏情况。


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


为了存储特定于线程的标记,log4j的NDC类使用一个由Thread对象本身键入的Hashtable(而不是线程id),因此直到NDC标记在内存中保留所有挂起的对象。线程对象也保留在内存中。在我们的Web应用程序中,我们使用NDC标记带有请求ID的logoutput,以区分日志和单个请求。将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对象。因此,即使在线程完成其正在执行的操作之后,对NDC上下文Hastable仍然保持对hugeList的引用,从而导致内存泄漏。

其它参考17


我觉得有趣的是没有人使用内部类的例子。如果你有内部课程;它本身就维护了对包含类的引用。当然,从技术上讲,它不是内存泄漏,因为Java最终会将其清理干净;但这可能会导致课程比预期更长时间。


public class Example1 {
  public Example2 getNewExample2() {
    return this.new Example2();
  }
  public class Example2 {
    public Example2() {}
  }
}


现在,如果您调用Example1并获取Example2丢弃Example1,您将固有地仍然拥有指向Example1对象的链接。


public class Referencer {
  public static Example2 GetAnExample2() {
    Example1 ex = new Example1();
    return ex.getNewExample2();
  }

  public static void main(String[] args) {
    Example2 ex = Referencer.GetAnExample2();
    // As long as ex is reachable; Example1 will always remain in memory.
  }
}


我还听说过一个传闻,如果你的变量存在的时间超过特定的时间; Java假定它将永远存在,并且如果再也无法在代码中找到它,它实际上永远不会尝试清理它。但是是完全未经证实的。

其它参考18


什么是内存泄漏:



  • 这是由错误糟糕的设计造成的。

  • 这是浪费记忆。

  • 随着时间的推移会变得更糟。

  • 垃圾收集器无法清除它。



典型示例:


对象缓存是弄乱事物的好起点。


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对象具有对其他对象的引用,则其他对象也会引用其他对象。在某种程度上,你也可以认为这是某种内存泄漏(由不良设计引起)。

其它参考19


创建一个静态Map并继续添加对它的硬引用。那些永远不会是GCd。


public class Leaker {
    private static final Map<String, Object> CACHE = new HashMap<String, Object>();

    // Keep adding until failure.
    public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}

其它参考20


每个人总是忘记本机代码路由。这是泄漏的简单公式:



  1. 声明原生方法。

  2. 在本机方法中,调用malloc。不要打电话free

  3. 调用本机方法。



请记住,本机代码中的内存分配来自JVM堆。

其它参考21


您可以通过在该类的finalize方法中创建类的新实例来创建移动内存泄漏。如果终结器创建多个实例,则可以获得奖励。这里是一个简单的程序,它会在几秒钟之间泄漏整个堆。几分钟,具体取决于您的堆大小:


class Leakee {
    public void check() {
        if (depth > 2) {
            Leaker.done();
        }
    }
    private int depth;
    public Leakee(int d) {
        depth = d;
    }
    protected void finalize() {
        new Leakee(depth + 1).check();
        new Leakee(depth + 1).check();
    }
}

public class Leaker {
    private static boolean makeMore = true;
    public static void done() {
        makeMore = false;
    }
    public static void main(String[] args) throws InterruptedException {
        // make a bunch of them until the garbage collector gets active
        while (makeMore) {
            new Leakee(0).check();
        }
        // sit back and watch the finalizers chew through memory
        while (true) {
            Thread.sleep(1000);
            System.out.println("memory=" +
                    Runtime.getRuntime().freeMemory() + " / " +
                    Runtime.getRuntime().totalMemory());
        }
    }
}

其它参考22


最近我遇到了一种更为微妙的资源泄漏。
我们通过类加载器的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


Etvoilà:你在当前的工作目录中找到一个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文件加载现有资源非常重要,大小在这里很重要!


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


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


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


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


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


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


非常简单和令人惊讶。

其它参考23


我不认为有人说过这个:你可以通过覆盖finalize()方法来复活一个对象,这样finalize()会在某处存储一个引用。垃圾收集器只会在对象上调用一次,所以之后对象永远不会被破坏。

其它参考24


正如许多人所建议的那样,资源泄漏很容易引起 - 就像JDBC示例一样。实际内存泄漏有点困难 - 特别是如果你不依赖于JVM的破碎位来为你做这件事......


创建具有非常大的占用空间然后无法访问它们的对象的想法也不是真正的内存泄漏。如果没有任何东西可以访问它那么它将被垃圾收集,如果某些东西可以访问它那么它不是泄漏...


使用工作的一种方式 - 我不知道它是否仍然存在 - 是有一个三深的循环链。如在对象A中引用了对象B,对象B有对对象C和对象C的引用引用了对象A.GC非常聪明地知道如果A和B不可访问,可以安全地收集两个深链 - 如在A< - > B中 - 其他任何东西,但无法处理三通链......

其它参考25


面试官可能正在寻找一个像下面的代码那样的循环引用(顺便说一下,只有在使用引用计数的非常旧的JVM中泄漏内存,这不再是这种情况。)但它是一个非常模糊的问题,所以它是sa展示您对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意识的正常范围之外分配本机资源。

其它参考26


线程在终止之前不会被收集。它们是垃圾收集的根源。它们是为数不多的仅仅通过忘记它们或清除对它们的引用而无法回收的物体之一。


考虑:终止工作线程的基本模式是设置线程看到的一些条件变量。线程可以定期检查变量并将其用作终止信号。如果变量未声明volatile,那么线程可能看不到对变量的更改,因此它不知道要终止。或者想象一下某些线程是否想要更新共享对象,但是死锁同时试图锁定它。


如果您只有少数线程,这些错误可能会很明显,因为您的程序将无法正常工作。如果您有一个根据需要创建更多线程的线程池,那么可能不会注意到过时/卡住的线程,并且将无限累积,从而导致内存泄漏。线程可能会在您的应用程序中使用其他数据,因此也会阻止他们直接引用的任何数据被收集。


作为一个玩具示例:


static void leakMe(final Object object) {
    new Thread() {
        public void run() {
            Object o = object;
            for (;;) {
                try {
                    sleep(Long.MAX_VALUE);
                } catch (InterruptedException e) {}
            }
        }
    }.start();
}


调用System.gc()你喜欢什么,但传递给leakMe的对象永远不会死。


(*编辑*)

其它参考27


有许多不同的情况,内存会泄漏。我遇到的一个,它暴露了一个不应该在其他地方暴露和使用的地图。


public class ServiceFactory {

private Map<String, Service> services;

private static ServiceFactory singleton;

private ServiceFactory() {
    services = new HashMap<String, Service>();
}

public static synchronized ServiceFactory getDefault() {

    if (singleton == null) {
        singleton = new ServiceFactory();
    }
    return singleton;
}

public void addService(String name, Service serv) {
    services.put(name, serv);
}

public void removeService(String name) {
    services.remove(name);
}

public Service getService(String name, Service serv) {
    return services.get(name);
}

// the problematic api, which expose the map.
//and user can do quite a lot of thing from this api.
//for example, create service reference and forget to dispose or set it null
//in all this is a dangerous api, and should not expose 
public Map<String, Service> getAllServices() {
    return services;
}

}

// resource class is a heavy class
class Service {

}

其它参考28


我认为一个有效的例子可能是在线程被池化的环境中使用ThreadLocal变量。


例如,使用Servlet中的ThreadLocal变量与其他Web组件进行通信,让容器创建线程并在池中维护空闲的线程。 ThreadLocal变量如果没有被正确清理,将会存在,直到可能相同的Web组件覆盖它们的值。


当然,一旦确定,问题就可以轻松解决。

其它参考29


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


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


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


-Wes Tarle