Khi nào bị Memory Leak trong các ngôn ngữ bậc cao?

Hôm nay mình ôn tập bị hỏi phần rò rỉ bộ nhớ trong các ngôn ngữ bậc cao. Như mình học C++ mình tự làm tay hết. Mình không hiểu vì sao các ngôn ngữ bậc cao có bộ thu hồi tự động rồi mà vẫn còn bị rò rỉ bộ nhớ. Mình có tìm hiểu thì hiểu như này.

    void generateMemoryLeak(int userInput){
        if( userInput == CMP ){
            var leak = new Obj();
        }
        new LeakedObj().doSomething();
        var unLeakedObj = new Obj();
    }

Không biết có phải không mong mn giúp đỡ ạ :sweat_smile:

Good question!

Để hiểu khi nào memory có thể bị leak, cậu nên hiểu ngôn ngữ bậc cao thu hồi cái gì tự động trước đã.
Về cơ bản, các ngôn ngữ bậc cao có một bộ dọn rác (garbage collector - GC for short). GC chịu trách nhiệm thu hồi bộ nhớ heap đối với các resource không còn được tham chiếu tới.
Có một vài điểm cậu cần biết:

  • Các object ở ngôn ngữ bậc cao đều được tạo ở trên bộ nhớ heap này. Ở C/cpp, khi cậu sử dụng malloc hoặc new, cậu đang khởi tạo một object nằm trên bộ nhớ này.
  • Một object khi không còn bất cứ biến nào tham chiếu tới sẽ dần dần được GC dọn dẹp.
    Ở các ngôn ngữ C/cpp, cậu cần phải free bộ nhớ này thủ công.
    Ở ví dụ của cậu đưa ra, biến leak sẽ không còn tham chiếu nào tới nó sau khi hết khối lệnh if, object new LeakObj cũng sẽ không có tham chiếu nào tới nó khi lệnh doSomething thực thi xong. 2 object này sẽ được dọn dẹp bởi GC, và đây không phải ví dụ về memory leak.

Khi cậu hiểu các điều trên, một cách logic, cậu sẽ biết những trường hợp khiến cậu có thể gặp memory leak:

  • Khi cậu luôn luôn giữ tham chiếu tới object đã tạo.
    Chẳng hạn, khi cậu có một embedded cache cài đặt on heap, và cậu setup cache đó với dung lượng lớn so với heap, hoặc disable luôn tính năng eviction của cache.
    Khi đó, các object trong cache khó có thể được evicted mà không khiến cache đó chiếm dung lượng lớn từ heap memory => memory leak.
  • Khi object của cậu tạo các resource không ở trên heap memory, nhưng không đóng nó lại một cách hợp lý.
    Chẳng hạn, nếu cậu làm việc với file, socket, database connection, etc., khi cậu tạo các object tương ứng, hệ điều hành sẽ tạo ra các resource để phục vụ việc đọc file/sử dụng socket/kết nối DB.
    Nếu cậu không đóng các object này cẩn thận (thường là gọi method close), các resource ở phía hệ điều hành sẽ không được giải phóng (GC không giải phóng gì ngoài heap). Dần dần, cậu sẽ hết bộ nhớ cho ứng dụng => leak memory.

Hope it helps!

15 Likes

cảm ơn nha, mình chỉ biết cái khúc bạn nói JDBC đóng các tài nguyên đã sử dụng để tránh leak :sweat_smile: :sweat_smile:

3 Likes

bạn có thể code cụ thể một vài trường hợp đơn giản bằng Java JavaScript được không ? mình còn mơ hồ chỗ giải thích đó :sweat_smile: :sweat_smile:

Được chứ :smile:
Cơ mà, cậu cần ví dụ cho phần nào vậy?

Với cả, để code 1 ví dụ đơn giản, tớ cần mở máy tính, nên cậu có thể sẽ phải chờ một thời gian để có ví dụ đó :smile:

1 Like

ví dụ viết một hàm có leak ă, Java JavaScript đều được :grin:

tạo 1 biến global là List<Object> rồi mỗi khi tạo object mới thì add nó vào đây là nó tồn tại tới hết chương trình, nghĩa là leak =] Nếu như trong C hoặc C++ phải chống leak bằng tay thì trong các ngôn ngữ có garbage collector ta phải leak bằng tay =]]

3 Likes

