用Java创建内存泄漏

我刚刚接受了一次采访,并被要求用Java创建内存泄漏。毋庸置疑,我对于如何开始创建一个自己而言毫无头绪。

一个例子会是什么?

答案


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

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

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

(在许多JVM实现中,特别是在Java 7之前,这种情况变得更糟,因为Classes和ClassLoader被直接分配到了permgen中,并且根本没有GC’d。但是,无论JVM如何处理类卸载,ThreadLocal仍然会阻止类对象被回收。)

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

更新:由于许多人不断询问它,下面是一些示例代码,显示了这种行为


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

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

调用String.intern()冗长的String

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选项,例如noclassgcIBM JDK上用于阻止未使用的类垃圾回收的选项

请参阅IBM jdk设置


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

如果你想让这些不好的键/元素挂在身边,你可以使用类似的静态字段

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.

下面将会有一个非常明显的例子,除了标准的被遗忘的监听器,静态引用,hashmaps中的伪造/可修改密钥,或者只是线程被卡住而没有任何机会结束它们的生命周期之外,Java漏洞。

  • File.deleteOnExit() – 总是泄漏字符串, 如果字符串是一个子字符串,则泄漏更加严重(底层字符[]也会泄漏)– 在Java 7子字符串中也会复制char[],所以后者不适用 ; 虽然,丹尼尔不需要投票。

我将专注于线程,主要显示非管理线程的危险,不希望甚至碰到摆动。

  • Runtime.addShutdownHook而不是删除…然后,即使使用removeShutdownHook,由于ThreadGroup类中有关未启动线程的错误,它可能无法收集,从而有效地泄漏了ThreadGroup。JGroup在GossipRouter中有泄漏。
  • 创建,但不是开始,a Thread进入与上面相同的类别。
  • 创建一个线程继承了ContextClassLoaderAccessControlContext,加上ThreadGroupInheritedThreadLocal所有这些引用都是潜在的泄漏,以及由类加载器加载的所有类以及所有静态引用和ja-ja。整个jucExecutor框架具有超级简单的ThreadFactory界面,这一效果尤为明显,但大多数开发人员不知道潜在的危险。许多图书馆也可以根据要求启动主题(太多行业流行的图书馆)。
  • ThreadLocal高速缓存; 在很多情况下这些都是邪恶的。我确信每个人都已经看到了很多基于ThreadLocal的简单缓存,还有一个坏消息:如果线程继续超出预期的上下文ClassLoader的生命期,那么这是一个纯粹的小漏洞。除非确实需要,否则不要使用ThreadLocal缓存。
  • ThreadGroup.destroy()当ThreadGroup本身没有线程时调用,但仍保留子线程组。一个不好的泄漏会阻止ThreadGroup从其父项中移除,但是所有的子项都会变得无法枚举。
  • 使用WeakHashMap和值(in)直接引用密钥。这是一个很难找到没有堆转储。这适用于所有扩展Weak/SoftReference,可能会保留对守护对象的硬引用。
  • 使用java.net.URLHTTP(S)协议并从(!)加载资源。这个是特殊的,KeepAliveCache在系统ThreadGroup中创建一个新线程,它泄漏当前线程的上下文类加载器。线程是在没有活动线程存在时根据第一个请求创建的,所以无论你是幸运还是泄漏。该漏洞已在Java 7中修复,创建线程的代码正确地删除了上下文类加载器。还有几个案例(像ImageFetcher也是固定的)创建类似的线程。
  • 使用InflaterInputStream传入new java.util.zip.Inflater()构造函数(PNGImageDecoder例如)而不是调用end()inflater。那么,如果你只是传入构造函数new,没有机会…是的,调用close()流不会关闭inflater,如果它手动传递为构造函数参数。这不是真正的泄漏,因为它会被终结者发布……当它认为有必要的时候。直到那一刻,它严重地吃掉了本机内存,它可能会导致Linux oom_killer不受惩罚地杀死这个进程。主要的问题是Java中的定型非常不可靠,G1让它变得更糟,直到7.0.2。故事的道德:尽快释放本土资源; 终结者太穷了。
  • 与之相同的情况java.util.zip.Deflater。由于Deflater在Java中是饥饿的内存,也就是说总是使用15位(最大值)和8个内存级别(最大值为9)来分配几百KB的本机内存,所以这个情况更糟糕。幸运的是,Deflater没有被广泛使用,据我所知,JDK没有任何滥用。end()如果您手动创建Deflater或,则始终致电Inflater。最后两个最好的部分:你不能通过普通的分析工具找到它们。

(我可以添加更多的时间浪费,我遇到了请求。)

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


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

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

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

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

我们可能有一个复杂的图表,用于存储计算所需的以前的状态。但之前的状态本身与之前的状态有联系等等。

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

内存泄漏的确是可能的,而且完全可以预测。无需特殊语言功能或角落案例。内存泄漏或者是某种可能缺失的指标,甚至是设计问题。


答案完全取决于面试官认为他们在问什么。

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

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

  • 理论上“完美”的Java实现容易泄漏吗?
  • 候选人是否理解理论与现实之间的差异?
  • 候选人是否了解垃圾收集的工作原理?
  • 或者垃圾收集在理想情况下应该如何工作?
  • 他们知道他们可以通过本地接口调用其他语言吗?
  • 他们是否知道以其他语言泄漏内存?
  • 候选人是否知道内存管理是什么,以及Java在幕后发生了什么?

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

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

添加评论

友情链接:蝴蝶教程