編寫一個多線程程序,每個線程打開同一個文件進行讀取,但不顯式關閉流:
public class FileLeakDemo {
public static void main(String[] args) throws Exception {
for (int i = 0; i < 10; i++) {
final int id = i;
new Thread(() -> {
try (FileInputStream fis = new FileInputStream("/opt/test.log")) {
Thread.sleep(60_000); // 持有 60 秒
} catch (Exception e) { e.printStackTrace(); }
}, "Thread-" + id).start();
}
Thread.sleep(10 * 60 * 1000); // 主線程等待
}
}
$ lsof -p $(pgrep -f FileLeakDemo) | grep test.log java 12345 user 3r REG 253,0 0 35471424 /opt/test.log java 12345 user 4r REG 253,0 0 35471424 /opt/test.log ... (共 10 個句柄持續佔用,即使線程結束也不釋放)
實驗證明:
null → 句柄仍佔用System.gc() → Full GC 後 句柄才被釋放// FileInputStream 源碼片段 (JDK 8)
protected void finalize() throws IOException {
close(); // 由 Finalizer 線程異步調用
}
finalize() 方法
┌─────────────────────────────────────────────────────────────┐
│ Java Heap │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ FileInputSt │ │ FileInputSt │ │ ... (10) │ │ ← 強引用消失後變為可觸及
│ │ ream#1 │ │ ream#2 │ │ │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Finalizer Reference Queue │ │ ← GC 發現不可達,入隊
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Finalizer │ │ Finalizer │ │ Finalizer │ ... │ ← Finalizer 線程逐個執行 close()
│ │ Thread │ │ Thread │ │ Thread │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ OS File Table │ ← 真正釋放文件描述符
└─────────────────┘
// 自動關閉,編譯器生成 finally 塊調用 close()
public void readFileCorrectly(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path);
BufferedInputStream bis = new BufferedInputStream(fis)) {
byte[] buffer = new byte[8192];
int len;
while ((len = bis.read(buffer)) != -1) {
// 處理數據
}
} // 此處隱式調用 bis.close() → fis.close()
}
FileInputStream fis = null;
try {
fis = new FileInputStream(path);
// 讀取操作
} finally {
if (fis != null) {
try { fis.close(); } catch (IOException ignored) {}
}
}
| 誤區 | 後果 | 正確做法 |
|---|---|---|
| 依賴 GC/Finalizer 自動關閉 | 文件句柄長期佔用,導致「Too many open files」崩潰 | 顯式 try-with-resources 或 finally close() |
| catch 塊中關閉流 | 正常執行路徑流未關閉 | finally 塊或 try-with-resources |
| 多層包裝流僅關閉外層 | 內層流(如 FileInputStream)未關閉 | 關閉最外層流即可鏈式關閉內層 |
| 忽略 close() 拋出的異常 | 掩蓋原始異常,難以排查 | try-with-resources 自動抑制;手動用 try-catch 記錄 |
// Java 7+ NIO.2 方式,同樣支援 try-with-resources
try (InputStream in = Files.newInputStream(Paths.get(path))) {
// 讀取
}
// 自定義資源實現 AutoCloseable
public class DbConnection implements AutoCloseable {
public void close() { /* 釋放連接 */ }
}
// 統一資源管理
try (DbConnection conn = pool.getConnection();
FileInputStream fis = new FileInputStream(file)) {
// 業務邏輯
}
「自己的事自己做」 —— 文件流、數據庫連接、網絡 Socket 等系統資源必須由應用代碼顯式關閉。依賴 Finalizer/GC 是不可靠的:時機不可控、性能開銷大、易導致資源耗盡。try-with-resources 是 Java 7+ 的標準解法,簡潔、安全、異常透明,請在所有資源管理場景中堅持使用。