提问人: 提问时间:6/25/2011 最后编辑:21 revs, 16 users 26%MatBanik 更新时间:9/20/2023 访问量:745435
如何在 Java 中创建内存泄漏?
How can I create a memory leak in Java?
答:
也许通过JNI使用外部本机代码?
对于纯 Java,这几乎是不可能的。
但这是关于“标准”类型的内存泄漏,当您无法再访问内存时,它仍然归应用程序所有。您可以改为保留对未使用对象的引用,或者打开流而不关闭它们。
评论
一个简单的方法是使用带有不正确(或不存在)或 的 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.
评论
BadKey myKey = new BadKey("key"); map.put(key,"value");
创建一个静态 Map,并不断添加对它的硬引用。这些永远不会被垃圾回收。
public class Leaker {
private static final Map<String, Object> CACHE = new HashMap<String, Object>();
// Keep adding until failure.
public static void addToCache(String key, Object value) { Leaker.CACHE.put(key, value); }
}
评论
每当您保留对不再需要的对象的引用时,都会发生内存泄漏。请参阅处理 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);
}
}
上述问题是对象没有关闭,因此物理对象将保持打开状态,直到垃圾收集器出现并发现它无法访问。GC 将调用该方法,但有些 JDBC 驱动程序不会以与实现相同的方式实现。由此产生的行为是,虽然 JVM 将由于收集了无法访问的对象而回收内存,但与该对象关联的资源(包括内存)可能不会被回收。Connection
Connection
finalize
finalize,
Connection.close
Connection
因此,Connection 的最终方法不会清理所有内容。人们可能会发现,数据库服务器的物理数据将持续几个垃圾回收周期,直到数据库服务器最终发现它不是活动的(如果是),应该关闭。Connection
Connection
即使JDBC驱动程序实现了,编译器也可能在最终确定期间抛出异常。由此产生的行为是,编译器不会回收与现在“休眠”对象关联的任何内存,因为保证只调用一次。finalize
finalize
上述在对象终结过程中遇到异常的场景与另一个可能导致内存泄漏的场景 - 对象复活有关。对象复活通常是有意完成的,方法是从另一个对象创建对正在完成的对象的强引用。当对象复活被滥用时,它将导致内存泄漏与其他内存泄漏源相结合。
你可以想出更多的例子——比如
- 管理一个实例,其中您只添加到列表中,而不是从中删除(尽管您应该删除不再需要的元素),或者
List
- 当不再需要它们时,打开或不关闭它们(类似于上面涉及类的示例)。
Sockets
Files,
Connection
- 关闭 Java EE 应用程序时不卸载单例。加载单例类的类加载器将保留对该类的引用,因此 JVM 永远不会收集单例实例。当部署应用程序的新实例时,通常会创建一个新的类装入器,并且由于单例的原因,以前的类装入器将继续存在。
评论
这是在纯 Java 中创建真正的内存泄漏(运行代码无法访问但仍存储在内存中的对象)的好方法:
- 应用程序创建一个长时间运行的线程(或使用线程池更快地泄漏)。
- 线程通过(可选的自定义)加载类。
ClassLoader
- 该类分配了一大块内存(例如),将对它的强引用存储在静态字段中,然后在 .分配额外的内存是可选的(泄漏类实例就足够了),但它会使泄漏工作得更快。
new byte[1000000]
ThreadLocal
- 应用程序将清除对自定义类或从中加载自定义类的所有引用。
ClassLoader
- 重复。
由于在 Oracle 的 JDK 中实现的方式,这会产生内存泄漏:ThreadLocal
- 每个都有一个私有字段,它实际上存储了线程本地值。
Thread
threadLocals
- 此映射中的每个键都是对对象的弱引用,因此在对该对象进行垃圾回收后,将从映射中删除其条目。
ThreadLocal
ThreadLocal
- 但是每个值都是一个强引用,因此当一个值(直接或间接)指向作为其键的对象时,只要线程存在,该对象就不会被垃圾回收,也不会从映射中删除。
ThreadLocal
在此示例中,强引用链如下所示:
Thread
对象→映射→示例类→示例类的实例→静态字段→对象。threadLocals
ThreadLocal
ThreadLocal
(这在创建泄漏方面并没有真正发挥作用,它只会使泄漏变得更糟,因为这个额外的引用链:示例类→ →它加载的所有类。在许多 JVM 实现中,尤其是在 Java 7 之前,情况更糟,因为类和 s 被直接分配到 permgen 中,并且根本没有被垃圾回收。ClassLoader
ClassLoader
ClassLoader
这种模式的一个变体是,如果您经常重新部署碰巧使用在某种程度上指向自身的应用程序,那么应用程序容器(如 Tomcat)可能会像筛子一样泄漏内存。这可能是由于许多微妙的原因而发生的,并且通常很难调试和/或修复。ThreadLocal
更新:由于很多人一直在要求它,下面是一些示例代码,展示了这种行为的实际效果。
评论
潜在内存泄漏以及如何避免它的最简单示例之一可能是 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
评论
在 GUI 代码中,一个常见的例子是创建小部件/组件并将侦听器添加到某个静态/应用程序范围的对象,然后在小部件被销毁时不删除侦听器。你不仅会得到内存泄漏,而且还会受到性能的影响,因为当你正在听什么时,你的所有老听众也会被调用。
我曾经遇到过一次与 PermGen 和 XML 解析相关的很好的“内存泄漏”。 我们使用的XML解析器(我不记得是哪一个)对标签名称进行了String.intern(),以加快比较速度。 我们的一位客户提出了一个好主意,即不以 XML 属性或文本形式存储数据值,而是以标记名的形式存储数据值,因此我们有一个文档,如下所示:
<data>
<1>bla</1>
<2>foo</>
...
</data>
事实上,他们没有使用数字,而是使用更长的文本 ID(大约 20 个字符),这些 ID 是独一无二的,每天以 1000-1500 万的速度出现。这样一来,每天就会产生 200 MB 的垃圾,这再也不需要了,也永远不会被 GCed(因为它在 PermGen 中)。我们将 permgen 设置为 512 MB,因此内存不足异常 (OOME) 大约需要两天时间才能到达......
评论
下面将有一个不明显的案例,即 Java 泄漏,除了被遗忘的侦听器、静态引用、哈希图中的虚假/可修改键,或者只是线程卡住而没有任何机会结束其生命周期的情况。
File.deleteOnExit()
- 总是泄漏字符串,如果字符串是子字符串,泄漏就更糟了(底层的 char[] 也被泄露了)- 在 Java 7 中,substring 也复制了char[]
,所以后者不适用;@Daniel,不过不需要投票。
我将专注于线程,以展示非托管线程的危险,甚至不希望触及摆动。
Runtime.addShutdownHook
而不是删除...然后,即使使用 removeShutdownHook,由于 ThreadGroup 类中关于未启动线程的错误,它可能不会被收集,也会有效地泄漏 ThreadGroup。JGroup在GossipRouter中存在泄漏。创建但未启动 a 与上述类别相同。
Thread
创建线程会继承 and 、 和 any ,所有这些引用都是潜在的泄漏,以及类加载器加载的整个类和所有静态引用,以及 ja-ja。这种效果在整个 j.u.c.Executor 框架中尤为明显,该框架具有超级简单的界面,但大多数开发人员对潜伏的危险一无所知。此外,许多库确实会根据请求启动线程(太多行业流行的库)。
ContextClassLoader
AccessControlContext
ThreadGroup
InheritedThreadLocal
ThreadFactory
ThreadLocal
缓存;在许多情况下,这些都是邪恶的。我相信每个人都看过很多基于 ThreadLocal 的简单缓存,坏消息是:如果线程继续运行超过上下文 ClassLoader 的预期寿命,这是一个纯粹的小泄漏。除非确实需要,否则不要使用 ThreadLocal 缓存。当 ThreadGroup 本身没有线程,但仍保留子 ThreadGroup 时调用。一个严重的泄漏,将阻止 ThreadGroup 从其父级中删除,但所有子级都变得不可枚举。
ThreadGroup.destroy()
使用 WeakHashMap 和值 (in) 直接引用键。如果没有堆转储,这是很难找到的。这适用于所有可能保留对受保护对象的硬引用的扩展。
Weak/SoftReference
与 HTTP(S) 协议一起使用并从 (!) 加载资源。这个很特别,它在系统 ThreadGroup 中创建一个新线程,该线程泄漏了当前线程的上下文类加载器。当不存在活动线程时,线程是在第一次请求时创建的,因此您可能会很幸运或只是泄漏。泄漏已经在 Java 7 中修复,并且创建线程的代码正确地删除了上下文类加载器。创建类似线程的情况很少(
如 ImageFetcher,也已修复)。java.net.URL
KeepAliveCache
使用传入构造函数(例如)而不调用充气器。好吧,如果你只传入构造函数,就没有机会......是的,如果手动将流作为构造函数参数传递,则调用流不会关闭 inflater。这不是真正的泄漏,因为它会由终结者发布......当它认为有必要时。直到那一刻,它才会严重消耗本机内存,以至于可能导致 Linux oom_killer 杀死该进程而不受惩罚。主要问题是 Java 中的最终确定非常不可靠,G1 使情况变得更糟,直到 7.0.2。故事的寓意:尽快释放原生资源;终结者实在是太差了。
InflaterInputStream
new java.util.zip.Inflater()
PNGImageDecoder
end()
new
close()
与 的情况相同。这要糟糕得多,因为 Deflater 在 Java 中内存不足,即总是使用 15 位(最大)和 8 个内存级别(最大 9 个)来分配数百 KB 的本机内存。幸运的是,JDK没有被广泛使用,据我所知,JDK不包含任何误用。如果手动创建 或 .最后两个最好的部分:您无法通过可用的普通分析工具找到它们。
java.util.zip.Deflater
Deflater
end()
Deflater
Inflater
(我可以根据要求添加一些我遇到的更多浪费时间的人。
祝你好运,注意安全;泄漏是邪恶的!
评论
Creating but not starting a Thread...
哎呀,几个世纪前我被这个咬得很厉害!(Java 1.3)
unstarted
ThreadGroup.destroy()
...”是一个非常微妙的错误;我已经追了几个小时了,误入歧途,因为在我的控制 GUI 中枚举线程什么也没显示,但线程组和至少一个子组不会消失。
这里的大多数例子都“太复杂了”。它们是边缘情况。在这些例子中,程序员犯了一个错误(比如不要重新定义等于/哈希码),或者被JVM/JAVA的极端情况(用静态类加载)咬了一口。我认为这不是面试官想要的例子类型,甚至不是最常见的情况。
但是,内存泄漏的情况确实更简单。垃圾回收器仅释放不再引用的内容。作为 Java 开发人员,我们并不关心内存。我们会在需要时分配它,并让它自动释放。好。
但是,任何长期存在的应用程序都倾向于具有共享状态。它可以是任何东西,静态的、单例的......通常,非平凡的应用程序倾向于制作复杂的对象图形。只是忘记将引用设置为 null,或者更常见的是忘记从集合中删除一个对象,就足以导致内存泄漏。
当然,如果处理不当,所有类型的侦听器(如 UI 侦听器)、缓存或任何长期存在的共享状态都会导致内存泄漏。应该理解的是,这不是一个 Java 极端情况,也不是垃圾收集器的问题。这是一个设计问题。我们设计为将侦听器添加到长期对象中,但在不再需要时不会删除侦听器。我们缓存对象,但我们没有将它们从缓存中删除的策略。
我们可能有一个复杂的图来存储计算所需的先前状态。但是前一个状态本身与之前的状态相关联,依此类推。
就像我们必须关闭 SQL 连接或文件一样。我们需要设置对 null 的正确引用并从集合中删除元素。我们将有适当的缓存策略(最大内存大小、元素数量或计时器)。允许通知侦听器的所有对象都必须同时提供 addListener 和 removeListener 方法。当不再使用这些通知程序时,它们必须清除其侦听器列表。
内存泄漏确实是可能的,并且是完全可以预测的。不需要特殊的语言功能或极端情况。内存泄漏要么表明可能缺少某些东西,要么表明存在设计问题。
评论
保存对象引用的静态字段 [尤其是最终字段]
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()
intern
答案完全取决于面试官认为他们在问什么。
在实践中是否有可能使 Java 泄漏?当然是,其他答案中有很多例子。
但是可能有多个元问题被问到?
- 理论上“完美”的 Java 实现是否容易泄露?
- 考生是否了解理论与现实的区别?
- 候选人是否了解垃圾回收的工作原理?
- 或者垃圾回收在理想情况下应该如何工作?
- 他们知道他们可以通过本机界面调用其他语言吗?
- 他们知道泄漏其他语言的记忆吗?
- 候选人是否知道什么是内存管理,以及 Java 的幕后发生了什么?
我正在阅读您的元问题,即“在这种面试情况下,我可以使用什么答案”。因此,我将专注于面试技巧而不是 Java。我相信你更有可能在面试中重复不知道问题答案的情况,而不是处于需要知道如何让 Java 泄漏的地方。所以,希望这会有所帮助。
你可以为面试培养的最重要的技能之一是学会积极倾听问题并与面试官合作提取他们的意图。这不仅可以让您以他们想要的方式回答他们的问题,而且还表明您具有一些重要的沟通技巧。当涉及到在许多同样有才华的开发人员之间做出选择时,我会聘请一个在他们每次都做出回应之前倾听、思考和理解的人。
评论
我可以从这里复制我的答案: 在 Java 中导致内存泄漏的最简单方法
“在计算机科学中,当计算机程序消耗内存但无法将其释放回操作系统时,就会发生内存泄漏(或在这种情况下的泄漏)。(维基百科)
简单的答案是:你不能。Java 执行自动内存管理,并释放不需要的资源。你无法阻止这种情况的发生。它将始终能够释放资源。在手动内存管理的程序中,这是不同的。你可以使用 malloc() 在 C 语言中获得一些内存。要释放内存,您需要 malloc 返回的指针并对其调用 free()。但是,如果您不再拥有指针(覆盖或超过生存期),那么不幸的是,您无法释放此内存,因此存在内存泄漏。
到目前为止,所有其他答案在我的定义中都不是真正的内存泄漏。它们都旨在用毫无意义的东西快速填充记忆。但是在任何时候,您仍然可以取消引用您创建的对象,从而释放内存 - >不会泄漏。不过,我不得不承认,acconrad 的答案非常接近,因为他的解决方案实际上是通过强制垃圾收集器进入无限循环来“崩溃”垃圾收集器)。
长答案是:通过使用 JNI 为 Java 编写库,您可以获得内存泄漏,该库可以手动进行内存管理,因此存在内存泄漏。如果调用此库,则 Java 进程将泄漏内存。或者,您可以在 JVM 中出现错误,从而使 JVM 失去内存。JVM 中可能存在错误,甚至可能存在一些已知的错误,因为垃圾回收不是那么微不足道,但它仍然是一个错误。根据设计,这是不可能的。您可能要求提供一些受此类错误影响的 Java 代码。对不起,我不知道一个,无论如何,在下一个 Java 版本中它很可能不再是错误。
评论
我认为一个有效的例子可能是在线程池化的环境中使用 ThreadLocal 变量。
例如,在 Servlet 中使用 ThreadLocal 变量与其他 Web 组件进行通信,让容器创建线程并在池中维护空闲线程。如果未正确清理 ThreadLocal 变量,则将一直存在,直到同一 Web 组件可能覆盖其值。
当然,一旦确定,问题就可以轻松解决。
我认为还没有人说过这一点:您可以通过覆盖 finalize() 方法来复活一个对象,以便 finalize() 在某处存储对此的引用。垃圾回收器只会在对象上调用一次,因此在此之后,该对象将永远不会被销毁。
评论
finalize()
finalize()
finalize()
以在任何 servlet 容器(Tomcat、Jetty、GlassFish 等)中运行的任何 Web 应用程序为例。连续重新部署应用程序 10 或 20 次(只需触摸服务器自动部署目录中的 WAR 就足够了。
除非有人实际测试过这一点,否则在几次重新部署后,您很可能会收到 OutOfMemoryError,因为应用程序没有注意自行清理。通过此测试,您甚至可能会在服务器中发现错误。
问题是,容器的生存期比应用程序的生存期长。您必须确保容器可能具有的对应用程序的对象或类的所有引用都可以进行垃圾回收。
如果只有一个引用在取消部署 Web 应用程序后幸存下来,则无法对相应的类装入器以及 Web 应用程序的所有类进行垃圾回收。
由应用程序启动的线程、ThreadLocal 变量、日志记录追加器是导致类加载器泄漏的一些常见嫌疑人。
评论
面试官可能一直在寻找循环参考解决方案:
public static void main(String[] args) {
while (true) {
Element first = new Element();
first.next = new Element();
first.next.next = first;
}
}
这是引用计数垃圾回收器的一个经典问题。然后,你会礼貌地解释一下,JVM 使用了一种更复杂的算法,没有这个限制。
评论
first
我觉得有趣的是,没有人使用内部类示例。如果您有内部类;它固有地维护对包含类的引用。当然,从技术上讲,这并不是内存泄漏,因为 Java 最终会清理它;但这可能会导致课程的停留时间比预期的要长。
public class Example1 {
public Example2 getNewExample2() {
return this.new Example2();
}
public class Example2 {
public Example2() {}
}
}
现在,如果您调用 Example1 并得到一个放弃 Example1 的 Example2,则您本质上仍将有一个指向 Example1 对象的链接。
public class Referencer {
public static Example2 GetAnExample2() {
Example1 ex = new Example1();
return ex.getNewExample2();
}
public static void main(String[] args) {
Example2 ex = Referencer.GetAnExample2();
// As long as ex is reachable; Example1 will always remain in memory.
}
}
我还听说过一个谣言,如果你有一个变量存在的时间超过特定时间;Java 假设它将永远存在,如果无法在代码中访问它,实际上永远不会尝试清理它。但这是完全未经证实的。
评论
有许多不同的情况会泄漏内存。我遇到的一个,它暴露了一张不应该暴露在其他地方使用的地图。
public class ServiceFactory {
private Map<String, Service> services;
private static ServiceFactory singleton;
private ServiceFactory() {
services = new HashMap<String, Service>();
}
public static synchronized ServiceFactory getDefault() {
if (singleton == null) {
singleton = new ServiceFactory();
}
return singleton;
}
public void addService(String name, Service serv) {
services.put(name, serv);
}
public void removeService(String name) {
services.remove(name);
}
public Service getService(String name, Service serv) {
return services.get(name);
}
// The problematic API, which exposes the map.
// and user can do quite a lot of thing from this API.
// for example, create service reference and forget to dispose or set it null
// in all this is a dangerous API, and should not expose
public Map<String, Service> getAllServices() {
return services;
}
}
// Resource class is a heavy class
class Service {
}
您可以使用 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();
}
}
}
一种可能性是为 ArrayList 创建一个包装器,该包装器仅提供一种方法:向 ArrayList 添加内容的方法。将 ArrayList 本身设为私有。现在,在全局范围内构造这些包装对象之一(作为类中的静态对象),并使用 final 关键字限定它(例如 )。所以现在引用不能改变。也就是说,将不起作用,也不能用于释放内存。但是,除了向其添加对象之外,也没有其他方法可以做任何事情。因此,您添加的任何对象都无法回收。public static final ArrayListWrapper wrapperClass = new ArrayListWrapper()
wrapperClass = null
wrapperClass
wrapperClass
我最近修复的一个例子是创建新的 GC 和 Image 对象,但忘记调用 dispose() 方法。
GC javadoc 代码段:
应用程序代码必须显式调用 GC.dispose() 方法以 在以下情况下释放每个实例管理的操作系统资源 不再需要这些实例。这一点尤为重要 在 Windows95 和 Windows98 上,操作系统有限 可用的设备上下文数。
图像 javadoc 片段:
应用程序代码必须显式调用 Image.dispose() 方法以 在以下情况下释放每个实例管理的操作系统资源 不再需要这些实例。
这是一个简单/险恶的通过 http://wiki.eclipse.org/Performance_Bloopers#String.substring.28.29。
public class StringLeaker
{
private final String muchSmallerString;
public StringLeaker()
{
// Imagine the whole Declaration of Independence here
String veryLongString = "We hold these truths to be self-evident...";
// The substring here maintains a reference to the internal char[]
// representation of the original string.
this.muchSmallerString = veryLongString.substring(0, 1);
}
}
由于子字符串是指原始字符串的内部表示形式,因此原始字符串会保留在内存中。因此,只要你有一个 StringLeaker,你也会在内存中拥有整个原始字符串,即使你可能认为你只是在保留一个单字符字符串。
避免存储对原始字符串的不需要的引用的方法是执行如下操作:
...
this.muchSmallerString = new String(veryLongString.substring(0, 1));
...
为了增加坏处,您还可以使用子字符串:.intern()
...
this.muchSmallerString = veryLongString.substring(0, 1).intern();
...
这样做会将原始长字符串和派生的子字符串保留在内存中,即使在丢弃 StringLeaker 实例后也是如此。
评论
正如很多人所建议的那样,资源泄漏是相当容易导致的——就像 JDBC 的例子一样。实际的内存泄漏有点困难 - 特别是如果你不依赖 JVM 的损坏位来为你做这件事......
创建占用空间非常大的对象,然后无法访问它们的想法也不是真正的内存泄漏。如果没有东西可以访问它,那么它将被垃圾回收,如果有东西可以访问它,那么它就不是泄漏......
不过,过去有效的一种方法 - 我不知道它是否仍然有效 - 是拥有一条三深的圆形链。就像对象 A 引用对象 B、对象 B 引用对象 C 和对象 C 引用对象 A 一样。GC 足够聪明,知道如果 A 和 B 无法被其他任何东西访问,则可以安全地收集两条深链——如 A < > B,但无法处理三向链......
评论
我最近遇到了由 log4j 引起的内存泄漏情况。
Log4j 具有这种称为嵌套诊断上下文 (NDC) 的机制,它是一种用于区分来自不同来源的交错日志输出的工具。NDC 工作的粒度是线程,因此它分别将日志输出与不同的线程区分开来。
为了存储特定于线程的标签,log4j 的 NDC 类使用一个 Hashtable,该表由 Thread 对象本身(而不是线程 ID)键控,因此直到 NDC 标签保留在内存中,线程对象挂起的所有对象也保留在内存中。在我们的 Web 应用程序中,我们使用 NDC 使用请求 ID 标记日志输出,以将日志与单个请求区分开来。将 NDC 标记与线程关联的容器也会在从请求返回响应时将其删除。在处理请求的过程中,生成子线程时,会出现此问题,类似于以下代码:
pubclic class RequestProcessor {
private static final Logger logger = Logger.getLogger(RequestProcessor.class);
public void doSomething() {
....
final List<String> hugeList = new ArrayList<String>(10000);
new Thread() {
public void run() {
logger.info("Child thread spawned")
for(String s:hugeList) {
....
}
}
}.start();
}
}
因此,NDC 上下文与生成的内联线程相关联。作为此 NDC 上下文键的线程对象是内联线程,其上挂有 hugeList 对象。因此,即使在线程完成它正在执行的操作后,对 hugeList 的引用仍由 NDC 上下文 Hastable 保持活动状态,从而导致内存泄漏。
评论
大家总是忘记原生代码路由。以下是泄漏的简单公式:
- 声明本机方法。
- 在本机方法中,调用 .不要打电话给 .
malloc
free
- 调用本机方法。
请记住,本机代码中的内存分配来自 JVM 堆。
理论上你不能。Java 内存模型阻止了它。但是,由于必须实现 Java,因此可以使用一些注意事项。这取决于您可以使用什么:
如果可以使用本机,则可以分配以后不会放弃的内存。
如果这不可用,那么关于Java有一个肮脏的小秘密,很少有人知道。您可以请求一个不受 GC 管理的直接访问阵列,因此可以很容易地用于造成内存泄漏。这是由 DirectByteBuffer (http://download.oracle.com/javase/1.5.0/docs/api/java/nio/ByteBuffer.html#allocateDirect(int)) 提供的。
如果你不能使用其中任何一个,你仍然可以通过欺骗 GC 来造成内存泄漏。JVM 是使用分代垃圾回收实现的。这意味着堆被划分为区域:年轻人、成年人和老年人。创建对象时,它从年轻区域开始。随着它被越来越多地使用,它发展到成人到老年人。到达老年人区的物体很可能不会被垃圾收集。你不能确定一个物体是否泄漏,如果你要求停止并清洁GC,它可能会清洁它,但很长一段时间它都会泄漏。更多信息请见(http://java.sun.com/docs/hotspot/gc1.4.2/faq.html)
此外,类对象不需要进行 GC 处理。可能有一种方法可以做到这一点。
评论
可以通过在类的 finalize 方法中创建类的新实例来创建移动内存泄漏。如果终结器创建多个实例,则奖励积分。这是一个简单的程序,它会在几秒钟到几分钟之间的某个时间泄漏整个堆,具体取决于您的堆大小:
class Leakee {
public void check() {
if (depth > 2) {
Leaker.done();
}
}
private int depth;
public Leakee(int d) {
depth = d;
}
protected void finalize() {
new Leakee(depth + 1).check();
new Leakee(depth + 1).check();
}
}
public class Leaker {
private static boolean makeMore = true;
public static void done() {
makeMore = false;
}
public static void main(String[] args) throws InterruptedException {
// make a bunch of them until the garbage collector gets active
while (makeMore) {
new Leakee(0).check();
}
// sit back and watch the finalizers chew through memory
while (true) {
Thread.sleep(1000);
System.out.println("memory=" +
Runtime.getRuntime().freeMemory() + " / " +
Runtime.getRuntime().totalMemory());
}
}
}
我在 Java 中看到的大多数内存泄漏都与进程不同步有关。
进程 A 通过 TCP 与 B 通信,并告诉进程 B 创建一些东西。B 向资源发出一个 ID,比如 432423,A 将其存储在一个对象中,并在与 B 通信时使用。在某个时候,A 中的对象被垃圾回收回收(可能是由于一个错误),但 A 从未告诉 B(可能是另一个错误)。
现在,A 不再拥有它在 B 的 RAM 中创建的对象的 ID,并且 B 不知道 A 不再引用该对象。实际上,对象已泄漏。
几点建议:
- 在 servlet 容器中使用 commons-logging(也许有点挑衅)
- 在 servlet 容器中启动线程,并且不从其 run 方法返回
- 在 servlet 容器中加载动画 GIF 图像(这将启动动画线程)
通过重新部署应用程序;)可以“改善”上述效果
我最近偶然发现了这一点:
- 调用“new java.util.zip.Inflater();”而不调用“Inflater.end()”
阅读 https://bugs.java.com/bugdatabase/view_bug?bug_id=5072161 和链接的问题,进行深入讨论。
评论
在 Java 中,“内存泄漏”主要是你使用了太多内存,这与在 C 中不同,在 C 中你不再使用内存,但忘记返回(释放)它。当面试官询问 Java 内存泄漏时,他们问的是 JVM 内存使用率似乎在不断上升,他们确定定期重新启动 JVM 是最好的解决方法(除非面试官非常精通技术)。
因此,回答这个问题就好像他们问是什么让 JVM 内存使用量随着时间的推移而增长一样。好的答案是在超时时间过长的 HttpSessions 中存储过多的数据,或者实现不当的内存中缓存(单例)从不刷新旧条目。另一个可能的答案是拥有大量 JSP 或动态生成的类。类被加载到称为 PermGen 的内存区域中,该区域通常很小,并且大多数 JVM 不实现类卸载。
我最近遇到了一种更微妙的资源泄漏。 我们通过类加载器的 getResourceAsStream 打开资源,碰巧输入流句柄没有关闭。
呃,你可能会说,真是个白痴。
有趣的是:通过这种方式,您可以泄漏底层进程的堆内存,而不是从 JVM 的堆中泄漏。
您只需要一个 jar 文件,其中包含一个将从 Java 代码引用的文件。jar 文件越大,分配内存的速度就越快。
您可以使用以下类轻松创建这样的 jar:
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class BigJarCreator {
public static void main(String[] args) throws IOException {
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(new File("big.jar")));
zos.putNextEntry(new ZipEntry("resource.txt"));
zos.write("not too much in here".getBytes());
zos.closeEntry();
zos.putNextEntry(new ZipEntry("largeFile.out"));
for (int i=0 ; i<10000000 ; i++) {
zos.write((int) (Math.round(Math.random()*100)+20));
}
zos.closeEntry();
zos.close();
}
}
只需粘贴到名为BigJarCreator.java的文件中,从命令行编译并运行它:
javac BigJarCreator.java
java -cp . BigJarCreator
瞧:您在当前工作目录中找到一个 jar 存档,其中包含两个文件。
让我们创建第二个类:
public class MemLeak {
public static void main(String[] args) throws InterruptedException {
int ITERATIONS=100000;
for (int i=0 ; i<ITERATIONS ; i++) {
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt");
}
System.out.println("finished creation of streams, now waiting to be killed");
Thread.sleep(Long.MAX_VALUE);
}
}
此类基本上不执行任何操作,但创建未引用的 InputStream 对象。这些对象将立即被垃圾回收,因此不会增加堆大小。 对于我们的示例来说,从 jar 文件加载现有资源很重要,大小在这里确实很重要!
如果您有疑问,请尝试编译并启动上面的类,但请确保选择一个合适的堆大小 (2 MB):
javac MemLeak.java
java -Xmx2m -classpath .:big.jar MemLeak
您不会在这里遇到 OOM 错误,因为没有保留引用,无论您在上面的示例中选择多大的 ITERATIONS,应用程序都会继续运行。 除非应用程序进入 wait 命令,否则进程(在顶部 (RES/RSS) 或进程资源管理器中可见)的内存消耗会增长。在上面的设置中,它将分配大约 150 MB 的内存。
如果希望应用程序安全运行,请在创建输入流的位置关闭输入流:
MemLeak.class.getClassLoader().getResourceAsStream("resource.txt").close();
您的进程不会超过 35 MB,与迭代计数无关。
相当简单和令人惊讶。
Swing 通过对话非常容易。创建一个JDialog,显示它,用户关闭它,然后泄漏!
您必须调用或配置 .dispose()
setDefaultCloseOperation(DISPOSE_ON_CLOSE)
如果最大堆大小为 X.Y1...。Yn 实例数
因此,总内存 = 实例数 X 每个实例的字节数。如果 X1......Xn 是每个实例的字节数,则总内存 (M)=Y1 * X1+.....+Yn *Xn。因此,如果 M>X,则超出堆空间。
以下可能是代码中的问题
- 使用比本地变量更多的实例变量。
- 每次都创建实例,而不是池化对象。
- 不按需创建对象。
- 在操作完成后,使对象引用为 null。同样,在程序中需要时重新创建。
这是一个非常简单的 Java 程序,它将耗尽空间
public class OutOfMemory {
public static void main(String[] arg) {
List<Long> mem = new LinkedList<Long>();
while (true) {
mem.add(new Long(Long.MAX_VALUE));
}
}
}
评论
如果不使用压缩垃圾回收器,则可能会由于堆碎片而导致某种内存泄漏。
Lapsed Listerners 是内存泄漏的一个很好的例子:Object 被添加为 Listener。当不再需要该对象时,对该对象的所有引用都将为空。但是,忘记从侦听器列表中删除对象会使对象保持活动状态,甚至响应事件,从而浪费内存和 CPU。查看 http://www.drdobbs.com/jvm/java-qa/184404011
从 finalize 方法引发未经处理的异常。
线程在终止之前不会被收集。它们是垃圾回收的根源。它们是为数不多的不会通过忘记它们或清除对它们的引用而被回收的对象之一。
考虑一下:终止工作线程的基本模式是设置线程看到的一些条件变量。线程可以定期检查变量,并将其用作终止信号。如果未声明变量,则线程可能无法看到对变量的更改,因此它不知道终止。或者想象一下,如果某些线程想要更新共享对象,但在尝试锁定它时死锁。volatile
如果你只有少数几个线程,这些错误可能会很明显,因为你的程序将停止正常工作。如果您的线程池根据需要创建更多线程,则过时/卡住的线程可能不会被注意到,并且会无限累积,从而导致内存泄漏。线程可能会使用应用程序中的其他数据,因此也会阻止收集它们直接引用的任何内容。
以玩具为例:
static void leakMe(final Object object) {
new Thread() {
public void run() {
Object o = object;
for (;;) {
try {
sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {}
}
}
}.start();
}
随心所欲地调用,但传递给的对象永远不会死。System.gc()
leakMe
评论
一个不会终止的线程(例如,在其 run 方法中无限期休眠)。即使我们丢失了对它的引用,它也不会被垃圾回收。您可以根据需要添加字段以使线程对象变大。
目前最热门的答案列出了更多关于这个问题的技巧,但这些似乎是多余的。
关于如何在 Java 中创建内存泄漏有很多答案,但请注意面试中提出的问题。
“如何使用Java创建内存泄漏?”是一个开放式问题,其目的是评估开发人员的经验程度。
如果我问你“你有解决Java内存泄漏的经验吗?”,你的回答是简单的“是”。 然后,我必须接着说“你能给我举例说明你如何解决内存泄漏问题吗?”,你会给我一两个例子。
然而,当面试官问到“如何用Java造成内存泄漏?”时,预期的答案应该遵循以下几行:
- 我遇到了内存泄漏......(说什么时候)[向我展示经验]
- 导致它的代码是......(解释代码)[你自己修好了]
- 我应用的修复程序基于...(解释修复)[这让我有机会询问有关修复的细节]
- 我做的测试是......[让我有机会询问其他测试方法]
- 我是这样记录的......[加分。如果你记录下来就好了]
- 因此,可以合理地认为,如果我们以相反的顺序遵循此操作,即获取我修复的代码并删除我的修复程序,我们将发生内存泄漏。
当开发人员没有遵循这种思路时,我会试着引导他/她问“你能给我举个例子来说明Java是如何泄漏内存的吗?”,然后是“你有没有修复过Java中的任何内存泄漏?
请注意,我不是在要求一个关于如何在 Java 中泄漏内存的示例。那太傻了。谁会对能够有效地编写泄漏内存代码的开发人员感兴趣?
评论
Java 1.6 中的 String.substring 方法会造成内存泄漏。这篇博文对此进行了解释:
SubString 方法在 Java 中的工作原理 - JDK 1.7 中修复的内存泄漏
什么是内存泄漏:
- 这是由错误或糟糕的设计引起的。
- 这是浪费记忆。
- 随着时间的流逝,情况会变得更糟。
- 垃圾回收器无法清理它。
典型示例:
对象缓存是搞砸事情的良好起点。
private static final Map<String, Info> myCache = new HashMap<>();
public void getInfo(String key)
{
// uses cache
Info info = myCache.get(key);
if (info != null) return info;
// if it's not in cache, then fetch it from the database
info = Database.fetch(key);
if (info == null) return null;
// and store it in the cache
myCache.put(key, info);
return info;
}
您的缓存会不断增长。很快,整个数据库就被占用在内存中。更好的设计使用 LRUMap(仅将最近使用的对象保留在缓存中)。
当然,你可以让事情变得更加复杂:
- 使用 ThreadLocal 构造。
- 添加更复杂的引用树。
- 或由第三方库引起的泄漏。
经常发生的情况:
如果此 Info 对象具有对其他对象的引用,则这些对象又具有对其他对象的引用。在某种程度上,您也可以将其视为某种内存泄漏(由糟糕的设计引起)。
评论
在具有自己生命周期的类中粗心地使用非静态内部类。
在 Java 中,非静态内部类和匿名类包含对其外部类的隐式引用。另一方面,静态内部类则不然。
以下是 Android 中内存泄漏的常见示例,但这并不明显:
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() { // Non-static inner class, holds the reference to the SampleActivity outer class
@Override
public void handleMessage(Message msg) {
// ...
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Post a message and delay its execution for a long time.
mLeakyHandler.postDelayed(new Runnable() {//here, the anonymous inner class holds the reference to the SampleActivity class too
@Override
public void run() {
//....
}
}, SOME_TOME_TIME);
// Go back to the previous Activity.
finish();
}}
这将防止对活动上下文进行垃圾回收。
评论
mLeakyHandler
mLeakyHandler
Runnable
Java 中没有内存泄漏这样的事情。内存泄漏是从 C 等人那里借来的一个短语。Java 在 GC 的帮助下在内部处理内存分配。有内存浪费(即留下搁浅的对象),但没有内存泄漏。
Java 中的内存泄漏不是典型的 C/C++ 内存泄漏。
要了解 JVM 的工作原理,请阅读了解内存管理。
基本上,重要的部分是:
标记和扫描模型
JRockit JVM 使用标记和清除垃圾回收模型 对整个堆执行垃圾回收。标记和扫描 垃圾回收由两个阶段组成,标记阶段和 扫描阶段。
在标记阶段,可从 Java 访问的所有对象 线程、本机句柄和其他根源被标记为活动,如 以及可从这些对象访问的对象等 第四。此过程识别并标记所有静止的对象 用过了,其余的可以算是垃圾。
在扫描阶段,遍历堆以查找 活动对象。这些差距记录在自由列表中并制作 可用于新对象分配。
JRockit JVM 使用标记和扫描的两个改进版本 型。一种主要是并发标记和扫描,另一种是 平行标记和扫描。您也可以混合使用这两种策略,运行 例如,主要是并发标记和并行扫描。
因此,要在 Java 中创建内存泄漏;最简单的方法是创建一个数据库连接,做一些工作,而不是它;然后生成新的数据库连接,同时保持在范围内。例如,这在循环中并不难做到。如果您有一个从队列中拉取并推送到数据库的工作线程,则很容易通过忘记连接或在不必要的时候打开连接等方式造成内存泄漏。Close()
Close()
最终,您将通过忘记连接来消耗已分配给 JVM 的堆。这将导致 JVM 垃圾回收疯狂;最终导致错误。应该注意的是,该错误可能并不意味着存在内存泄漏;这可能只是意味着你没有足够的内存;例如,像 Cassandra 和 Elasticsearch 这样的数据库可能会抛出该错误,因为它们没有足够的堆空间。Close()
java.lang.OutOfMemoryError: Java heap space
值得注意的是,这适用于所有 GC 语言。以下是我作为 SRE 看到的一些示例:
- 使用 Redis 作为队列的 Node.js;开发团队每 12 小时创建一次新连接,忘记关闭旧连接。最终节点是 OOMd,因为它消耗了所有内存。
- 去吧(我对此感到内疚);使用大型 JSON 文件解析,然后通过引用传递结果并使其保持打开状态。最终,这导致整个堆被我打开以解码 JSON 的意外引用所消耗。
json.Unmarshal
面试官可能正在寻找一个循环引用,就像下面的代码一样(顺便说一句,它只在使用引用计数的非常旧的 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 感知的正常范围之外分配本机资源。
评论
就这样!
public static void main(String[] args) {
List<Object> objects = new ArrayList<>();
while(true) {
objects.add(new Object());
}
}
评论
JDK 1.7 之前的内存泄漏的实时示例:
假设您读取了一个包含 1000 行文本的文件,并将它们保存在 String 对象中:
String fileText = 1000 characters from file
fileText = fileText.subString(900, fileText.length());
在上面的代码中,我最初读取了 1000 个字符,然后执行子字符串以仅获取最后 100 个字符。现在应该只引用 100 个字符,所有其他字符都应该被垃圾回收,因为我丢失了引用,但在 JDK 1.7 之前,子字符串函数间接引用了最后 100 个字符的原始字符串,并防止整个字符串进行垃圾回收,整个 1000 个字符将存在于内存中,直到您丢失对子字符串的引用。fileText
您可以创建如上所述的内存泄漏示例。
评论
造成潜在巨大内存泄漏的另一种方法是保留对 .Map.Entry<K,V>
TreeMap
很难评估为什么这仅适用于 s,但通过查看实现,原因可能是:a 存储对其同级的引用,因此,如果 a 已准备好被收集,但其他类保留了对其任何一个的引用,那么整个 Map 将保留到内存中。TreeMap
TreeMap.Entry
TreeMap
Map.Entry
现实生活场景:
想象一下,有一个返回大数据结构的数据库查询。人们通常使用 s 作为元素插入顺序的保留。TreeMap
TreeMap
public static Map<String, Integer> pseudoQueryDatabase();
如果查询被调用了很多次,并且对于每个查询(因此,对于每个返回的查询),您保存了一个位置,则内存将不断增长。Map
Entry
请考虑以下包装类:
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()
map
Entry
根据您的设置,应用程序可能会在早期阶段崩溃,因为 .jvm
OutOfMemoryError
从这张图中可以看出内存是如何不断增长的。visualvm
散列数据结构 () 不会发生同样的情况。HashMap
这是使用 .HashMap
解决方案是什么?只需直接保存键/值(您可能已经这样做了),而不是保存 .Map.Entry
我在这里写了一个更广泛的基准测试。
我想就如何使用 JVM 中提供的工具监视应用程序的内存泄漏提供建议。它没有显示如何生成内存泄漏,但解释了如何使用最少的可用工具检测内存泄漏。
您需要先监视 Java 内存消耗。
执行此操作的最简单方法是使用 JVM 附带的 jstat 实用程序:
jstat -gcutil <process_id> <timeout>
它将报告每一代人(年轻人、老年人和老年人)的内存消耗和垃圾收集时间(年轻人和满员)。
一旦您发现完全垃圾回收执行得太频繁并且花费了太多时间,您就可以认为应用程序正在泄漏内存。
然后,您需要使用 jmap 实用程序创建内存转储:
jmap -dump:live,format=b,file=heap.bin <process_id>
然后,您需要使用内存分析器(例如Eclipse内存分析器(MAT))分析堆.bin文件。
MAT 将分析内存并为您提供有关内存泄漏的可疑信息。
内存泄漏是一种资源泄漏,当计算机程序错误地管理内存分配时,就会发生这种泄漏,以至于不再需要的内存不会被释放=>维基百科的定义
这是一个相对基于上下文的主题,你可以根据自己的喜好创建一个,只要未使用的引用永远不会被客户使用,但仍然保持活力。
第一个示例应该是一个自定义堆栈,而不会在 Effective Java 第 6 项中清空过时的引用。
当然,只要你愿意,还有更多,但如果我们只看一下 Java 内置类,它可能会有一些
让我们检查一些超级愚蠢的代码来产生泄漏。
public class MemoryLeak {
private static final int HUGE_SIZE = 10_000;
public static void main(String... args) {
letsLeakNow();
}
private static void letsLeakNow() {
Map<Integer, Object> leakMap = new HashMap<>();
for (int i = 0; i < HUGE_SIZE; ++i) {
leakMap.put(i * 2, getListWithRandomNumber());
}
}
private static List<Integer> getListWithRandomNumber() {
List<Integer> originalHugeIntList = new ArrayList<>();
for (int i = 0; i < HUGE_SIZE; ++i) {
originalHugeIntList.add(new Random().nextInt());
}
return originalHugeIntList.subList(0, 1);
}
}
实际上,还有另一个技巧,我们可以利用HashMap的查找过程来导致内存泄漏。实际上有两种类型:
hashCode()
总是一样的,但又是不同的;equals()
- 使用 random 和 always true;
hashCode()
equals()
为什么?
hashCode(
) -> bucket => equals()
来定位货币对
我打算先提到substring(),
然后再提到,但似乎这个问题已经得到解决,因为它的源代码出现在 JDK 8 中。subList()
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
Java 内存泄漏示例之一是 MySQL 的内存泄漏错误,当忘记调用 ResultSets close 方法时。例如:
while(true) {
ResultSet rs = database.select(query);
...
// going to next step of loop and leaving resultset without calling rs.close();
}
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class Main {
public static void main(String args[]) {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
((Unsafe) f.get(null)).allocateMemory(2000000000);
} catch (Exception e) {
e.printStackTrace();
}
}
}
评论
创建一个仅包含 while-true 循环的 JNI 函数,并使用另一个线程中的大对象调用它。GC 不太喜欢 JNI,并且会将对象永远保留在内存中。
Java 中有很多内存泄漏的好例子,我将在这个答案中提到其中的两个。
示例 1:
下面是 Effective Java, Third Edition 一书中内存泄漏的一个很好的示例(第 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);
}
}
本书的这段话描述了为什么这种实现会导致内存泄漏:
如果堆栈增长然后收缩,则从 堆栈不会被垃圾回收,即使程序使用 堆栈不再引用它们。这是因为 堆栈维护对这些对象的过时引用。一个过时的 引用只是一个永远不会被取消引用的引用 再。在这种情况下,任何超出“活动部分”的引用 元素数组已过时。活动部分包括 索引小于 size 的元素
以下是本书解决此内存泄漏的解决方案:
解决此类问题的方法很简单:空出 一旦它们过时,引用。对于我们的 Stack 类, 对某个项目的引用在弹出后立即过时 离开堆栈。pop 方法的更正版本如下所示:
public Object pop() {
if (size == 0) throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // Eliminate obsolete reference
return result;
}
但是,我们如何防止内存泄漏的发生呢?这是书中的一个很好的警告:
一般来说,每当一个类管理自己的内存时, 程序员应警惕内存泄漏。每当一个元素 ,则元素中包含的任何对象引用都应 无效。
示例 2:
观察者模式也可能导致内存泄漏。您可以在以下链接中阅读有关此模式的信息:观察者模式。
这是 Observer 模式的一种实现:
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);
}
}
}
在此实现中,在 Observer 设计模式中是 Observable 的,可以保存指向对象的链接,但此链接永远不会从 中的字段中删除。因此,它们永远不会被垃圾收集器收集。解决此问题的一种解决方案是向客户端提供另一种方法,以便在不再需要上述观察者时从现场删除这些观察者:EventSource
Observer
observers
EventSource
observers
public void removeObserver(Observer observer) {
observers.remove(observer);
}
对以前的答案(更快地生成内存泄漏)的一点改进是使用从大型 XML 文件加载的 DOM 文档实例。
这很简单:
Object[] o = new Object[]{};
while(true) {
o = new Object[]{o};
}
评论
您可以尝试让许多缓冲的读取器尝试使用条件从不为 false 的循环同时打开同一文件。最重要的是,这些永远不会关闭。while
我在javax.swing.JPopupMenu
中经历了非常真实的内存泄漏。
我有一个 GUI 应用程序,它显示多个选项卡式文档。关闭文档后,如果在选项卡上的任何组件上使用右键单击上下文菜单,则该文档会滞留在内存中。菜单在选项卡之间共享,结果发现,在调用 popupMenu.show(Component invoker, int x, int y) 后,
该组件会悄悄地作为菜单的“调用程序”保留下来,直到它被 更改或清除。间接地,调用程序引用保留了整个文档以及与之关联的所有内容。setInvoker(null)
值得注意的是,菜单只能以这种方式保存对旧组件的一个引用,因此这种内存泄漏不会无限制地增长。
评论