Java手動建立一個記憶體洩漏的程式
阿新 • • 發佈:2019-02-19
最近在stackoverflow上看到一個非常有意思的問題,提問者面試的時候被問到用Java手機建立一個記憶體洩漏的程式,面試者不知如何回答。
其中一個被頂過一千多次的回答非常的好,他描述的步驟大概如下:
- 程式建立一個長時間執行的執行緒(或者使用執行緒池來加速記憶體溢位)
- 這個執行緒通過ClassLoader(可以自定義)來載入一個類
- 這個類分配一大塊記憶體(例如 new byte[1000000]),並且儲存在一個表態變數裡,然後在ThreadLocal裡儲存一個對這個類的引用。前面分配大塊的記憶體是可選的,其實溢位這個類的例項已經足夠,僅僅是為了讓記憶體溢位更快一些而已。
- 記憶體中釋放所有在第二步中載入這個類和或者這個類的ClassLoader的引用。
- 然後使用While迴圈重複以上的步驟,產生更多的類溢位
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); } } }