mm leaks

Java ngầm thu hồi lại bộ nhớ thông qua từng chu kỳ kiểm tra của GC - Garbage Collector. GC phát hiện và thu hồi các unreachable objects - tức các object không được tham chiếu tới.

Như vậy, nếu object không được sử dụng đến trong application (hay còn gọi là unused) nhưng bằng một cách nào đó, vẫn tồn tại tham chiếu tới object đó thì đây chính là nguy cơ xảy ra memory leaks.

Oái oăm thay, bộ Garbage Collector chỉ nhận biết được "unreachable" object chứ không nhận biết được unused object. Sự tồn tại của unused object lệ thuộc vào logic của chương trình. Do đó, lập trình viên phải hết sức thận trọng khi code. Nếu không, những lỗi ngớ ngẩn nhất có thể biến thành tai họa khôn lường trong tương lai.

Chúng ta cùng xem xét 6 nguy cơ xảy ra memory leaks trong Java.

#1 Autoboxing:

package com.example.memoryleak;

public class Adder {

       public long addIncremental(long l)

       {

              Long sum=0L;

               sum =sum+l;

               return sum;

       }

       public static void main(String[] args) {

              Adder adder = new Adder();

              for(long ;i<1000;i++)

              {

                     adder.addIncremental(i);

              }

       }

}

Bạn có phát hiện được memory leaks trong trường hợp này?

Sai lầm của tôi nằm ở dòng code thứ 5. Thay vì sử dụng kiểu dữ liệu nguyên thủy long cho biến sum, tôi lại dùng Wrapper class Long, từ đó, theo cơ chế autoboxing, vòng lặp này sẽ tạo ra 1000 object sau 1000 bước lặp.

Bài học: chúng ta cần phân biệt rạch ròi các trường hợp sử dụng primitive type và wrapper class. Hãy cố gắng sử dụng primitive type nhiều nhất có thể.

#2 Sử dụng Custom Cache

package com.example.memoryleak;

import java.util.HashMap;

import java.util.Map;

public class Cache {

       private Map<String,String> map= new HashMap<String,String>();

       public void initCache()

       {

              map.put("Anil", "Work as Engineer");

              map.put("Shamik", "Work as Java Engineer");

              map.put("Ram", "Work as Doctor");

       }

       public Map<String,String> getCache()

       {

              return map;

       }

       public void forEachDisplay()

       {

              for(String key : map.keySet())

              {

                String val = map.get(key);                 

                System.out.println(key + " :: "+ val);

              }

       }

       public static void main(String[] args) {            

              Cache cache = new Cache();
              cache.initCache();
              cache.forEachDisplay();

       }

}

Tại ví dụ này, memory leaks xảy ra do cấu trúc dữ liệu map

Trên thực tế, nhiệm vụ của class Cache là hiển thị giá trị của employee từ cache. Một khi giá trị đã được hiển thị, ta không cần lưu chúng trong cache nữa. Vấn đề ở đây là ta "quên" clear cache. Mặc dù các object kiểu HashMap trong cache không được application dùng đến nhưng chúng vẫn tồn tại  và được "lưu trữ trong map" (tức map vẫn chứa tham chiếu tới các object này), do đó bộ GC không thể "dọn dẹp" các object "thừa". Vậy là... memory leaks.

Từ ví dụ này, có 2 giải pháp tránh memory leaks khi làm việc với cache:

  1. Clear cache khi không sử dụng
  2. Sử dụng WeakHashMap để init cache thay vì HashMap như truyền thống. Với các cặp <key,value> trong WeakHashMap, nếu key không bị tham chiếu bởi object nào thì cặp <key,value> đó sẽ được GC dọn dẹp. WeakHashMap là một chủ đề hấp dẫn, tôi sẽ dành hẳn một bài viết về nó. 

Khóa học Java căn bản - có sản phẩm - có việc làm

#3 Closing Connection

try

{

  Connection con = DriverManager.getConnection();

  …………………..

    con.close();

}

Catch(exception ex)

{

}

Qúa rõ ràng, chúng ta close connection trong khối try, như vậy nếu có exception, connection sẽ không bị close(), và đây là lỗi memory leaks ngớ ngẩn nhất mà bạn có thể mắc phải.

Hãy nhớ, chỉ nên gọi hàm close() trong khối finally.

#4 Custom Key

package com.example.memoryleak;

import java.util.HashMap;

import java.util.Map;

public class CustomKey {

       public CustomKey(String name)

       {

              this.name=name;

       }

       private String name;

       public static void main(String[] args) {

              Map<CustomKey,String> map = new HashMap<CustomKey,String>();

              map.put(new CustomKey("Shamik"), "Shamik Mitra");

              String val = map.get(new CustomKey("Shamik"));

              System.out.println("Missing equals and hascode so value is not accessible from Map " + val);

       }

}

