我最近参加了一次面试,其中被要求使用Java创建一个内存泄漏。
不用说,我感到很笨,因为我不知道如何开始创建。
你可以给一个例子吗?
我最近参加了一次面试,其中被要求使用Java创建一个内存泄漏。
不用说,我感到很笨,因为我不知道如何开始创建。
你可以给一个例子吗?
这是在纯Java中创建真正的内存泄漏(对象无法通过运行代码访问但仍存储在内存中)的好方法:
ClassLoader
加载一个类。new byte[1000000]
),将其强引用存储在静态字段中,然后将对其自身的引用存储在ThreadLocal
中。分配额外的内存是可选的(泄漏类实例足以),但它会使泄漏工作得更快。ClassLoader
的所有引用。由于Oracle JDK中ThreadLocal
的实现方式,这会创建内存泄漏:
Thread
都有一个私有字段threadLocals
,实际上存储线程本地值。ThreadLocal
对象的弱引用,因此在该ThreadLocal
对象被垃圾回收后,其条目将从映射中删除。ThreadLocal
对象时,只要线程存在,该对象就不会被垃圾回收或从映射中删除。在这个例子中,强引用链如下:
Thread
对象 → threadLocals
映射 → 示例类的实例 → 示例类 → 静态ThreadLocal
字段 → ThreadLocal
对象。
(ClassLoader
并没有在创建内存泄漏中发挥作用,它只是使内存泄漏变得更严重,因为存在这样的额外引用链:示例类 → ClassLoader
→ 所有已加载的类。在许多JVM实现中,特别是Java 7之前,情况甚至更糟,因为类和ClassLoader
直接分配到permgen中,并且根本不进行垃圾回收。)
这种模式的一个变体是,如果经常重新部署应用程序,而这些应用程序碰巧使用某些方式指向自身的ThreadLocal
,那么应用程序容器(如Tomcat)可能会像筛子一样泄漏内存。这可能会出现许多微妙的原因,通常很难调试或修复。
更新:由于许多人一直在要求,这里提供了一些展示此行为的示例代码。
静态字段保存一个对象引用[尤其是一个 final 字段]
class MemorableClass {
static final ArrayList list = new ArrayList(100);
}
(未关闭的)打开的流(文件、网络等)
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设置。
close()
操作将被调度执行(通常情况下 close()
不会在终结器线程中调用,因为它可能会阻塞)。虽然不关闭是一种不良实践,但不会导致泄漏。未关闭的 java.sql.Connection
也同样如此。 - bestsss一个简单的方法是使用一个错误的(或不存在的)hashCode()
或equals()
的HashSet,然后不断添加“重复”的元素。 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.
BadKey myKey = new BadKey("key"); map.put(key,"value");
时,才能把它取出来。你是正确的,你可以使用迭代器来删除它,但是并不是所有的数据结构都能这样做。例如,如果你不在 @meriton 的答案中置空该引用,那么它将永远丢失。你无法访问支撑数组,而迭代器会停留在其短处。 - corsiKa除了常见的忘记移除监听器、静态引用、哈希映射中伪造/可修改的键或只是线程无法结束其生命周期等标准情况外,下面将会出现Java泄漏的一种不明显的情况。
File.deleteOnExit()
- 总是会泄漏字符串,如果字符串是子字符串,则泄漏会更严重(底层char[]
,因此后者不适用;@Daniel,请不必投票,谢谢。我将专注于线程,以展示未受管理的线程的危险性,不希望甚至触及swing。
Runtime.addShutdownHook
如果没有移除钩子,即使使用removeShutdownHook由于ThreadGroup类关于未启动线程的错误,可能不会被收集,从而导致ThreadGroup泄漏。JGroup在GossipRouter中存在泄漏。
创建但不启动Thread
与上述类似。
创建线程会继承ContextClassLoader
和AccessControlContext
,以及ThreadGroup
和任何InheritedThreadLocal
,所有这些引用都是潜在的泄漏,以及由类加载器加载的所有类和所有静态引用,还有ja-ja。这种影响在整个j.u.c.Executor框架中表现得特别明显,该框架具有超级简单的ThreadFactory
接口,但大多数开发人员没有意识到潜藏的危险。此外,许多库确实会根据请求启动线程(行业流行的库太多了)。
ThreadLocal
缓存;在许多情况下,它们都是有害的。我相信每个人都见过基于ThreadLocal的简单缓存,而不幸的消息是:如果线程保持运行时间比预期的上下文ClassLoaders的生命周期更长,那么它就是一个完全良好的小泄漏。除非真的需要,否则不要使用ThreadLocal缓存。
当ThreadGroup没有线程,但仍保留子ThreadGroups时调用ThreadGroup.destroy()
。这是一个严重的内存泄漏,会阻止ThreadGroup从其父级中移除,但所有子线程变得不可枚举。
使用WeakHashMap并且value(间接)引用了key。这很难在没有堆转储的情况下发现。 扩展的Weak/SoftReference
都可能将硬引用保留回受保护对象。
使用java.net.URL
通过HTTP(S)协议加载资源。这个问题很特别,KeepAliveCache
会在系统ThreadGroup中创建一个新线程,会泄漏当前线程的上下文类加载器。当不存在活动线程时,线程在第一次请求时被创建,因此您可能会有运气或者只是泄漏。 泄漏在Java 7中已经修复,并且创建线程的代码已正确删除上下文类加载器。还存在其他几种情况(例如ImageFetcher,也已修复)会创建类似的线程。
在构造函数中传递new java.util.zip.Inflater()
给InflaterInputStream
(例如PNGImageDecoder),但未调用inflater的end()
。如果在构造函数中只使用new
,那么没有机会...是的,在流上调用close()
不会关闭手动传递的inflater。这不是真正的内存泄漏,因为它将通过finalizer释放...当它认为有必要时才会释放。在此期间,它会大量消耗本地内存,以至于可能导致Linux oom_killer无情地杀死该进程。主要问题在于Java中的finalization非常不可靠,在G1更新到7.0.2之前更糟糕。故事的寓意是:尽快释放本地资源;finalizer太差了。
与java.util.zip.Deflater
一样,这个也很糟糕,因为在Java中Deflater
会占用大量内存,始终使用15位(最大值)和8个内存级别(9是最大值),分配几百KB的本地内存。幸运的是,Deflater
没有被广泛使用,并且据我所知JDK没有误用。如果您手动创建了Deflater
或Inflater
,请始终调用end()
。最后两个类的最好部分:您无法通过常规可用的分析工具找到它们。
(如果需要,我可以添加我遇到的一些更多时间浪费者。)
祝你好运,保持安全; 内存泄漏是邪恶的!
ThreadGroup.destroy()
时,如果ThreadGroup本身没有线程,这是一个非常微妙的错误;我已经追踪了几个小时,因为在我的控制GUI中枚举线程时什么也没有显示,但是线程组和至少一个子组似乎不会消失。 - Lawrence Dol这里的大多数示例都“过于复杂”,它们是边缘情况。在这些示例中,程序员可能犯了错误(例如未重新定义equals/hashcode),或者被JVM/JAVA的一个角落案例所困扰(例如静态类的加载)等。我认为这不是面试官想要的类型的示例,甚至不是最常见的情况。
但是有一些更简单的情况会导致内存泄漏。垃圾收集器只释放不再被引用的内容。我们作为Java开发人员并不关心内存,我们需要时会分配内存,并让它自动释放。不错。
但是长时间运行的应用程序往往会有共享状态。它可以是任何东西,例如静态变量、单例等。通常,非平凡的应用程序倾向于创建复杂的对象图。仅仅忘记将引用设置为null或更常见的是忘记从集合中删除一个对象就足以导致内存泄漏。
当然,所有类型的监听器(例如UI监听器)、缓存或任何长期存在的共享状态如果没有得到正确处理就会产生内存泄漏。应该理解的是,这不是Java的边缘情况,也不是垃圾收集器的问题。这是一个设计问题。我们设计了将监听器添加到长时间存在的对象中,但当不再需要时却没有将监听器删除。我们缓存对象,但没有策略从缓存中删除它们。
也许我们有一个复杂的图形来存储计算所需的先前状态。但是先前的状态本身又与之前的状态相连,以此类推。
就像我们必须关闭SQL连接或文件一样。我们需要设置正确的引用为null,并从集合中删除元素。我们应该有适当的缓存策略(最大内存大小、元素数或定时器)。允许通知侦听器的所有对象必须同时提供addListener和removeListener方法。当这些通知者不再使用时,它们必须清除其侦听器列表。
内存泄漏确实是完全可能的,并且是完全可预测的。不需要特殊的语言功能或边缘情况。内存泄漏要么表明某些内容可能缺失,要么表明存在设计问题。
Connection
、Statement
和ResultSet
实例的引用之前关闭它们,而不是依赖于实现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
对象没有关闭,因此物理的Connection
将保持打开状态,直到垃圾回收器到来并发现它是不可达的。GC将调用finalize
方法,但有些JDBC驱动程序并未实现finalize
,至少不是像Connection.close
这样实现的。结果是,虽然JVM将回收由于不可达对象被收集而导致的内存,但与Connection
对象相关的资源(包括内存)可能不会被回收。Connection
将持续几个垃圾回收周期,直到数据库服务器最终发现Connection
不可用(如果它确实如此)并应该关闭。finalize
,编译器也可能在finalization过程中抛出异常。结果是,任何与现在“休眠”对象相关联的内存都不会被编译器回收,因为finalize
只保证被调用一次。List
实例,只向列表中添加元素而不从中删除(尽管您应该清除不再需要的元素),或者Sockets
或Files
,但在不再需要它们时不关闭它们(类似于涉及Connection
类的上述示例)。可能是最简单的潜在内存泄漏示例之一,以及如何避免它的实现是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
)?这个引用可能会保持一个很大的对象存在...
您可以使用 sun.misc.Unsafe 类来造成内存泄漏。事实上,这个服务类在不同的标准类中被使用(例如在 java.nio 类中)。不能直接创建此类的实例,但是您可以使用反射来获取实例。
代码无法在 Eclipse IDE 中编译 - 请使用命令 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();
}
}
}