1. 程式人生 > >Java手動建立一個記憶體洩漏的程式

Java手動建立一個記憶體洩漏的程式

最近在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 {
		@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。在本例子中,我們為個模擬很多類被載入,僅僅是載入了這個類然後丟棄它(而不是像系統載入器一樣重用這個類)。
	 */
	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 {
				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 ThreadLocal
			threadLocal.set(this);
		}
	}
}