Java手动创建一个内存泄漏的程序

来源:互联网 发布:js 对象key 编辑:程序博客网 时间:2024/05/17 09:35

最近在stackoverflow上看到一个非常有意思的问题,提问者面试的时候被问到用Java手机创建一个内存泄漏的程序,面试者不知如何回答。

其中一个被顶过一千多次的回答非常的好,他描述的步骤大概如下:

  1. 程序创建一个长时间运行的线程(或者使用线程池来加速内存溢出)
  2. 这个线程通过ClassLoader(可以自定义)来加载一个类
  3. 这个类分配一大块内存(例如 new byte[1000000]),并且存储在一个表态变量里,然后在ThreadLocal里存储一个对这个类的引用。前面分配大块的内存是可选的,其实溢出这个类的实例已经足够,仅仅是为了让内存溢出更快一些而已。
  4. 内存中释放所有在第二步中加载这个类和或者这个类的ClassLoader的引用。
  5. 然后使用While循环重复以上的步骤,产生更多的类溢出
作者解释这个程序会导致内存溢出主要是由于ThreadLocal保持了一个对这个类的实例对象的强引用,从而强引用了这个类,从而反过来就引用了这个类的ClassLoader。而这个ClassLoader反过来把所有它加载的类引用住了。更可怕的是很多JVM实现类和ClassLoader是在permgen(永久代)中直接分配的并且永远不会GC。

使用上面这种模式,可以解释为什么一些应用容器(比如Tomcat)如果你频繁的重新部署使用了ThreadLocal的应用,它就像筛子一样一点一点内存泄漏。原因是应用就像上面一样使用线程,然后每次你重新部署你的应用新的ClassLoader就会像上面一样被使用了。

然后作者还提供一个github的例子:https://gist.github.com/dpryden/b2bb29ee2d146901b4ae

其实就一个类,我把它下载到我的Eclipse中并给大家翻译了一下
import java.io.IOException;import java.nio.file.Files;import java.nio.file.Path;import java.nio.file.Paths;/** * 创建内存泄漏的例子 * * <p> * 想要运行本例子,请把本类复制到一个目录下面,然后运行如下命令 *  * <pre> * {@code *   javac ClassLoaderLeakExample.java *   java -cp . ClassLoaderLeakExample * } * </pre> * * <p> * 然后监控内存增长(可以使用jvisualvm)! 在使用的是JDK 1.8.0_45(JDK 1.7也可以), 分分钟就会导致内存溢出了. * */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 {@Overridepublic 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。在本例子中,我们为个模拟很多类被加载,仅仅是加载了这个类然后丢弃它(而不是像系统加载器一样重用这个类)。 */static final class ChildOnlyClassLoader extends ClassLoader {ChildOnlyClassLoader() {super(ClassLoaderLeakExample.class.getClassLoader());}@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {if (!LoadedInChildClassLoader.class.getName().equals(name)) {return super.loadClass(name, resolve);}try {String fullPathName = LoadedInChildClassLoader.class.getName().replace(".", "/") + ".class";System.out.println("fullPathName: " + fullPathName);Path path = Paths.get(fullPathName);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);}}}/** *  * 创建一个ClassLoader,加载一个类并且丢弃它们。理论上看起来并不会导致GC的问题,因为这个方法出栈之后引用都会被释放。但是实践中这个就像筛子一样会导致内存泄漏。 *  */static void loadAndDiscard() throws Exception {ClassLoader childClassLoader = new ChildOnlyClassLoader();Class<?> childClass = Class.forName(LoadedInChildClassLoader.class.getName(), true, childClassLoader);childClass.newInstance();// 当这个方法返回时,看起来并没有地方可以引用到它们。但是JVM仍然可以通过根搜索算法找到他们。}/** * 一个内部类 */public static final class LoadedInChildClassLoader {// 每个遍历的循环都创建一大块内存,此处是10M,仅仅是为了让程序死的更快一点而已static final byte[] moreBytesToLeak = new byte[1024 * 1024 * 10];private static final ThreadLocal<LoadedInChildClassLoader> threadLocal = new ThreadLocal<>();public LoadedInChildClassLoader() {// Stash a reference to this class in the ThreadLocalthreadLocal.set(this);}}}



0 0