我刚刚接受采访,并被要求使用Java 造成内存泄漏。
不用说,我对如何开始创建它一无所知。
一个例子是什么?
我刚刚接受采访,并被要求使用Java 造成内存泄漏。
不用说,我对如何开始创建它一无所知。
一个例子是什么?
Answers:
这是在纯Java中创建真正的内存泄漏(运行代码无法访问但仍存储在内存中的对象)的好方法:
ClassLoader
。new byte[1000000]
),在静态字段中存储对它的强引用,然后在中存储对自身的引用ThreadLocal
。分配额外的内存是可选的(泄漏类实例就足够了),但是它将使泄漏工作快得多。ClassLoader
从中加载的所有引用。由于该方法ThreadLocal
是在Oracle JDK中实现的,因此会造成内存泄漏:
Thread
都有一个私有字段threadLocals
,该字段实际存储线程局部值。ThreadLocal
对象的弱引用,因此在对该ThreadLocal
对象进行垃圾回收后,其条目将从映射中删除。ThreadLocal
作为其键的对象时,只要线程存在,该对象就不会被垃圾回收或从映射中删除。在此示例中,强引用链如下所示:
Thread
对象→ threadLocals
地图→示例类的实例→示例类→静态ThreadLocal
字段→ ThreadLocal
对象。
(ClassLoader
在创建泄漏中并没有真正发挥作用,它只是由于以下附加引用链而使泄漏更糟:示例类→→ ClassLoader
它已加载的所有类。在许多JVM实现中,甚至在更糟糕的情况下,情况甚至更糟。 Java 7,因为类和ClassLoader
s被直接分配到permgen中,并且根本不会被垃圾回收。)
这种模式的一个变种是,如果您频繁地重新部署碰巧使用ThreadLocal
s的应用程序,而应用程序容器(例如Tomcat)像sieve 那样以某种方式指向自身,那么它可以像筛子一样泄漏内存。发生这种情况的原因很多,通常很难调试和/或修复。
更新:由于很多人一直在要求它,因此以下示例代码展示了这种行为。
静态字段保存对象参考[特别是最终字段]
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设置。
close()
通常不会在终结器中调用)线程,因为它可能是阻塞操作)。不关闭是一种不好的做法,但不会造成泄漏。未关闭的java.sql.Connection相同。
intern
哈希表内容上仅具有弱引用。因此,它是正确收集的垃圾,而不是泄漏。(但IANAJP)mindprod.com/jgloss/interned.html#GC
一个简单的事情是使用带有错误(或不存在)hashCode()
或的HashSet 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
而不是删除...,即使由于ThreadGroup类中与未启动的线程有关的错误,即使使用removeShutdownHook,也可能无法收集未启动的线程,这实际上会泄漏ThreadGroup。JGroup在GossipRouter中泄漏。
创建(但不开始)Thread
与上述类别相同。
创建线程继承了ContextClassLoader
and和AccessControlContext
plus ThreadGroup
以及any InheritedThreadLocal
,所有这些引用都是潜在的泄漏,包括类加载器加载的整个类,所有静态引用以及ja-ja。在具有超简单ThreadFactory
界面的整个jucExecutor框架中,这种效果尤其明显,但是大多数开发人员都不知道潜伏的危险。另外,很多库都根据请求启动线程(太多了行业流行的库)。
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()
充气机。好吧,如果您用just传递构造函数new
,则没有机会...而且,是的,close()
如果手动将其作为构造函数参数传递,则调用流不会关闭充气机。这不是真正的泄漏,因为它将由终结器释放……在其认为必要时。直到那一刻,它吞噬了本机内存,严重到可能导致Linux oom_killer毫无惩罚地杀死进程。主要问题是Java中的终结处理非常不可靠,G1恶化到7.0.2。故事的寓意:尽快释放本机资源;终结器太差了。
与相同的情况java.util.zip.Deflater
。由于Deflater在Java中占用大量内存,因此这一情况要差得多,即,始终使用15位(最大)和8个内存级别(最大9个)分配数百KB的本机内存。幸运的是,Deflater
它没有被广泛使用,据我所知,JDK不包含任何误用。end()
如果您手动创建Deflater
或,请始终致电Inflater
。最后两个最好的部分:您无法通过可用的常规配置工具找到它们。
(我可以根据要求添加更多遇到的时间浪费者。)
祝你好运,保持安全;泄漏是邪恶的!
Creating but not starting a Thread...
耶克斯,几个世纪前,我被这个咬了!(Java 1.3)
unstarted
计数,还防止了线程组的破坏(邪恶程度较小,但仍然是泄漏)
ThreadGroup.destroy()
当ThreadGroup本身没有线程时调用...”是一个非常微妙的错误;我一直在追赶这个问题达数小时之久,误入歧途,因为枚举我的控制GUI中的线程没有显示任何内容,但是线程组以及大概至少一个子组不会消失。
这里的大多数示例都是“太复杂”。他们是极端情况。在这些示例中,程序员犯了一个错误(例如,不重新定义equals / hashcode),或者被JVM / JAVA的一个极端案例(带有静态类的负载...)所困扰。我认为这不是面试官想要的例子,甚至不是最常见的情况。
但是确实存在内存泄漏的简单情况。垃圾收集器仅释放不再引用的内容。作为Java开发人员,我们不关心内存。我们在需要时分配它,并使其自动释放。精细。
但是任何长期存在的应用程序都倾向于具有共享状态。它可以是任何东西,静态变量,单例……通常,非平凡的应用程序倾向于制作复杂的对象图。只是忘记将引用设置为null或更经常忘记从集合中删除一个对象就足以导致内存泄漏。
当然,如果处理不当,则所有类型的侦听器(如UI侦听器),缓存或任何长期存在的共享状态都可能导致内存泄漏。应该理解的是,这不是Java的极端情况,也不是垃圾收集器的问题。这是一个设计问题。我们设计为在长期存在的对象中添加侦听器,但在不再需要时不删除该侦听器。我们缓存对象,但是我们没有策略从缓存中删除它们。
我们可能有一个复杂的图,它存储了计算所需的先前状态。但是以前的状态本身链接到之前的状态,依此类推。
就像我们必须关闭SQL连接或文件一样。我们需要将适当的引用设置为null并从集合中删除元素。我们将拥有适当的缓存策略(最大内存大小,元素数或计时器)。所有允许通知侦听器的对象都必须提供addListener和removeListener方法。并且当这些通知器不再使用时,它们必须清除其侦听器列表。
确实确实有可能发生内存泄漏,并且完全可以预测。无需特殊的语言功能或特殊情况。内存泄漏要么表明某些东西可能丢失,要么表明设计问题。
WeakReference
)也可能使对象关心另一个对象的存在。从一个到另一个。如果对象引用有多余的余地,那么有一个“关心目标”指示符可能会有所帮助...
PhantomReference
),如果发现某个对象没有任何人关心它。 WeakReference
有点接近,但是必须先转换为强引用,然后才能使用;如果在存在强参考的同时发生GC周期,则该目标将被认为是有用的。
答案完全取决于访问者认为他们要问的内容。
在实践中是否有可能使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
该类的上述示例)。Connection.close
所有SQL调用的finally块之前,我经常达到SQL数据库的连接限制。为了获得更多乐趣,我调用了一些长时间运行的Oracle存储过程,这些存储过程要求Java端具有锁,以防止对数据库的过多调用。
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中表现出来以及如何处理的示例。
...then the question of "how do you create a memory leak in X?" becomes meaningless, since it's possible in any language.
我不明白你是如何得出这个结论的。有较少的方式通过任何定义创建Java中的内存泄漏。这绝对仍然是一个有效的问题。
您可以使用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();
}
}
}
我可以从这里复制答案: 导致Java内存泄漏的最简单方法?
“在计算机科学中,内存泄漏(或在这种情况下为泄漏)是在计算机程序消耗内存但无法将其释放回操作系统时发生的。” (维基百科)
简单的答案是:您不能。Java会执行自动内存管理,并将释放您不需要的资源。您无法阻止这种情况的发生。它将始终能够释放资源。在具有手动内存管理功能的程序中,这是不同的。您无法使用malloc()在C中获得一些内存。要释放内存,您需要malloc返回的指针并对其调用free()。但是,如果您再也没有指针(被覆盖或超过了生存期),那么很遗憾,您将无法释放此内存,从而导致内存泄漏。
到目前为止,所有其他答案在我的定义中并不是真正的内存泄漏。它们都旨在快速地用毫无意义的东西填充内存。但是在任何时候,您仍然可以取消引用创建的对象,从而释放内存->无泄漏。acconrad的答案非常接近,但我不得不承认,因为他的解决方案实际上是通过强制其无限循环来“ 破坏 ”垃圾收集器)。
长答案是:您可以通过使用JNI编写Java库来获得内存泄漏,该库可以进行手动内存管理,从而导致内存泄漏。如果调用此库,则Java进程将泄漏内存。或者,您可以在JVM中出现错误,以便JVM释放内存。JVM中可能存在错误,甚至可能有一些已知的错误,因为垃圾回收并不是那么简单,但是那仍然是一个错误。通过设计,这是不可能的。您可能会要求一些受此类错误影响的Java代码。抱歉,我不知道,在下一个Java版本中也可能不再是bug。
这是通过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实例之后,也将原始的长字符串和派生的子字符串都保留在内存中。
muchSmallerString
释放时(因为StringLeaker
对象被破坏),长字符串也将被释放。我所说的内存泄漏是在这种JVM实例中永远无法释放的内存。但是,您已经向自己展示了如何释放内存:this.muchSmallerString=new String(this.muchSmallerString)
。发生真正的内存泄漏时,您无能为力。
intern
与“内存泄漏”相比,这种非案例可能更像是“内存意外”。.intern()
但是,确定子字符串肯定会导致保留对较长字符串的引用,并且无法将其释放的情况。
GUI代码中的一个常见示例是在创建窗口小部件/组件并将侦听器添加到某个静态/应用程序作用域对象时,然后在销毁窗口小部件时不删除侦听器。不仅会发生内存泄漏,而且还会降低性能,因为无论您在听什么火灾事件,所有旧的监听器都将被调用。
使用在任何servlet容器(Tomcat,Jetty,Glassfish等)中运行的任何Web应用程序。连续10或20次重新部署该应用程序(仅触摸服务器的自动部署目录中的WAR可能就足够了。
除非有人进行了实际测试,否则几经重新部署后,您很可能会收到OutOfMemoryError的错误,因为该应用程序在执行之后并未对其进行清理。通过此测试,您甚至可能在服务器中发现错误。
问题是,容器的寿命比应用程序的寿命更长。您必须确保可以对容器可能对应用程序的对象或类的所有引用进行垃圾收集。
如果只有一个引用幸免于Web应用程序的取消部署,则相应的类加载器以及结果将无法垃圾收集Web应用程序的所有类。
由您的应用程序启动的线程,ThreadLocal变量,日志记录附加程序是导致类加载器泄漏的一些常见嫌疑人。
也许通过JNI使用外部本机代码?
使用纯Java,几乎是不可能的。
但这是关于“标准”类型的内存泄漏,当您无法再访问该内存时,它仍归应用程序所有。您可以保留对未使用对象的引用,也可以在不关闭流的情况下打开流。
与PermGen和XML解析有关的一次“内存泄漏”很好。我们使用的XML解析器(我不记得是哪个解析器)对标记名称执行了String.intern(),以使比较更快。我们的一位客户有个好主意,它不以XML属性或文本形式存储数据值,而是以标记名的形式存储数据,因此我们拥有一个文档,例如:
<data>
<1>bla</1>
<2>foo</>
...
</data>
实际上,他们没有使用数字,而是使用更长的文本ID(大约20个字符),这些ID是唯一的,每天以10-15百万的速度出现。每天将产生200 MB的垃圾,不再需要,也不再进行GC(因为它在PermGen中)。我们将permgen设置为512 MB,因此内存不足异常(OOME)大约需要两天的时间到达...
什么是内存泄漏:
典型示例:
缓存对象是弄乱事物的一个很好的起点。
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(仅将最近使用的对象保留在缓存中)。
当然,您可以使事情复杂得多:
经常发生的情况:
如果此Info对象具有对其他对象的引用,则该对象又具有对其他对象的引用。从某种意义上讲,您还可以认为这是某种内存泄漏(由不良的设计引起)。
我认为没有人使用内部类示例很有趣。如果您有内部课程;它固有地维护对包含类的引用。当然,从技术上讲,这不是内存泄漏,因为Java最终会清除它;但这会导致课程停留时间比预期的更长。
public class Example1 {
public Example2 getNewExample2() {
return this.new Example2();
}
public class Example2 {
public Example2() {}
}
}
现在,如果您调用Example1并获得一个Example2来丢弃Example1,则您仍然固有地具有指向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假定它将一直存在,并且如果再也无法在代码中实现,则实际上将永远不会尝试对其进行清理。但这是完全未经证实的。
我最近遇到了由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对象。因此,即使在线程完成其正在执行的操作之后,NDC上下文Hastable仍保持对hugeList的引用,从而导致内存泄漏。
采访者可能正在寻找像下面的代码这样的循环引用(顺便说一句,这种引用只是在使用引用计数的非常老的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不再使用引用计数,大多数使用清除垃圾收集器,实际上它将收集此内存。
接下来,您可能会解释如何创建一个具有基础本机资源的对象,如下所示:
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感知的正常范围之外分配本地资源。
创建一个静态地图,并继续为其添加硬引用。这些将永远不会被GC。
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); }
}
您可以通过在该类的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());
}
}
}
我认为还没有人说过:您可以通过覆盖finalize()方法来使对象复活,以便finalize()在某个地方存储对此的引用。垃圾收集器将仅在对象上被调用一次,因此之后该对象将永远不会被破坏。
finalize()
将不会被调用,但是一旦没有更多引用,该对象将被收集。垃圾收集器也不被“调用”。
finalize()
方法只能由JVM调用一次,但这并不意味着如果重新使该对象然后再次取消引用该对象,则不能重新收集该方法。如果该finalize()
方法中有资源关闭代码,则此代码将不会再次运行,这可能会导致内存泄漏。
最近,我遇到了一种更细微的资源泄漏。我们通过类加载器的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,与迭代计数无关。
非常简单和令人惊讶。
正如很多人所建议的那样,资源泄漏很容易引起-就像JDBC示例一样。实际的内存泄漏要难一些-特别是如果您不依靠JVM的损坏位来为您做的话...
创建具有很大占用空间的对象然后无法访问它们的想法也不是真正的内存泄漏。如果没有人可以访问它,那么它将被垃圾回收,如果有东西可以访问它,那么这不是泄漏。
但是,曾经起作用的一种方法-我不知道它是否仍然起作用-是拥有三层深的圆形链条。正如对象A中对对象B的引用,对象B中对对象C的引用,对象C对对象A的引用。GC非常聪明,足以知道两条深链-如A <-> B -如果A和B无法通过其他任何方式访问,但无法处理三通链,则可以安全地收集它...
创造潜在的巨大的内存泄漏的另一种方式是坚持引用Map.Entry<K,V>
的TreeMap
。
很难评估为什么这仅适用于TreeMap
s,但是通过查看实现,原因可能是:a TreeMap.Entry
存储对其兄弟姐妹的引用,因此,如果TreeMap
准备好收集a,但是其他一些类则保留对任何a的引用。它Map.Entry
,那么整个地图将被保留到内存中。
现实场景:
想象有一个返回大TreeMap
数据结构的数据库查询。人们通常使用TreeMap
s作为保留元素的插入顺序。
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
。
我在这里写了一个更广泛的基准。
直到终止线程才收集线程。它们是垃圾收集的根源。它们是为数不多的仅通过忘记它们或清除对它们的引用就不会回收的对象之一。
考虑:终止工作线程的基本模式是设置一些线程看到的条件变量。线程可以定期检查变量并将其用作终止信号。如果未声明变量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
将永远不会消失。
(*编辑*)
面试官可能一直在寻找圆形参考解决方案:
public static void main(String[] args) {
while (true) {
Element first = new Element();
first.next = new Element();
first.next.next = first;
}
}
这是引用计数垃圾收集器的经典问题。然后,您要礼貌地解释JVM使用了一种更复杂的算法,没有此限制。
-韦斯·塔勒
first
都没有用,应该进行垃圾回收。在引用计数垃圾收集器中,对象不会被释放,因为它上面有一个活动的引用(本身)。无限循环是在这里演示泄漏:运行程序时,内存将无限期增加。