How to avoid memory leaks in Java

Tự quản lý bộ nhớ trong máy ảo Java (JVM) được xem là tính năng mạnh nhất của Java, và là một trong những lý do khiến các lập trình viên chọn Java thay vì chọn các nền tảng và ngôn ngữ lập trình khác. Theo lý thuyết mà các Java-er thường quảng cáo là "bạn chỉ cần viết code tạo các đối tượng - object và Java sẽ triển khai Garbage Collector của nó để phân bổ và phóng bộ nhớ giúp bạn". Trên thực tế Java không hoàn hảo đến vậy. Việc bộ nhớ bị "rò rỉ" (memory leaks - không được giải phóng khi object không còn được sử dụng) xảy ra nhiều trong các ứng dụng Java. Mình tìm hiểu và trình bày trong bài viết này một số phương pháp phát hiện, tránh và khắc phục memory leaks trong Java.

Memory leaks là gì? Nó có khiến bạn lo lắng?

Memory leaks chỉ việc ứng dụng trong quá trình chạy không được giải phóng bộ nhớ khi các object không còn được sử dụng nữa, nó làm cho bộ nhớ trống giảm dần. Thường thì memory leaks ảnh hưởng đến một lượng nhỏ bộ nhớ nên ứng dụng Java của bạn vẫn chạy bình thường. Nhưng khi hệ thống bắn ra ngoại lệ java.lang.OutOfMemoryError và ứng dụng ngừng hoạt động thì đó là vấn đề cần được xử lý. Có hai lại object trong bộ nhớ củ JVM là Referenced Objects và Unreferenced Objects. Unreferenced Objects là những object thuộc đối tượng thu gom của Garbage Collector, còn Referenced Object thì không. Ở hình trên ta thấy một số object không còn được sử dụng nữa nhưng vần thuộc nhóm Referenced Objects, chúng là biểu hiện của Memory leaks. Memory leaks là dấu hiệu điểm chỉ rằng chương trình được viết không tốt. Nếu bạn thuộc nhóm lập trình viên mà muốn mọi thứ thật hoàn hảo thì bạn phải điều tra bất kì dấu hiệu memory leaks nào xảy ra. Là lập trình viên Java chúng ta hiểu rằng không có cách nào biết được chính xác trình Garbage Collector của JVM thực sự hoạt động khi nào, ngay cả khi ta chủ động gọi method System.gc() trong ứng dụng của ta. Ta chỉ biết rằng Garbage Collector hoạt động khi bộ nhớ trống thấp, hay bộ nhớ trống ít hơn nhu cầu của hệ ứng dụng. Và nếu Garbage Collector không giải phóng đủ bộ nhớ cho ứng dụng thì JVM sẽ lấy bộ nhớ từ hệ điều hành. Tuy vậy, memory leaks xảy ra trong Java thì ít nghiêm trọng hơn khi xảy ra với C++ hay một số ngôn ngữ khác. Theo Jim Patrick của IBM Developer Works thì chúng ta cần quan tâm đến hai yếu tố của memory leaks; thứ nhất là độ lớn của bộ nhớ bị "rò rỉ", và hai là tuổi thọ của chương trình.

Một ứng dụng có thời gian chạy liên tục dài, bị memory leaks với kích thước nhỏ và JVM có đủ memory cho ứng dụng chạy thông suốt. Một ứng dụng khác bị memory leaks lớn nhưng có thời gian hoạt động ngắn thì memory leaks không phải là vấn đề nghiêm trọng.

Làm sao để loại bỏ memory leaks?

Để loại bỏ memory leaks cho ứng dụng ta cần chú ý khi viết source code, và sử dụng các công cụ giám sát, sau đây là một vài phương pháp cụ thể.

1. Sử dụng Reference Objects đặc biệt

