这是在纯 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) ThreadLocal
某种方式指向自身,那么它可以像筛子一样泄漏内存。发生这种情况的原因可能很多,而且很难调试和 / 或修复。
更新 :由于很多人一直在要求它,因此以下示例代码展示了这种行为 。
静态字段保存对象参考 [特别是最终字段]
class MemorableClass {
static final ArrayList list = new ArrayList(100);
}
在冗长的 String 上调用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 选项 ,例如防止未使用的类垃圾回收的 IBM JDK 上的noclassgc
选项
请参阅IBM jdk 设置 。
一个简单的事情是使用带有不正确(或不存在)的hashCode()
或equals()
的 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.
除了被遗忘的侦听器,静态引用,哈希映射中的伪造 / 可修改键的标准情况,或者只是线程卡住而没有任何机会终止其生命周期的标准情况,下面将出现 Java 泄漏的非明显情况。
File.deleteOnExit()
- 始终泄漏字符串, char[]
,因此后者不适用 ; @Daniel,不过不需要投票。 我将集中讨论线程,以大体上显示非托管线程的危险,甚至不希望摆动。
Runtime.addShutdownHook
而不是不删除...,即使由于 ThreadGroup 类中关于未启动的线程的错误(可能无法收集未启动的线程)导致的错误,即使使用 removeShutdownHook 也会有效地泄漏 ThreadGroup。 JGroup 在 GossipRouter 中泄漏。
创建(而不是启动) Thread
与上述类别相同。
创建线程将继承ContextClassLoader
和AccessControlContext
,以及ThreadGroup
和任何InheritedThreadLocal
,所有这些引用都是潜在的泄漏,包括类加载器加载的整个类,所有静态引用以及 ja-ja。在具有超简单ThreadFactory
接口的整个 jucExecutor 框架中,效果尤其明显,但是大多数开发人员都不知道潜伏的危险。另外,很多库都根据请求启动线程(太多了行业流行的库)。
ThreadLocal
缓存;在许多情况下,这些都是邪恶的。我敢肯定,每个人都已经看到了很多基于 ThreadLocal 的简单缓存,这是个坏消息:如果线程在类 ClassLoader 上下文中的寿命超过预期,那将是一个很好的泄漏。除非确实需要,否则不要使用 ThreadLocal 缓存。
当 ThreadGroup 本身没有线程,但仍保留子 ThreadGroups 时,调用ThreadGroup.destroy()
。严重的泄漏将阻止 ThreadGroup 从其父级中移除,但是所有子级都变得无法枚举。
使用 WeakHashMap 和值(in)直接引用键。如果没有堆转储,这是很难找到的。这适用于所有扩展的Weak/SoftReference
,这些扩展可能会将硬引用保留回受保护的对象。
使用具有 HTTP(S)协议的java.net.URL
并从(!)加载资源。这很特殊, KeepAliveCache
在系统 ThreadGroup 中创建了一个新线程,该线程泄漏了当前线程的上下文类加载器。当不存在活动线程时,将在第一个请求时创建该线程,因此您可能会很幸运,或者只是泄漏。 Java 7 中已经修复了该泄漏,并且正确创建线程的代码删除了上下文类加载器。还有更多的情况( 像 ImageFetcher , 也修复了创建类似线程的问题)。
使用InflaterInputStream
通过new java.util.zip.Inflater()
在构造函数( PNGImageDecoder
例如),而不是调用end()
充气的。好吧,如果您传入new
的构造函数,就没有机会了…… 是的,如果手动将其作为构造函数参数传递,则在流上调用close()
不会关闭充气机。这不是真正的泄漏,因为它将由终结器释放…… 在其认为必要时。直到那一刻,它严重消耗了本机内存,可能导致 Linux oom_killer 毫无惩罚地杀死进程。主要问题是 Java 中的终结处理非常不可靠,G1 恶化到 7.0.2。故事的寓意:尽快释放本机资源;终结器太差了。
与java.util.zip.Deflater
相同。由于 Deflater 在 Java 中占用大量内存,因此这一情况要糟得多,即,始终使用 15 位(最大)和 8 个内存级别(最大 9 个)分配数百 KB 的本机内存。幸运的是, Deflater
并未得到广泛使用,据我所知 JDK 不包含任何误用。如果您手动创建Deflater
或Inflater
始终调用end()
。最后两个最好的部分: 您无法通过可用的常规配置工具找到它们。
(我可以根据要求添加更多遇到的时间浪费者。)
祝你好运,保持安全;泄漏是邪恶的!
这里的大多数示例都是 “太复杂”。他们是极端情况。在这些示例中,程序员犯了一个错误(例如,不重新定义 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
期间引发异常。最终的结果是,与 finalt 对象相关联的任何内存都不会被回收,因为finalize
只能被调用一次。
上面在对象完成期间遇到异常的方案与可能导致内存泄漏的另一种方案 - 对象复活有关。对象复活通常是通过从另一个对象最终确定对对象的强烈引用来有意识地完成的。当对象复活被滥用时,将导致内存泄漏以及其他内存泄漏源。
您还可以想到更多示例,例如
List
实例(尽管您应该摆脱不再需要的元素),或者Socket
或File
,但是在不再需要它们时不关闭它们(类似于上面涉及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
)?该引用可能会使一个巨大的对象保持活动状态...
每当您保留对不再需要的对象的引用时,就会发生内存泄漏。请参阅处理 Java 程序中的内存泄漏以获取有关内存泄漏如何在 Java 中表现出来以及如何处理的示例。
您可以使用sun.misc.Unsafe类使内存泄漏。实际上,此服务类在不同的标准类中使用(例如,在java.nio类中)。 您不能直接创建此类的实例 ,但是您可以使用反射来实现 。
代码无法在 Eclipse IDE 中进行编译 - 使用命令javac
对其进行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();
}
}
}