我最近参加了一次面试,其中被要求使用Java创建一个内存泄漏。
不用说,我感到很笨,因为我不知道如何开始创建。
你可以给一个例子吗?
我最近参加了一次面试,其中被要求使用Java创建一个内存泄漏。
不用说,我感到很笨,因为我不知道如何开始创建。
你可以给一个例子吗?
我可以从这里复制我的答案: 在Java中导致内存泄漏的最简单方法
"内存泄漏在计算机科学中(或在此上下文中泄漏)是指当计算机程序消耗内存但无法将其释放回操作系统时发生的情况。"(维基百科)
简单的答案是:你无法导致Java内存泄漏。Java具有自动内存管理,并将释放不再需要的资源。你无法阻止这种情况的发生。它将始终能够释放资源。在手动内存管理的程序中,情况就不同了。你可以使用malloc()在C语言中获取一些内存。要释放内存,你需要malloc返回的指针并调用free()。但是,如果你不再拥有该指针(已被覆盖或超过了生命周期),那么你无法释放该内存,因此会出现内存泄漏。
到目前为止,所有其他答案在我看来都不是真正的内存泄漏。它们都旨在通过快速填充内存来占用有意义的空间。但是,您随时可以取消引用所创建的对象,从而释放内存,因此不存在泄漏。 acconrad的答案实际上很接近,因为他的解决方案就是通过将垃圾收集器强制置于无限循环中来“崩溃”垃圾收集器。
长话短说:使用JNI编写Java库时,可能会出现手动内存管理,导致内存泄漏。如果调用这个库,你的Java进程将会泄露内存。或者JVM存在缺陷,导致JVM失去内存。JVM可能存在一些错误,甚至可能已知一些错误,因为垃圾回收并不是那么简单,但这仍然是一个错误。从设计上讲,这是不可能发生的。你可能会询问一些受此类错误影响的Java代码。很抱歉我不知道有没有这样的代码,而且在下一个Java版本中也可能已经修复了这个错误。
这里有一个简单同时也可怕的例子,来自于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实例被丢弃后仍然保留原始长字符串和派生的子字符串在内存中。
在GUI代码中,一个常见的例子是创建小部件/组件并将监听器添加到一些静态/应用程序作用域对象中,然后在小部件被销毁时不删除该监听器。这不仅导致内存泄漏,而且当您正在侦听的任何内容触发事件时,也会产生性能影响,因为所有旧的监听器都会被调用。
将任何在任何servlet容器中运行的Web应用程序(Tomcat,Jetty,GlassFish等)重新部署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是唯一的,每天达到1000-1500万个。这意味着每天产生大约200 MB的垃圾数据,这些数据从未被再次使用,并且永远不会被GC回收(因为它们位于PermGen中)。我们将Permgen设置为512 MB,因此需要大约两天才能出现内存溢出异常(OOME)...
面试官可能正在寻找类似下面的循环引用代码(顺带一提,这种代码只会在使用引用计数的非常旧的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正常范围感知的本地资源。
什么是内存泄漏:
典型案例:
对象缓存是搞砸事情的好起点。
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(只在缓存中保留最近使用的对象)。
当然,您可以使事情变得更加复杂:
经常发生的事情:
如果此信息对象具有对其他对象的引用,而这些对象又具有对其他对象的引用。从某种意义上说,您也可以将其视为某种内存泄漏(由于糟糕的设计引起)。
我觉得有趣的是没有人使用内部类的例子。如果你有一个内部类,它会自然地保留对包含类的引用。当然,这并不是严格意义上的内存泄漏,因为Java最终会清理它;但这可能导致类的存在时间比预期要长。
public class Example1 {
public Example2 getNewExample2() {
return this.new Example2();
}
public class Example2 {
public Example2() {}
}
}
如果您调用 Example1 并获得一个 Discarding 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会认为它永远存在,并且如果在代码中无法访问它,Java实际上不会尝试清除它。但这完全没有经过验证。
我最近遇到了一个内存泄漏的情况,是由log4j引起的。
Log4j有一个叫做Nested Diagnostic Context(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上下文哈希表仍然保留对hugeList的引用,从而导致内存泄漏。