Sử dụng gói thư viện java.lang.ref ta có thể làm việc với trình rọn rác Garbage Collector trong ứng dụng của mình. Bằng cách này ta tránh được việc trực tiếp tham chiếu các object, nhưng có thể sử dụng các object tham chiếu đặc biệt mà những object này dễ dàng được "rọn" bởi Garbage Collector. Các class con đặc biệt giúp ta liên hệ đến các object một cách trực tiếp. Ví dụ như các subclasss: PhantomReference, SoftReference, và WeakReference. Một tham chiếu hay một đối tượng mà được tham chiếu bởi các subclass này, ta có thể truy cập chúng bằng việc gọi method get(). Lợi ích của việc sử dụng method get() này là ta có thể clear một tham chiếu một cách dễ dàng bằng cách gán chúng bằng NULL. Giờ ta sẽ tìm hiểu xem trình rọn rác Garbage collector làm việc thế nào với từng loại tham chiếu.

  • Đối tượng SoftReference: Garbage collector sẽ loại bỏ toàn bộ các đối tượng SoftReference khi bộ nhớ (RAM) còn lại thấp.
  • Đối tượng WeakReference: Khi Garbage collector tìm thấy một tham chiếu object "yếu" thì nó xóa toàn bộ các tham chiếu đến nó, loại bỏ tất cả chúng khỏi bộ nhớ.
  • Đối tượng PhantomReference: Garbage collector không tự động xóa các đối tượng thuộc loại PhantomReference, ta phải xóa các tham chiếu đến nó một cách thủ công. Sử dụng các đối thượng tham chiếu, ta có thể làm việc với Garbage collector để tự động loại bỏ các đối tượng tham chiếu "yếu". Đặc biệt với các đối tượng WeakReference ta có thể loại bỏ memory leaks bằng cách tạo các thread chuyên xóa các tham chiếu không còn được sử dụng.

2. Luôn đóng connection

Trong ứng dụng Java việc sử dụng kết nối đến database hay FTP server là thường xuyên. Khi viết code ta không lưu ý đến việc đóng connection khi kết thúc phiên làm việc thì dễ dẫn đến memory leaks.

try
{
  Connection con = DriverManager.getConnection();
  …………………..
    con.close();
}
Catch(exception ex)
{
}

Ở ví dụ trên, ta đã chủ đọng đóng kết nối khi kết thúc tác vụ, nhưng vấn đề ở đây là việc đóng connection được thực hiện trong try block, nếu có exception xảy tra trong try thì connection không được đóng nữa. Do vậy ta phải tuân thủ quy tắc là luôn đóng kết nối ở finally block.

3. Loại bỏ memory leaks liên quan đến trình classloader WebApp

Trong Java, mỗi object luôn giữ tham chiếu đến class tạo ra nó java.lang.Class, mỗi class lại giữ tham chiếu đến classloader đã nạp nó. Mỗi classloader lại giữ tham chiếu đến toàn bộ class mà nó đã nạp. Tình huống một object A1 được tạo ra bởi classloader A tham chiếu đến một object B1 được tạo ra bở classloader B. Như vậy object B1 sẽ không được giải phóng chừng nào object A1 còn được sử dụng. Nghĩa là classloader B cũng không được giải phóng, toàn bộ class thuộc classloader B cũng không được giải phóng lâu dần dẫn đến OutOfMemory exception. Chúng cần phải tìm ra classloader này và xem nó nắm giữ những gì để xử lý. Công cụ Java Visual VM là một công cụ với giao diện người dùng trực quan có thể kết nối đến mọi máy ảo Java - JVM. Nó cho phép ta lấy bản dump bộ nhớ heap của JVM và điều hướng heap đó. Trong Tomcat, classloader WebApp là một class có tên org.apache.catalina.loader.WebappClassLoader. Nếu Tomcat có một web app được deploy thì chỉ có một đối tượng của classloader trong heap ngoài đối tượng quản lý ứng dụng của Tomcat . Nếu có nhiều hơn hai object trong heap thì xảy ra memory leaks. Chúng ta có thể sử dụng VisualVM để kiểm tra heap. Khởi động VisualVM bằng lệnh

${JAVA_HOME}/bin/jvisualvm

Giao diện người dùng như sau Để kiểm tra có bao nhiêu object classloader trong heap ta gõ lệnh search trong cửa sổ "Query Editor" của tab OQL Console

select x from org.apache.catalina.loader.WebappClassLoader x

Trường hợp này cửa sổ "Query Results" trả về 3 object vậy là hiện leaks xảy ra. Để biết object nào là leaks, trong Tomcat có cách thức khá đơn giản là classloader nào đang hoạt động thì trường started có giá trị là true, ngược lại có giá trị là false. Tại tab Instances, ta click chọn từng object Id (#1, #2, #3) đến khi thấy giá trị false của trường started thì đó là object ta cần tìm (trong trường hợp này là #1). Trong phạm vi bài viết này mình không thể đi chi tiết của công cụ VisualVM này mà chỉ mang tính chất giới thiệu chức năng.

Tài liệu tham khảo