如何在 Java 中创建内存泄漏?

我刚刚接受采访,并被要求使用 Java 造成内存泄漏
不用说,我对如何开始创建它一无所知。

一个例子是什么?

答案

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

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

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

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

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

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

ClassLoader在创建泄漏中并没有真正起作用,它只是由于以下附加参考链而使泄漏更糟:示例类→ ClassLoader →它已加载的所有类。在许多 JVM 实现中,甚至更糟在 Java 7 之前,因为类和ClassLoader是直接分配到 permgen 中的,所以根本不会进行垃圾回收。)

这种模式的一个变种是,如果您频繁地重新部署恰巧使用ThreadLocal的应用程序,而应用程序容器(例如 Tomcat) ThreadLocal某种方式指向自身,那么它可以像筛子一样泄漏内存。发生这种情况的原因可能很多,而且很难调试和 / 或修复。

更新 :由于很多人一直在要求它,因此以下示例代码展示了这种行为

静态字段保存对象参考 [特别是最终字段]

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

在冗长的 String 上调用String.intern()

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 设置

一个简单的事情是使用带有不正确(或不存在)的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.

除了被遗忘的侦听器,静态引用,哈希映射中的伪造 / 可修改键的标准情况,或者只是线程卡住而没有任何机会终止其生命周期的标准情况,下面将出现 Java 泄漏的非明显情况。

  • File.deleteOnExit() - 始终泄漏字符串, 如果字符串是子字符串,则泄漏会更加严重(底层的 char [] 也被泄漏) - 在 Java 7 中,子字符串还复制了char[] ,因此后者不适用 ; @Daniel,不过不需要投票。

我将集中讨论线程,以大体上显示非托管线程的危险,甚至不希望摆动。

  • Runtime.addShutdownHook而不是不删除...,即使由于 ThreadGroup 类中关于未启动的线程的错误(可能无法收集未启动的线程)导致的错误,即使使用 removeShutdownHook 也会有效地泄漏 ThreadGroup。 JGroup 在 GossipRouter 中泄漏。

  • 创建(而不是启动) Thread与上述类别相同。

  • 创建线程将继承ContextClassLoaderAccessControlContext ,以及ThreadGroup和任何InheritedThreadLocal ,所有这些引用都是潜在的泄漏,包括类加载器加载的整个类,所有静态引用以及 ja-ja。在具有超简单ThreadFactory接口的整个 jucExecutor 框架中,效果尤其明显,但是大多数开发人员都不知道潜伏的危险。另外,很多库都根据请求启动线程(太多了行业流行的库)。

  • ThreadLocal缓存;在许多情况下,这些都是邪恶的。我敢肯定,每个人都已经看到了很多基于 ThreadLocal 的简单缓存,这是个坏消息:如果线程在类 ClassLoader 上下文中的寿命超过预期,那将是一个很好的泄漏。除非确实需要,否则不要使用 ThreadLocal 缓存。

  • 当 ThreadGroup 本身没有线程,但仍保留子 ThreadGroups 时,调用ThreadGroup.destroy() 。严重的泄漏将阻止 ThreadGroup 从其父级中移除,但是所有子级都变得无法枚举。

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

  • 使用具有 HTTP(S)协议的java.net.URL并从(!)加载资源。这很特殊, KeepAliveCache在系统 ThreadGroup 中创建了一个新线程,该线程泄漏了当前线程的上下文类加载器。当不存在活动线程时,将在第一个请求时创建该线程,因此您可能会很幸运,或者只是泄漏。 Java 7 中已经修复了该泄漏,并且正确创建线程的代码删除了上下文类加载器。还有更多的情况( 像 ImageFetcher 也修复了创建类似线程的问题)。

  • 使用InflaterInputStream通过new java.util.zip.Inflater()在构造函数( PNGImageDecoder例如),而不是调用end()充气的。好吧,如果您传入new的构造函数,就没有机会了…… 是的,如果手动将其作为构造函数参数传递,则在流上调用close()不会关闭充气机。这不是真正的泄漏,因为它将由终结器释放…… 在其认为必要时。直到那一刻,它严重消耗了本机内存,可能导致 Linux oom_killer 毫无惩罚地杀死进程。主要问题是 Java 中的终结处理非常不可靠,G1 恶化到 7.0.2。故事的寓意:尽快释放本机资源;终结器太差了。

  • java.util.zip.Deflater相同。由于 Deflater 在 Java 中占用大量内存,因此这一情况要糟得多,即,始终使用 15 位(最大)和 8 个内存级别(最大 9 个)分配数百 KB 的本机内存。幸运的是, Deflater并未得到广泛使用,据我所知 JDK 不包含任何误用。如果您手动创建DeflaterInflater始终调用end() 。最后两个最好的部分: 您无法通过可用的常规配置工具找到它们。

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

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

