我刚刚接受了一次采访,被要求用Java创建内存泄漏。
不用说,我觉得自己很傻,不知道如何开始创作。
什么样的例子?
我刚刚接受了一次采访,被要求用Java创建内存泄漏。
不用说,我觉得自己很傻,不知道如何开始创作。
什么样的例子?
当前回答
面试官可能在寻找一个循环引用,比如下面的代码(顺便说一下,这只会在使用引用计数的非常旧的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感知范围之外的本地资源。
其他回答
另一种可能造成巨大内存泄漏的方法是保存对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。
我在这里写了一个更广泛的基准。
您可以使用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中可用的工具监视应用程序的内存泄漏提供建议。它没有显示如何生成内存泄漏,但解释了如何使用最少的可用工具检测内存泄漏。
您需要首先监视Java内存消耗。
最简单的方法是使用JVM附带的jstat实用程序:
jstat -gcutil <process_id> <timeout>
它将报告每一代(年轻、老年和老年)的内存消耗和垃圾收集时间(年轻和完整)。
一旦您发现一个完整的垃圾收集执行得太频繁并且花费了太多时间,您就可以假设应用程序正在泄漏内存。
然后需要使用jmap实用程序创建内存转储:
jmap -dump:live,format=b,file=heap.bin <process_id>
然后需要使用内存分析器(例如Eclipse memory Analyzer(MAT))分析heap.bin文件。
MAT将分析内存并向您提供有关内存泄漏的可疑信息。
也许通过JNI使用外部本机代码?
使用纯Java,这几乎是不可能的。
但这是一种“标准”类型的内存泄漏,即您无法再访问内存,但它仍然属于应用程序。相反,您可以保留对未使用对象的引用,或者打开流而不关闭它们。