Trong class CustomKey này, chúng ta quên không implement 2 method là equals()hashCode(). Nói thêm một chút, để truy cập tới một object của map, method get() sẽ check hashCode()equals() cho từng object. Như vậy, hậu qủa của việc không implement 2 method kia là key và value lưu trong map không thể lấy ra được. Việc trong map tồn tại các cặp <key,value> nhưng application không thể "dùng" được chúng chắc chắn được liệt vào danh sách memory leaks.

Như vậy, khi tạo CustomKey, đừng quên equals()hashCode().

#5 Mutable Custom Key

package com.example.memoryleak;

import java.util.HashMap;

import java.util.Map;

public class MutableCustomKey {

       public MutableCustomKey(String name)

       {

              this.name=name;

       }

       private String name;

       public String getName() {

              return name;

       }

       public void setName(String name) {

              this.name = name;

       }

       @Override

       public int hashCode() {

              final int prime = 31;

              int result = 1;

              result = prime * result + ((name == null) ? 0 : name.hashCode());

              return result;

       }

       @Override

       public boolean equals(Object obj) {

              if (this == obj)

                     return true;

              if (obj == null)

                     return false;

              if (getClass() != obj.getClass())

                     return false;

              MutableCustomKey other = (MutableCustomKey) obj;
              if (name == null) {

                     if (other.name != null)

                           return  false;

              } elseif (!name.equals(other.name))

                     return false;

              return true;

       }

       public static void main(String[] args) {

              MutableCustomKey key = new MutableCustomKey("Shamik");             

              Map<MutableCustomKey,String> map = new HashMap<MutableCustomKey,String>();

              map.put(key, "Shamik Mitra");

              MutableCustomKey refKey = new MutableCustomKey("Shamik");

              String val = map.get(refKey);

              System.out.println("Value Found " + val);

              key.setName("Bubun");

              String val1 = map.get(refKey);

              System.out.println("Due to MutableKey value not found " + val1);

       }

}

Trường hợp này có equals()hashCode() cho CustomKey nhưng vô tình chúng ta đã biến các object CustomKey thành mutable sau khi lưu nó vào map. Nếu thuộc tính của chúng thay đổi thì application sẽ không thể "tìm" được chúng. Tuy nhiên map vẫn giữ tham chiếu tới các object đó >> Memory leaks.

Bài học: CustomKey luôn đi kèm với tính Immutable

#6 Memory leaks trong nội tại cấu trúc dữ liệu

package com.example.memoryleak;

public class Stack {

       private int maxSize;

       private int[] stackArray;

       private int pointer;

       public Stack(int s) {

              maxSize = s;

              stackArray = newint[maxSize];

              pointer = -1;

       }

       public void push(int j) {

              stackArray[++pointer] = j;

       }

       public int pop() {

              return stackArray[pointer--];

       }

       public int peek() {

              return stackArray[pointer];

       }

       public boolean isEmpty() {

              return (pointer == -1);

       }

       public boolean isFull() {

              return (pointer == maxSize - 1);

       }
       public static void main(String[] args) {

              Stack stack = new Stack(1000);

              for(int ;i<1000;i++)

              {

                     stack.push(i);

              }

              for(int ;i<1000;i++)

              {

                     int element = stack.pop();

                     System.out.println("Poped element is "+ element);

              }

       }

}

Vấn đề này hơi oái oăm - stack của chúng ta bị "co-giãn".

Mọi đau khổ bắt nguồn từ qúa trình implementation một stack của lập trình viên. Dưới góc nhìn của con người, Stack giống một mảng (với quy tắc ghi-xóa phần tử đặc biệt), tuy nhiên từ góc nhìn của application, bộ phận khả dụng của Stack là nơi con trỏ trỏ đến.

Vì thế, xét trong ví dụ này, khi Stack tăng lên 1000 phần tử, các ô trống của nó sẽ được lấp đầy. Tuy nhiên sau khi chúng ta pop() tất cả phần tử ra, con trỏ trỏ về 0. Application căn cứ vào gía trị con trỏ trỏ đến, và sẽ xem xét stack này là stack rỗng. Tuy vậy, stack này vẫn chữa tham chiếu tới các phần tử bị pop(). Trong Java, các tham chiếu kiểu này được gọi là obsolete reference - tham chiếu thừa. Tham chiếu thừa là những tham chiếu không thể "gỡ" đi được - tức không thể "dọn dẹp" chúng bằng GC. 

Khắc phục: gán giá trị null cho mỗi object sau khi chúng bị pop().

Leak fixed

Tổng hợp lại, ta có 5 giải pháp phòng ngừa memory leaks trong Java:

  1. Không lẫn lộn Primitive Type với Wrapper Class trong quá trình truyền parameter. 
  2. Khi sử dụng custom cache, nhớ clear cache
  3. Luôn đặt method close() trong khối finally
  4. Cảnh giác với obsolete reference
  5. Khi sử dụng Custom Object, chắc chắn rằng chúng là Immutable
Methods
5 Biện pháp phòng tránh Memory leaks

Bài viết được dịch từ dzone.com