这里的大多数示例都是 “太复杂”。他们是极端情况。在这些示例中,程序员犯了一个错误(例如,不重新定义 equals / hashcode),或者被 JVM / JAVA 的一个极端情况(带有静态类的负载...)所困扰。我认为这不是面试官想要的例子,甚至不是最常见的情况。

但是确实存在内存泄漏的简单情况。垃圾收集器仅释放不再引用的内容。作为 Java 开发人员,我们不关心内存。我们在需要时分配它,并使其自动释放。精细。

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

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

我们可能有一个复杂的图,它存储了计算所需的先前状态。但是以前的状态本身链接到之前的状态,依此类推。

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

确实确实有可能发生内存泄漏,并且完全可以预测。无需特殊的语言功能或特殊情况。内存泄漏要么表明某些东西可能丢失,要么表明设计问题。

答案完全取决于访问者认为他们要问的内容。

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

但是可能会问多个元问题?

  • 理论上 “完美” 的 Java 实现是否容易受到泄漏的影响?
  • 候选人是否了解理论与现实之间的区别?
  • 候选人是否了解垃圾收集的工作原理?
  • 还是应该在理想情况下进行垃圾收集?
  • 他们是否知道可以通过本机界面调用其他语言?
  • 他们知道以其他语言泄漏内存吗?
  • 候选人甚至不知道什么是内存管理以及 Java 幕后发生了什么吗?

我正在将您的元问题阅读为 “在这种采访情况下我可以使用的答案是什么”。因此,我将专注于面试技巧而不是 Java。我相信您比在需要知道如何使 Java 泄漏的地方更容易重复这种情况,即在面试中不知道问题的答案。因此,希望这会有所帮助。

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

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

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对象关联的资源(包括内存)可能根本不会回收。

如果Connectionfinalize方法无法清除所有内容,则实际上可能会发现与数据库服务器的物理连接将持续多个垃圾回收周期,直到数据库服务器最终确定该连接未激活为止(如果确实如此),则应将其关闭。

即使 JDBC 驱动程序要实现finalize ,也有可能在finalize期间引发异常。最终的结果是,与 finalt 对象相关联的任何内存都不会被回收,因为finalize只能被调用一次。

上面在对象完成期间遇到异常的方案与可能导致内存泄漏的另一种方案 - 对象复活有关。对象复活通常是通过从另一个对象最终确定对对象的强烈引用来有意识地完成的。当对象复活被滥用时,将导致内存泄漏以及其他内存泄漏源。

您还可以想到更多示例,例如

  • 管理仅添加到列表而不删除列表的List实例(尽管您应该摆脱不再需要的元素),或者
  • 打开SocketFile ,但是在不再需要它们时不关闭它们(类似于上面涉及Connection类的示例)。
  • 关闭 Java EE 应用程序时不卸载 Singleton。显然,加载单例类的 Classloader 将保留对该类的引用,因此单例实例将永远不会被收集。部署应用程序的新实例时,通常会创建一个新的类加载器,并且由于单例的原因,以前的类加载器将继续存在。

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 )?该引用可能会使一个巨大的对象保持活动状态...

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

您可以使用sun.misc.Unsafe类使内存泄漏。实际上,此服务类在不同的标准类中使用(例如,在java.nio类中)。 您不能直接创建此类的实例 ,但是您可以使用反射来实现

代码无法在 Eclipse IDE 中进行编译 - 使用命令javac对其进行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();
        }
    }

}