99 Ngày Java — Ngày 80: Memory Leak trong Java
Java có GC nhưng vẫn có memory leak: đối tượng không còn hữu ích vẫn reachable từ GC root. Heap tăng sau mỗi lần thu, pause dài dần rồi OOM. Đừng kết luận chỉ từ biểu đồ; cần xác định ai đang giữ đối tượng, vì sao đường tham chiếu ấy còn tồn tại và sửa đúng nguồn giữ.

1. Những nguồn giữ ngoài ý muốn
Static Map chỉ thêm mà không xóa, cache không giới hạn, listener không unregister và ThreadLocal không remove là những mẫu thường gặp. Executor pool giữ thread sống lâu, nên ThreadLocal có thể kéo theo toàn bộ request context. Classloader cũ có thể bị giữ bởi thread hoặc static reference khi redeploy. Đối tượng còn reachable thì GC không thể tự quyết nó vô dụng.
private static final Map<String, byte[]> CACHE = new HashMap<>();
void remember(String id, byte[] payload) {
CACHE.put(id, payload); // no eviction: grows with unique ids
}2. Đo xu hướng trước khi chụp dump
Theo dõi mức heap sau full/mixed collection ở cùng tải, allocation rate và GC pause. Chụp histogram nhiều thời điểm để thấy loại object nào tăng. Heap dump là ảnh chụp tại một thời điểm, có thể tạm dừng ứng dụng và chiếm nhiều đĩa; lấy lúc phù hợp, bảo vệ dữ liệu vì nó có thể chứa secret và thông tin người dùng.
jcmd <pid> GC.class_histogram > before.txt
# Reproduce load, wait for comparable GC state
jcmd <pid> GC.class_histogram > after.txt
jcmd <pid> GC.heap_dump /secure/path/leak.hprof3. MAT: retained size và path to roots
Trong Eclipse MAT, mở Leak Suspects, Dominator Tree và Histogram. Shallow size là kích thước bản thân object; retained size ước lượng phần heap có thể giải phóng nếu object đó biến mất. Path to GC Roots chỉ ra chuỗi tham chiếu giữ nó. Tìm owner ở đầu đường dẫn, không chỉ xóa object ở cuối; phân biệt cache hợp lệ với dữ liệu mồ côi.
GC root -> static CACHE -> HashMap table
-> entry -> byte[] (retained: 400 MB)
Fix: bounded cache + expiry + measured hit rate4. Sửa và chứng minh đã hết leak
Cache cần maximum size/weight và expiry theo nghiệp vụ. Listener phải unregister trong lifecycle; ThreadLocal.remove() đặt trong finally. Đóng executor, stream và kết nối khi hết vòng đời. Lặp lại cùng tải trước/sau: post-GC floor phải ổn định, số object không tăng không giới hạn, latency không suy giảm. Chỉ tăng heap sau khi biết dữ liệu tăng là hợp lệ.
context.set(requestContext);
try { handleRequest(); }
finally { context.remove(); } // essential on pooled threadsMemory leak là đường tham chiếu sống lâu hơn nhu cầu nghiệp vụ. Đo, tìm GC root, sửa nguồn giữ rồi đo lại. Ngày 81 chuyển sang var và suy luận kiểu cục bộ.
Tài liệu đối chiếu
https://docs.oracle.com/en/java/javase/21/troubleshoot/troubleshooting-memory-leaks.html
https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/ThreadLocal.html
Site Admin
Engineer and writer. Building things with TypeScript and distributed systems.
Bình luận (0)
Bạn cần đăng nhập bằng Google để bình luận.
Hãy là người bình luận đầu tiên.


