我刚刚接受了一次采访,被要求用Java创建内存泄漏。
不用说,我觉得自己很傻,不知道如何开始创作。
什么样的例子?
我刚刚接受了一次采访,被要求用Java创建内存泄漏。
不用说,我觉得自己很傻,不知道如何开始创作。
什么样的例子?
当前回答
下面将有一个不明显的Java泄漏案例,除了被遗忘的侦听器、静态引用、哈希图中的伪/可修改键,或者只是线程被卡住而没有机会结束其生命周期的标准案例之外。
File.deleteOnExit()-总是泄漏字符串,如果字符串是子字符串,则泄漏更严重(底层的char[]也泄漏)-在Java 7中,子字符串也会复制char[],因此后者不适用@丹尼尔,不过不需要投票。
我将集中讨论线程,以展示非托管线程的危险性,甚至不希望触及摆动。
Runtime.addShutdownHook,不删除。。。然后,即使使用removeShutdownHook,由于ThreadGroup类中关于未启动线程的错误,它也可能无法被收集,从而有效地泄漏了ThreadGroup。JGroup在GossipRouter中有漏洞。创建一个线程,但不是启动它,它属于与上面相同的类别。创建线程继承ContextClassLoader和AccessControlContext,加上ThreadGroup和任何InheritedThreadLocal,所有这些引用都是潜在的泄漏,以及类加载器加载的所有类和所有静态引用,以及ja-ja。这种效果在整个j.u.c.Executor框架中尤其明显,该框架具有超简单的ThreadFactory接口,但大多数开发人员对潜在的危险一无所知。此外,许多库确实会根据请求启动线程(太多行业流行的库)。ThreadLocal缓存;这些在很多情况下都是邪恶的。我相信每个人都看到过很多基于ThreadLocal的简单缓存,但坏消息是:如果线程在上下文ClassLoader的生命周期中继续运行超过预期,这是一个非常好的小泄漏。除非确实需要,否则不要使用ThreadLocal缓存。当ThreadGroup本身没有线程,但仍保留子ThreadGroups时,调用ThreadGroup.destroy()。一个严重的泄漏,将阻止ThreadGroup从其父级中删除,但所有子级都无法枚举。使用WeakHashMap和值(in)直接引用键。如果没有堆转储,这很难找到。这适用于可能将硬引用保留回受保护对象的所有扩展弱/软引用。将java.net.URL与HTTP(S)协议一起使用,并从(!)加载资源。这一个是特殊的,KeepAliveCache在系统ThreadGroup中创建了一个新线程,该线程泄漏了当前线程的上下文类加载器。当不存在活动线程时,线程会在第一个请求时创建,因此您可能会幸运,或者只是泄漏。泄漏在Java7中已经修复,创建线程的代码正确地删除了上下文类加载器。创建类似线程的情况很少(如ImageFetcher,也已修复)。使用充气器InputStream在构造函数(例如PNGImageDecoder)中传递新的java.util.zip充气器(),而不调用充气器的end()。好吧,如果你只传递一个新的构造函数,就没有机会。。。是的,如果将其作为构造函数参数手动传递,则对流调用close()不会关闭充气机。这不是真正的泄漏,因为它将由终结器释放。。。当它认为有必要时。直到那一刻,它会严重消耗本地内存,导致Linux oom_killer肆无忌惮地终止进程。主要的问题是,在Java中完成是非常不可靠的,G1使其更糟,直到7.0.2。故事的寓意:尽快释放本土资源;终结器太差了。与java.util.zip.Deflater的情况相同。这一情况更糟,因为Deflater在java中需要内存,即总是使用15位(最大值)和8个内存级别(最大值为9)来分配数百KB的本地内存。幸运的是,Deflater没有被广泛使用,据我所知,JDK没有任何误用。如果手动创建放气器或充气器,请始终调用end()。最后两种方法中最棒的一点是:您无法通过常规的分析工具找到它们。
(我可以根据要求再添加一些我遇到的时间浪费者。)
祝你好运,保持安全;泄漏是邪恶的!
其他回答
我想就如何使用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将分析内存并向您提供有关内存泄漏的可疑信息。
我认为,一个有效的例子可能是在线程集中的环境中使用ThreadLocal变量。
例如,使用Servlet中的ThreadLocal变量与其他web组件通信,让容器创建线程,并在池中维护空闲线程。ThreadLocal变量如果没有正确清理,将一直存在,直到同一个web组件覆盖它们的值。
当然,一旦确定,问题很容易解决。
Java中有很多内存泄漏的好例子,我将在这个答案中提到其中两个。
示例1:
以下是《有效Java,第三版》(第7项:消除过时的对象引用)一书中的一个内存泄漏的好例子:
// Can you spot the "memory leak"?
public class Stack {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private Object[] elements;
private int size = 0;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
return elements[--size];
}
/*** Ensure space for at least one more element, roughly* doubling the capacity each time the array needs to grow.*/
private void ensureCapacity() {
if (elements.length == size) elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
本书的这一段描述了为什么此实现会导致内存泄漏:
如果堆栈增长然后收缩即使程序使用堆栈没有对它们的更多引用。这是因为堆栈维护对这些对象的过时引用。一个过时的引用只是一个永远不会被取消引用的引用再一次在这种情况下元素数组已过时。活动部分包括索引小于大小的元素
以下是本书解决此内存泄漏的解决方案:
解决这类问题的方法很简单:null out引用一旦过时。在Stack类的情况下,对项目的引用一经弹出就过时从堆栈中删除。pop方法的修正版本如下所示:
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
但我们如何防止内存泄漏的发生?这是本书中一个很好的警告:
一般来说,每当类管理自己的内存时,程序员应该警惕内存泄漏。每当元素元素中包含的任何对象引用都应该为空。
示例2:
观察者模式也会导致内存泄漏。您可以在以下链接中阅读此模式:观察者模式。
这是观察者模式的一种实现:
class EventSource {
public interface Observer {
void update(String event);
}
private final List<Observer> observers = new ArrayList<>();
private void notifyObservers(String event) {
observers.forEach(observer -> observer.update(event)); //alternative lambda expression: observers.forEach(Observer::update);
}
public void addObserver(Observer observer) {
observers.add(observer);
}
public void scanSystemIn() {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
notifyObservers(line);
}
}
}
在这个实现中,EventSource(在Observer设计模式中是可观察的)可以保存到Observer对象的链接,但这个链接从未从EventSource的Observer字段中删除。所以垃圾收集器永远不会收集它们。解决这一问题的一个解决方案是向客户提供另一种方法,当他们不再需要这些观察员时,将上述观察员从观察员字段中删除:
public void removeObserver(Observer observer) {
observers.remove(observer);
}
Java1.6中的String.substring方法会造成内存泄漏。这篇博文解释了这一点:
SubString方法在Java中的工作原理-JDK1.7中修复了内存泄漏
在Java中,“内存泄漏”主要是因为您使用了太多内存,这与在C中不同,在C中,您不再使用内存,而是忘记返回(释放)内存。当面试官询问Java内存泄漏时,他们询问的是JVM内存使用情况,但似乎一直在增加,他们认为定期重新启动JVM是最好的解决方案(除非面试官非常精通技术)。
所以,回答这个问题,就像他们问JVM内存使用量随时间增长的原因一样。好的答案是在HttpSessions中存储太多数据,超时时间过长,或者内存缓存(singleton)实现不佳,从不刷新旧条目。另一个可能的答案是拥有大量JSP或动态生成的类。类被加载到一个名为PermGen的内存区域,该区域通常很小,大多数JVM不实现类卸载。