面試必問:手寫一個記憶體洩漏的程式
阿新 • • 發佈:2020-01-14
手寫一個記憶體洩露的程式是面試官經常問的問題。
造成記憶體洩漏,就是讓執行的程式無法訪問儲存在記憶體中的物件,下面是Java實現:
- 建立一個長時間執行的執行緒(使用執行緒池洩露的速度更快)。
- 執行緒通過ClassLoader載入某個類(也可以用自定義ClassLoader)。
- 這個類分配了大量記憶體(例如new byte[1000000]),賦給靜態欄位儲存對它的強引用,然後在ThreadLocal中儲存對自身的引用。還可以分配額外的記憶體,這樣洩漏的速度更快(其實只要洩漏Class例項就足夠了)。
- 這個執行緒會清除所有自定義類及載入它的ClassLoader的引用。
- 重複執行。
這個方法之所以奏效,是因為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洩漏演示
*
* <p>要檢視實際執行效果,請將此檔案複製到某個臨時目錄,
* 然後執行:
* <pre>{@code
* javac ClassLoaderLeakExample.java
* java -cp .ClassLoaderLeakExample
* }</pre>
*
* <p>可以看到記憶體不斷增加!在我的系統上,使用JDK 1.8.0_25,開始
* 短短几秒鐘就收到了OutofMemoryErrors
*
* <p>這個類用到了一些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<LoadedInChildClassLoader> threadLocal
= new ThreadLocal<>();
public LoadedInChildClassLoader() {
// 在ThreadLocal中儲存對這個類的引用
threadLocal.set(this);
}
}
}