hịc vẫn chưa hiểu lắm :sweat_smile:, bạn có thể code một hàm mà có leak một cách đơn giản được không ạ ? dùng Java hoặc JavaScipt cũng đc ă

Code dưới đây là phiên bản khác của lời mà @tntxtnt đã đề cập ở trên thôi :smile:

public class MyDumbStringCache {
  private static Map<String, Object> cache = new HashMap<>();

  public static void put(String key, Object value) {
    cache.put(key, value);
  }

  public static Optional<Object> get(String key) {
    return Optional.ofNullable(cache.get(key));
  }
}

Trên đây là ví dụ đơn giản nhất về memory leak. Cậu có thể thấy khi cậu add entry vào trong cache, cache sẽ giữ reference tới các object key và value cậu truyền vào, và vì cache object là static member, nó luôn có reference một khi class MyDumbStringCache được load bởi class loader. Vì thế, GC trong TH này không bao giờ có thể dọn dẹp các object đó.

Hope it helps!

5 Likes

giả lập C malloc/free :joy:

private static HashSet<Object> hset = new HashSet<>();

public static Object oalloc(Object obj) {
    hset.add(obj);
    return obj;
}

public static void ofree(Object obj) {
    hset.remove(obj);
}

public static String getRandomString(int length, Random rnd) {
    String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
    StringBuilder sb = new StringBuilder();
    while (sb.length() < length) { // length of the random string.
        int index = (int) (rnd.nextFloat() * CHARS.length());
        sb.append(CHARS.charAt(index));
    }
    return sb.toString();
}

public static void main(String args[]) {
    Random rnd = new Random();
    for (int i = 0; i < 1_000_000; ++i) {
        String s = (String)oalloc(getRandomString(24, rnd));
        // ofree(s);
    }
}

đương nhiên đâu ai khùng viết code như vậy :V :V


ví dụ leak từ Java docs: https://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
   
    FileReader fr = new FileReader(path);
    BufferedReader br = new BufferedReader(fr);
    try {
        return br.readLine();
    } finally {
        br.close();
        fr.close();
    }
}

viết thế này là có thể leak rồi :V

6 Likes

Trường hợp trong ví dụ này thì sửa như thế nào để không bị memory leak hả @tntxtnt?

1 Like

Trong link có ghi đó, xài try-with-resource statement:

try (FileReader fr = new FileReader(path);
     BufferedReader br = new BufferedReader(fr)) {
    return br.readLine(); 
}
6 Likes

Uhm, hoặc là sửa thế này (java 6-):

static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
   
    FileReader fr = null;
    BufferedReader br = null;
    try {
        FileReader fr = new FileReader(path);
        BufferedReader br = new BufferedReader(fr);
        return br.readLine();
    } finally {
        if (br != null) {
          br.close();
        }
        if (fr != null) {
          fr.close();
        }
    }
}

Try with resource thực tế làm nhiệm vụ tương tự với code trên. Nếu resource không implement interface AutoClosable mà cần phải gọi method close (hay method nào tương tự) để dọn dẹp tài nguyên, thì cách implement trên là cách duy nhất.

4 Likes
  1. Hình như chỗ này có vấn đề.
static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
   
    FileReader fr = null;
    BufferedReader br = null;
    try {
        FileReader fr = new FileReader(path);
        BufferedReader br = new BufferedReader(fr);
    ...

Phải là

static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
   
    FileReader fr = null;
    BufferedReader br = null;
    try {
        fr = new FileReader(path);
        br = new BufferedReader(fr);

Đúng không @library?

  1. Còn nữa, nếu bỏ đi 2 dòng init null thì có ổn không?
static String readFirstLineFromFileWithFinallyBlock(String path) throws IOException {
   
    try {
        FileReader fr = new FileReader(path);
        BufferedReader br = new BufferedReader(fr);
        return br.readLine();
    } finally {
        if (br != null) {
          br.close();
        }
        if (fr != null) {
          fr.close();
        }
    }
}
3 Likes

À, đoạn code đầu tiên của cậu đúng đó. Tớ copy paste ở trên và quên không xóa đoạn khai báo trong try block :sweat_smile:

Đoạn code sau của cậu có lẽ sẽ không chạy, vì scope của biến frbr chỉ ở try block. Finally block sẽ không có biến frbr nữa.

4 Likes
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?