Thời lượng 5:39. Nghe tập này trên Apple Podcasts rồi đọc kèm bản chữ dưới đây.
Linh: Xin chào anh, và chào mừng anh đến với đoạn podcast mở đầu về Java Memory Analyzer. Hôm nay, chúng ta sẽ nói về một kỹ năng rất thực tế với lập trình viên Java ở môi trường production, đó là đọc heap dump để tự tìm ra memory leak, thay vì chỉ khởi động lại ứng dụng mỗi khi nó báo hết bộ nhớ.
Minh: Nghe đúng cảnh quá. Ứng dụng chạy vài ngày thì chậm dần, bộ nhớ cứ tăng, bộ thu gom rác chạy liên tục, rồi cuối cùng văng ra lỗi OutOfMemoryError. Khởi động lại thì hết, nhưng vài ngày sau lại lặp lại.
Linh: Đúng vậy, và khởi động lại chỉ chữa triệu chứng thôi, nó không cho mình biết object nào đang giữ bộ nhớ. Muốn tìm được thủ phạm, mình cần một heap dump. Anh hình dung heap dump giống như một bức ảnh chụp toàn bộ bộ nhớ heap tại đúng một thời điểm, trong đó có mọi object đang còn sống, và cả mối liên hệ giữa chúng.
Minh: Vậy khi có heap dump rồi, mình mở bằng công cụ nào, và mình nhìn vào đâu đầu tiên hả Linh?
Linh: Công cụ chính của chúng ta là Eclipse MAT, tên đầy đủ là Memory Analyzer Tool. Nhưng trước khi bấm vào bất kỳ nút nào, anh cần nắm hai khái niệm nền tảng nhất, là shallow heap và retained heap. Shallow heap là kích thước của riêng một object, chưa tính những object mà nó đang giữ. Retained heap thì khác, đó là tổng bộ nhớ sẽ được giải phóng nếu object đó biến mất và bị thu gom.
Minh: Cho em một ví dụ cho dễ hình dung đi.
Linh: Dễ lắm. Một cái ArrayList có shallow heap chỉ vài chục byte, vì bản thân cái list thì rất nhỏ. Nhưng bên trong nó có thể đang giữ một triệu object đơn hàng, tổng cộng bốn trăm năm mươi megabyte. Vậy retained heap của cái list đó là bốn trăm năm mươi megabyte. Cho nên khi săn memory leak, mình luôn nhìn retained heap trước, vì nó mới cho thấy ai đang thật sự chiếm bộ nhớ.
Minh: À, vậy là object nhỏ xíu vẫn có thể là thủ phạm lớn nhất. Thế còn GC Roots mà mọi người hay nhắc là gì?
Linh: GC Roots là những điểm bắt đầu mà bộ thu gom rác coi là đang sống, ví dụ như biến local trên stack của một thread đang chạy, trường static của một class, hay chính các thread. Một object bị coi là rò rỉ khi nó không còn được dùng về mặt logic nữa, nhưng vẫn còn một đường tham chiếu từ GC Root giữ nó lại, nên rác không thu được.
Minh: Tức là muốn kết luận một object bị leak, mình phải lần được đường giữ nó về tới GC Root đúng không?
Linh: Chính xác, và trong Eclipse MAT, thao tác đó có tên là Path to GC Roots, nghĩa là đường đi tới GC Root. Nó cho mình thấy cả một chuỗi, ai đang giữ ai, và mắt xích nào là nơi mình cần sửa code.
Minh: Vậy quy trình đọc một heap dump cụ thể sẽ gồm những bước nào?
Linh: Anh nhớ năm bước này là đủ dùng cho hầu hết các ca thật. Bước một, mở heap dump trong Eclipse MAT và xem báo cáo Leak Suspects, vì công cụ sẽ chỉ sẵn vài nghi phạm lớn nhất. Bước hai, mở Histogram để xem class nào có nhiều instance bất thường. Bước ba, xem Dominator Tree để tìm object có retained heap lớn nhất. Bước bốn, từ object đó, chạy Path to GC Roots để tìm chuỗi đang giữ nó. Và bước năm, quay lại code, tìm đúng chỗ đã thêm object vào mà quên xóa ra.
Minh: Linh lấy ví dụ ca kinh điển nhất cho mình được không?
Linh: Đó chính là bài lab đầu tiên của anh, rò rỉ vì static List. Trong code có một danh sách dạng static dùng để lưu session của người dùng. Mỗi lần có người đăng nhập, một session mới được thêm vào, nhưng khi người dùng đăng xuất thì không ai xóa nó ra. Danh sách static đó là một GC Root, nên mọi session cứ nằm đó mãi, heap tăng đều mỗi ngày, cho tới khi hết bộ nhớ. Trong Dominator Tree, cái danh sách đó sẽ đứng đầu với retained heap khổng lồ, và Path to GC Roots sẽ chỉ thẳng về trường static. Cách sửa cũng rất rõ, hoặc là xóa session khi đăng xuất, hoặc là đổi sang một cache có giới hạn số phần tử.
Minh: Thế còn VisualVM thì đóng vai trò gì, nó khác Eclipse MAT ở chỗ nào?
Linh: Câu này hay nè. VisualVM mạnh ở việc theo dõi trực tiếp khi ứng dụng đang chạy, anh xem được heap tăng giảm theo thời gian, xem hoạt động thu gom rác, và xem các thread đang làm gì. Còn Eclipse MAT mạnh ở việc mổ xẻ sau sự cố, khi mình đã có heap dump trên tay. Một dấu hiệu leak kinh điển trên VisualVM là sau mỗi lần thu gom rác, mức bộ nhớ nền lại cao hơn lần trước một chút, cứ thế leo thang không dừng.
Minh: Vậy tóm lại, mình dùng VisualVM để phát hiện xu hướng, còn Eclipse MAT để tìm đúng thủ phạm.
Linh: Chuẩn luôn. Và đó cũng là lộ trình mình sẽ đi cùng nhau. Ban ngày, anh nghe và ôn khái niệm trên điện thoại. Buổi tối, anh mở laptop khoảng ba mươi phút, tự tay mở lại ca static List trong Eclipse MAT, tìm Leak Suspect và Path to GC Roots bằng chính tay mình. Nghe hiểu mới là một nửa, tự tay lần ra được chuỗi giữ object mới là lúc kỹ năng này thành của anh.
Minh: Rất rõ ràng. Vậy câu thần chú của tập hôm nay là gì hả Linh?
Linh: Là ba câu này. Một, khi săn leak, hãy nhìn retained heap trước. Hai, object bị leak luôn có một đường giữ về tới GC Root. Và ba, đừng chỉ khởi động lại ứng dụng, hãy chụp heap dump và để dữ liệu chỉ cho anh thấy thủ phạm. Cảm ơn anh đã nghe, và hẹn anh ở tập tiếp theo, nơi mình sẽ nói về bốn cách chụp heap dump an toàn trên production.