我刚接受采访,并被要求用Java创建内存泄漏。 毋庸置疑,我觉得自己很傻,甚至不知道如何开始创建一个。
一个例子是什么?
我刚接受采访,并被要求用Java创建内存泄漏。 毋庸置疑,我觉得自己很傻,甚至不知道如何开始创建一个。
一个例子是什么?
这是在纯Java中创建真正的内存泄漏(通过运行代码但仍然存储在内存中无法访问的对象)的好方法:
new byte[1000000]
),在静态字段中存储对它的强引用,然后在ThreadLocal中存储对它自己的引用。分配额外的内存是可选的(泄漏Class实例就足够了),但它会使泄漏工作更快。这是有效的,因为ThreadLocal保留对该对象的引用,该对象保持对其Class的引用,而Class又保持对其ClassLoader的引用。反过来,ClassLoader保持对它已加载的所有类的引用。
(在许多JVM实现中,尤其是在Java 7之前,情况更糟,因为Classes和ClassLoader直接分配到permgen并且根本就没有GC。但是,无论JVM如何处理类卸载,ThreadLocal仍然会阻止被回收的类对象。)
此模式的一个变体是,如果您经常重新部署碰巧以任何方式使用ThreadLocals的应用程序,那么应用程序容器(如Tomcat)可能会像筛子一样泄漏内存。 (由于应用程序容器使用了所描述的线程,每次重新部署应用程序时都会使用新的ClassLoader。)
更新:因为很多人一直在要求它, 这是一些显示此行为的示例代码。
静态字段保持对象引用[esp final field]
class MemorableClass {
static final ArrayList list = new ArrayList(100);
}
调用 String.intern()
在冗长的字符串上
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选项不正确或不适当, 如那个 noclassgc
IBM 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.
下面将有一个非显而易见的案例,其中Java泄漏,除了被遗忘的侦听器的标准情况,静态引用,哈希映射中的虚假/可修改键,或者只是没有任何机会结束其生命周期的线程。
File.deleteOnExit()
- 总是泄漏字符串, char[]
,所以后者不适用; @Daniel,不需要投票。我将专注于线程,以显示大多数非托管线程的危险,不希望甚至触摸挥杆。
Runtime.addShutdownHook
而不是删除...然后甚至使用removeShutdownHook由于ThreadGroup类中有关未启动线程的错误,它可能无法收集,有效地泄漏了ThreadGroup。 JGroup在GossipRouter中有漏洞。
创造,但不是开始,a Thread
进入与上述相同的类别。
创建一个线程继承了 ContextClassLoader
和 AccessControlContext
加上 ThreadGroup
和任何 InheritedThreadLocal
,所有这些引用都是潜在的泄漏,以及类加载器和所有静态引用加载的整个类,以及ja-ja。整个j.u.c.Executor框架具有超级简单的效果,尤其明显 ThreadFactory
界面,但大多数开发人员都不知道潜伏的危险。此外,许多库都会根据请求启动线程(太多行业流行的库)。
ThreadLocal
高速缓存;那些在许多情况下是邪恶的。我确信每个人都已经看到了很多基于ThreadLocal的简单缓存,这也是坏消息:如果线程持续超过预期生命的上下文ClassLoader,那么这是一个纯粹的小漏洞。除非确实需要,否则不要使用ThreadLocal缓存。
调用 ThreadGroup.destroy()
当ThreadGroup本身没有线程时,它仍然保留子ThreadGroups。一个错误的泄漏将阻止ThreadGroup从其父级中删除,但所有子级都变为不可枚举。
使用WeakHashMap和值(in)直接引用键。没有堆转储,这是一个很难找到的。这适用于所有扩展 Weak/SoftReference
这可能会保留一个硬参考回到受保护的对象。
运用 java.net.URL
使用HTTP(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泄漏的地方。所以,希望这会有所帮助。
您可以为面试开发的最重要的技能之一是学习积极倾听问题并与面试官合作以提取他们的意图。这不仅可以让你以他们想要的方式回答他们的问题,而且还表明你有一些重要的沟通技巧。当谈到许多同样有才华的开发人员之间的选择时,我会聘请那些在每次回复之前倾听,思考和理解的人。
如果你不明白,以下是一个非常毫无意义的例子 JDBC。或者至少JDBC希望开发人员关闭的方式 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
对象未关闭,因此物理连接将保持打开状态,直到垃圾收集器出现并看到它无法访问。 GC会调用 finalize
方法,但有没有实现的JDBC驱动程序 finalize
,至少不是这样的 Connection.close
已实施。由此产生的行为是,由于收集了无法访问的对象,将回收内存,与之关联的资源(包括内存) Connection
对象可能根本无法回收。
在这样的情况下 Connection
的 finalize
方法不会清理所有内容,实际上可能会发现与数据库服务器的物理连接将持续几个垃圾收集周期,直到数据库服务器最终确定连接不存在(如果存在),并且应该关闭。
即使JDBC驱动程序要实现 finalize
,有可能在最终确定期间抛出异常。由此产生的行为是,与现在“休眠”对象关联的任何内存都不会被回收,如 finalize
保证只被调用一次。
在对象最终化期间遇到异常的上述情况与另一个可能导致内存泄漏的情况有关 - 对象复活。对象复活通常是通过从另一个对象创建对象的强引用来有意识地完成的。当对象复活被滥用时,它将导致内存泄漏以及其他内存泄漏源。
你可以想出更多的例子 - 比如
List
您只是添加到列表而不是从列表中删除的实例(尽管您应该删除不再需要的元素),或者Socket
s或 File
s,但在不再需要它们时不关闭它们(类似于上面涉及的例子) Connection
类)。