面试必问:用 Java 写一个内存泄漏程序

编译:ImportNew/唐尤华

原文链接:stackoverflow.com/questions/6470651/creating-a-memory-leak-with-java

问题:

刚参加的一个面试,要我用Java写一个内存泄露程序。

这题完全没有思路,

有好心人能给出一个例子吗?

回答:

造成内存泄漏,就是让运行的程序无法访问存储在内存中的对象,下面是Java实现:

  1. 创建一个长时间运行的线程(使用线程池泄露的速度更快)。

  2. 线程通过ClassLoader加载某个类(也可以用自定义ClassLoader)。

  3. 这个类分配了大量内存(例如new byte[1000000]),赋给静态字段存储对它的强引用,然后在ThreadLocal中存储对自身的引用。还可以分配额外的内存,这样泄漏的速度更快(其实只要泄漏Class实例就足够了)。

  4. 这个线程会清除所有自定义类及加载它的ClassLoader的引用。

  5. 重复执行。

这个方法之所以奏效,是因为ThreadLocal保留了对该对象的引用,对象引用保留了对Class的引用,而Class引用又保留了对ClassLoader的引用。反过来,ClassLoader会保留通过它加载的所有类的引用。

(在许多JVM实现中情况更糟,尤其Java 7之前版本。因为Class和ClassLoader会直接分配到permgen中,GC不进行回收)。但是,无论JVM如何处理类卸载,ThreadLocal仍然会阻止被回收的Class对象)。

这种方案还可以变化为,频繁地重新部署碰巧用到ThreadLocal的应用程序。这时像Tomcat这样的应用程序容器会像筛子一样泄漏内存。(因为应用程序容器会像上面那样启动线程,并且每次重新部署应用程序时,都会使用新的ClassLoader)

更新:鉴于大家强烈要求,这里给出一个演示程序。

ClassLoaderLeakExample.java

import java.io.IOException;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.Path;

/**
* ClassLoader泄漏演示
*
*

要查看实际运行效果,请将此文件复制到某个临时目录,
* 然后运行:
*

{@code
* javac ClassLoaderLeakExample.java
* java -cp .ClassLoaderLeakExample
* }

*
*

可以看到内存不断增加!在我的系统上,使用JDK 1.8.0_25,开始
* 短短几秒钟就收到了OutofMemoryErrors
*
*

这个类用到了一些Java 8功能,主要用于
* I/O 操作同样的原理可以适用于
* Java 1.2以后的任何Java版本
*/
public final class ClassLoaderLeakExample {

static volatile boolean running = true;

public static void main(String[] args) throws Exception {
Thread thread = new LongRunningThread();
try {
thread.start();
System.out.println("Running, press any key to stop.");
System.in.read();
} finally {
running = false;
thread.join();
}
}

/**
* 线程的实现只是循环调用
* {@link #loadAndDiscard()}
*/

static final class LongRunningThread extends Thread {
@Override public void run() {
while(running) {
try {
loadAndDiscard();
} catch (Throwable ex) {
ex.printStackTrace();
}
try {
Thread.sleep(100);
} catch (InterruptedException ex) {
System.out.println("Caught InterruptedException, shutting down.");
running = false;
}
}
}
}

/**
* 这是一个简单的ClassLoader实现,只能加载一个类
* 即LoadedInChildClassLoader类.这里需要解决一些麻烦
* 必须确保每次得到一个新的类
* (而非系统class loader提供的
* 重用类).如果此子类所在JAR文件不在系统的classpath中,
* 不需要这么麻烦.
*/

static final class ChildOnlyClassLoader extends ClassLoader {
ChildOnlyClassLoader() {
super(ClassLoaderLeakExample.class.getClassLoader());
}

@Override protected Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
if (!LoadedInChildClassLoader.class.getName().equals(name)) {
return super.loadClass(name, resolve);
}
try {
Path path = Paths.get(LoadedInChildClassLoader.class.getName()
+ ".class");
byte[] classBytes = Files.readAllBytes(path);
Class c = defineClass(name, classBytes, 0, classBytes.length);
if (resolve) {
resolveClass(c);
}
return c;
} catch (IOException ex) {
throw new ClassNotFoundException("Could not load " + name, ex);
}
}
}

/**
* Helper方法会创建一个新的ClassLoader, 加载一个类,
* 然后丢弃对它们的所有引用.从理论上讲,应该不会影响GC
* 因为没有引用可以逃脱该方法! 但实际上,
* 结果会像筛子一样泄漏内存.
*/

static void loadAndDiscard() throws Exception {
ClassLoader childClassLoader = new ChildOnlyClassLoader();
Class childClass = Class.forName(
LoadedInChildClassLoader.class.getName(), true, childClassLoader);
childClass.newInstance();
// 该方法返回时,将无法访问
// childClassLoader或childClass的引用,
// 但是这些对象仍会成为GC Root!
}

/**
* 一个看起来人畜无害的类,没有做什么特别的事情.
*/

public static final class LoadedInChildClassLoader {
// 获取一些bytes.对于泄漏不是必需的,
// 只是让效果出得更快一些.
// 注意:这里开始真正泄露内存,这些bytes
// 每次迭代都为这个final静态字段创建了!
static final byte[] moreBytesToLeak = new byte[1024 * 1024 * 10];

private static final ThreadLocal threadLocal
= new ThreadLocal<>();

public LoadedInChildClassLoader() {
// 在ThreadLocal中存储对这个类的引用
threadLocal.set(this);
}
}
}

有道无术,术可成;有术无道,止于术

欢迎大家关注Java之道公众号

好文章,我在看❤️

你可能感兴趣的:(面试必问:用 Java 写一个内存泄漏程序)