我刚刚接受了一次采访,被要求用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,与迭代计数无关。
非常简单和令人惊讶。
其他回答
GUI代码中的一个常见示例是创建小部件/组件并向某个静态/应用程序范围的对象添加侦听器,然后在小部件被破坏时不删除侦听器。不仅会出现内存泄漏,而且性能也会受到影响,因为无论你听什么都会引发事件,所有的老听众都会被调用。
关于如何在Java中创建内存泄漏,有很多答案,但请注意采访中提出的问题。
“如何使用Java创建内存泄漏?”是一个开放式问题,其目的是评估开发人员的经验程度。
如果我问你“你有解决Java内存泄漏的经验吗?”,你的答案很简单:“是”。然后,我会继续说“你能给我举个例子来解决内存泄漏问题吗?”,你会给我一两个例子。
然而,当面试官问“如何用Java创建内存泄漏?”时,预期答案应该是以下几行:
我遇到了内存泄漏。。。(说什么时候)[这显示了我的经验]导致它的代码是。。。(解释代码)[你自己修的]我应用的修复基于。。。(解释修复)[这让我有机会询问修复的细节]我做的测试是。。。[让我有机会询问其他测试方法]我是这样记录的。。。[额外加分。如果你记录下来,那就好了]因此,有理由认为,如果我们按照相反的顺序执行,也就是说,得到我修复的代码,然后删除我的修复,我们就会出现内存泄漏。
当开发人员未能遵循这一思路时,我试图引导他/她问“你能给我一个Java如何泄漏内存的例子吗?”,然后问“你曾经修复过Java中的内存泄漏吗?”
请注意,我并不是在询问如何在Java中泄漏内存的示例。那太傻了。谁会对一个能够有效编写泄漏内存的代码的开发人员感兴趣?
另一种可能造成巨大内存泄漏的方法是保存对TreeMap的Map.Entry<K,V>的引用。
很难理解为什么这只适用于TreeMaps,但通过查看实现,原因可能是:TreeMap.Entry存储了对其同级的引用,因此,如果TreeMaps准备好被收集,但其他类保存了对其Map.Intry的引用,则整个Map将保留在内存中。
现实生活场景:
想象一下,有一个数据库查询返回一个大的TreeMap数据结构。人们通常使用TreeMaps作为元素插入顺序。
public static Map<String, Integer> pseudoQueryDatabase();
如果查询被多次调用,并且对于每个查询(因此,对于返回的每个Map),您在某个地方保存了一个条目,那么内存将不断增长。
考虑以下包装类:
class EntryHolder {
Map.Entry<String, Integer> entry;
EntryHolder(Map.Entry<String, Integer> entry) {
this.entry = entry;
}
}
应用程序:
public class LeakTest {
private final List<EntryHolder> holdersCache = new ArrayList<>();
private static final int MAP_SIZE = 100_000;
public void run() {
// create 500 entries each holding a reference to an Entry of a TreeMap
IntStream.range(0, 500).forEach(value -> {
// create map
final Map<String, Integer> map = pseudoQueryDatabase();
final int index = new Random().nextInt(MAP_SIZE);
// get random entry from map
for (Map.Entry<String, Integer> entry : map.entrySet()) {
if (entry.getValue().equals(index)) {
holdersCache.add(new EntryHolder(entry));
break;
}
}
// to observe behavior in visualvm
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
public static Map<String, Integer> pseudoQueryDatabase() {
final Map<String, Integer> map = new TreeMap<>();
IntStream.range(0, MAP_SIZE).forEach(i -> map.put(String.valueOf(i), i));
return map;
}
public static void main(String[] args) throws Exception {
new LeakTest().run();
}
}
在每次pseudoQueryDatabase()调用之后,映射实例应该准备好进行收集,但这不会发生,因为至少有一个Entry存储在其他地方。
根据您的jvm设置,应用程序可能会在早期因OutOfMemoryError而崩溃。
您可以从这个可视化虚拟机图中看到内存是如何保持增长的。
哈希数据结构(HashMap)不会发生同样的情况。
这是使用HashMap时的图形。
解决方案?只需直接保存键/值(您可能已经这样做了),而不是保存Map.Entry。
我在这里写了一个更广泛的基准。
Java1.6中的String.substring方法会造成内存泄漏。这篇博文解释了这一点:
SubString方法在Java中的工作原理-JDK1.7中修复了内存泄漏
我最近遇到了由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保持活动状态,从而导致内存泄漏。