Chuyển đến nội dung chính

Rò rỉ bộ nhớ: Nguyên nhân, cách phát hiện và khắc phục

Khám phá nguyên nhân gây rò rỉ bộ nhớ, ví dụ thực tế, phương pháp phát hiện và thực tiễn tốt nhất để ngăn sự cố hiệu năng.
Srujana Maddula
Srujana Maddula

Tạo tài khoản miễn phí của bạn

Luyện tập và Lộ trình tiến trình của bạn.

Tiếp tục với GoogleBằng cách tiếp tục, bạn chấp nhận Điều khoản sử dụng, Chính sách quyền riêng tư và việc dữ liệu của bạn được lưu trữ tại Hoa Kỳ.

Mọi chương trình bạn chạy, dù lớn hay nhỏ, đều cần bộ nhớ để xử lý. Tuy nhiên, khi một chương trình sử dụng bộ nhớ mà không giải phóng lại sau khi dùng xong, điều đó dẫn đến rò rỉ bộ nhớ. 

Theo thời gian, nhiều rò rỉ bộ nhớ có thể gây thiếu hụt tài nguyên và không thể xử lý các tác vụ sắp tới. Đó là lý do việc phát hiện và quản lý rò rỉ bộ nhớ rất quan trọng để tránh lỗi không cần thiết.

Trong bài viết này, chúng ta sẽ tìm hiểu sâu về rò rỉ bộ nhớ. Bắt đầu thôi!

Rò rỉ bộ nhớ là gì?

Rò rỉ bộ nhớ xảy ra khi một chương trình hoặc ứng dụng sử dụng bộ nhớ nhưng không giải phóng lại sau khi dùng xong. Ở đây bộ nhớ tức là RAM—đừng nhầm với bộ nhớ ổ cứng.  Những rò rỉ này dần tích tụ, khiến RAM quá đầy để xử lý các tiến trình mới. 

Rò rỉ bộ nhớ làm cạn kiệt RAM và ảnh hưởng hiệu năng bằng cách tăng các thao tác I/O. Khi rò rỉ tích tụ, hệ thống cố giải phóng RAM bằng cách chuyển dữ liệu xuống đĩa, dẫn đến bùng nổ thao tác I/O trên đĩa.

Bảo mật cũng gặp rủi ro khi rò rỉ bộ nhớ giữ lại dữ liệu nhạy cảm. Nếu thông tin như mật khẩu hoặc khóa mã hóa lưu trong RAM lâu hơn cần thiết, chúng sẽ dễ bị kẻ tấn công nhắm tới hơn.

Ví dụ về rò rỉ bộ nhớ trong các ngôn ngữ lập trình phổ biến

Để hiểu rõ hơn rò rỉ bộ nhớ xảy ra thế nào, không gì bằng xem chúng hoạt động qua một số ví dụ. 

Rò rỉ bộ nhớ trong Python

Python dựa vào đếm tham chiếu để quản lý bộ nhớ. Nó xóa một đối tượng khi số đếm tham chiếu—số đối tượng khác tham chiếu đến nó—giảm về 0. Tuy nhiên, cơ chế này có hạn chế, chẳng hạn không xử lý tham chiếu vòng.

Với những ai chưa quen thuật ngữ này, tham chiếu vòng xảy ra khi hai hoặc nhiều biến tham chiếu lẫn nhau tạo thành vòng tròn. 

Ví dụ, đối tượng a tham chiếu đến b, b tham chiếu đến c, và c tham chiếu ngược lại a, tạo thành vòng lặp vô tận. Ở đây, số đếm tham chiếu không thể về 0, và các đối tượng bị giữ mãi trong bộ nhớ, dẫn đến rò rỉ bộ nhớ trong Python.

class Node:
    def __init__(self, value):
        self.value = value
        self.next = None 

# Create two nodes
node1 = Node(1)
node2 = Node(2)

# Establish a circular reference
node1.next = node2  # node1 refers to node2
node2.next = node1  # node2 refers back to node1

Đoạn mã trên cho thấy một tham chiếu vòng. Python có bộ gom rác để xử lý các trường hợp này. Tuy nhiên, nếu nó không xử lý được một số tình huống cụ thể, như khi dùng biến toàn cục, bạn nên tự quản lý rò rỉ bộ nhớ bằng cách đặt các tham chiếu đối tượng không còn dùng đến về None. 

Tôi khuyên bạn nên học cách viết lớp tiết kiệm bộ nhớ trong Python để tăng tốc thời gian chạy trong khi tiêu tốn ít tài nguyên hơn.

Rò rỉ bộ nhớ trong Java

Java tự động quản lý bộ nhớ và không cần lập trình viên can thiệp trực tiếp. Tuy nhiên, trong các trường hợp như đăng ký listener không đúng cách hoặc tham chiếu tĩnh, bộ gom rác có thể không giải phóng được vùng nhớ, dẫn đến rò rỉ bộ nhớ. Cách xảy ra như sau:

  • Nguồn sự kiện tạo ra các sự kiện, và một đối tượng listener tự đăng ký để được thông báo khi sự kiện xảy ra. 
  • Nếu nguồn sự kiện giữ một tham chiếu mạnh đến listener, listener sẽ không thể bị gom rác chừng nào nguồn sự kiện còn sống, ngay cả khi listener không còn được sử dụng. 

Trong các trường hợp như vậy, rò rỉ bộ nhớ có thể xảy ra. Để tránh, hãy hủy đăng ký listener thủ công khi không còn cần thiết.

Một nguyên nhân phổ biến khác của rò rỉ bộ nhớ là biến tĩnh (static). Biến tĩnh được lưu trong bộ nhớ suốt vòng đời chương trình. Vì vậy, nếu chúng tham chiếu đối tượng, các đối tượng đó sẽ không bị gom rác, dù không còn dùng, gây rò rỉ bộ nhớ. Ví dụ:

import java.util.ArrayList;
import java.util.List;

public class StaticMemoryLeak {

    // Static list
    private static List<Object> staticList = new ArrayList<>();

    public static void addObject(Object obj) {
        // Add the object to the static list
        staticList.add(obj);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            Object obj = new byte[1024 * 1024];
            // Add the object to the static list
            addObject(obj);
        }
    }
}

Trong chương trình trên, biến staticList được khai báo là mảng tĩnh và các đối tượng được thêm vào đó. Vòng lặp chạy 1000 lần, mỗi lần thêm 1 MB đối tượng, tổng cộng 1 GB bộ nhớ. 

Vì danh sách là tĩnh, nó sẽ không bao giờ mất tham chiếu đến các đối tượng chừng nào chương trình còn chạy. Do đó, bộ gom rác không thể xóa các đối tượng đó và 1 GB bộ nhớ sẽ bị giữ trong suốt vòng đời. Bộ nhớ đối tượng không sử dụng này có thể gây lỗi OutOfMemoryError. Để khắc phục, bạn nên gọi rõ ràng staticList.clear() để giải phóng bộ nhớ.

Rò rỉ bộ nhớ trong C/C++

Khác với Java và Python, C++ không có bộ gom rác tự động. Vì vậy, rò rỉ bộ nhớ trong C++ xảy ra khi lập trình viên cấp phát bộ nhớ nhưng quên giải phóng. Ví dụ, 

void sample_leak() { 
    int* ptr = new int(5);
    return; 
}

Trong đoạn mã trên, con trỏ giữ địa chỉ bộ nhớ nơi số nguyên 5 được lưu. Khi hàm kết thúc, biến cục bộ ptr ra khỏi phạm vi, nhưng vùng nhớ đã cấp phát trên heap vẫn còn, gây rò rỉ bộ nhớ. 

Để ngăn chặn, hãy thêm rõ ràng dòng delete ptr; để giải phóng bộ nhớ.

Rò rỉ bộ nhớ trong JavaScript

Javascript có bộ gom rác tự động quản lý bộ nhớ. Nó hoạt động qua các bước: tìm nút gốc, đệ quy theo đường dẫn đến các nút con và đánh dấu mỗi nút con là đang hoạt động hoặc không. Cuối cùng, xóa tất cả tham chiếu không hoạt động. 

Tuy nhiên, bộ gom rác có thể không giải phóng bộ nhớ trong các trường hợp sau:

  1. Biến toàn cục: Khi bạn quên khai báo biến với let, var hoặc const, Javascript sẽ tự động tạo nó thành biến toàn cục. Chừng nào chương trình chạy, biến toàn cục còn nằm trong bộ nhớ và luôn có thể truy cập, ngăn bộ gom rác giải phóng chúng. 
  2. setTimeOut(): setTimeOut() lên lịch một hàm gọi lại (callback) để thực thi sau một khoảng thời gian. Rò rỉ bộ nhớ có thể xảy ra khi hàm callback này giữ tham chiếu quá lâu. Ví dụ:
function TimeoutExample() {
  var obj = 10; 
  setTimeout(function() {
    console.log(obj); 
  }, 1000);  // This runs after 1 second
}

TimeoutExample();

Trong đoạn mã trên, ngay cả sau khi hàm TimeoutExample() kết thúc, obj vẫn giữ tham chiếu cho đến khi hàm callback được thực thi vì obj được sử dụng bên trong callback. Nếu điều này xảy ra với các đối tượng lớn, nó dẫn đến rò rỉ bộ nhớ nghiêm trọng. 

Nguyên nhân gây rò rỉ bộ nhớ

Như đã đề cập, rò rỉ bộ nhớ có thể xảy ra trong nhiều ngôn ngữ lập trình vì nhiều lý do. Một số ngôn ngữ thiếu bộ gom rác tự động, trong khi số khác có thể không giải phóng bộ nhớ trong các tình huống bất thường. 

Hãy xem chi tiết một số nguyên nhân phổ biến:

  • Tham chiếu không được giải phóng: Bất cứ khi nào chúng ta tạo đối tượng hoặc biến, hệ thống sẽ cấp phát bộ nhớ hoặc tham chiếu cho chúng. Không đóng các tham chiếu đó sau khi sử dụng có thể chặn bộ nhớ. Dạng bộ nhớ này tích tụ theo thời gian và dẫn đến vấn đề bộ nhớ. 
  • Tham chiếu vòng: Tham chiếu vòng xảy ra khi nhiều đối tượng tham chiếu lẫn nhau tạo thành một vòng. Dù các đối tượng không còn dùng, chúng vẫn được tham chiếu bởi những đối tượng khác trong chu kỳ, ngăn bộ gom rác thu hồi bộ nhớ của chúng.
  • Quản lý tài nguyên không đúng cách: Đôi khi, chúng ta không chú ý đóng đúng cách kết nối cơ sở dữ liệu, socket mạng, hoặc file handle sau khi sử dụng. Sự lơ là này có thể dẫn đến vấn đề rò rỉ bộ nhớ nghiêm trọng. Ví dụ, hệ điều hành có số lượng file descriptor giới hạn để xử lý tệp đang mở. Nếu tệp không bao giờ được đóng, hệ điều hành sẽ cạn file descriptor, khiến hệ thống không thể mở tệp mới.
  • Lạm dụng biến tĩnh: Rò rỉ bộ nhớ thường xảy ra do sử dụng quá nhiều biến tĩnh. Vì biến tĩnh tồn tại suốt vòng đời chương trình, mọi đối tượng chúng tham chiếu sẽ nằm trong bộ nhớ—ngay cả khi không còn dùng. Để ngăn rò rỉ, các đối tượng này phải được giải phóng rõ ràng hoặc tránh dùng biến tĩnh.
  • Thư viện và framework bên ngoài: Thư viện bên thứ ba và framework bên ngoài cũng có thể gây rò rỉ bộ nhớ do quản lý tài nguyên kém hiệu quả. Rò rỉ xảy ra nếu chúng sử dụng tài nguyên hệ thống của bạn nhưng không giải phóng lại. 

Cách phát hiện rò rỉ bộ nhớ?

Giờ chúng ta đã xem vì sao rò rỉ bộ nhớ có thể xảy ra, hãy bàn về một số phương pháp đáng tin cậy để phát hiện và gỡ lỗi chúng.

  • Kiểm tra thủ công: Xem xét kỹ mã nguồn để xác định nguyên nhân tiềm ẩn của rò rỉ bộ nhớ, như tham chiếu vòng, biến tĩnh, thiếu câu lệnh giải phóng, hoặc biến giữ tham chiếu đến đối tượng không dùng.  Bằng cách rà soát kỹ các mẫu này trong mã, bạn có thể phát hiện chính xác vấn đề và xử lý chủ động. 
  • Dùng công cụ gỡ lỗi: Có nhiều công cụ và thư viện gỡ lỗi theo từng ngôn ngữ để phát hiện rò rỉ bộ nhớ. Ví dụ, tracemalloc của Python cho phép so sánh snapshot bộ nhớ tại các thời điểm khác nhau và theo dõi cấp phát giữa các khoảng thời gian.  Ngoài ra, trình profiler bộ nhớ của Python cho phép gỡ lỗi từng dòng mã và phát hiện khu vực sử dụng bộ nhớ quá mức bất ngờ.
  • Giám sát việc sử dụng bộ nhớ: Công cụ giám sát bộ nhớ theo dõi chủ động mức tiêu thụ và phát hiện vấn đề theo thời gian thực. Các công cụ này cảnh báo sự cố bộ nhớ nghiêm trọng và cung cấp log liên quan, kèm gợi ý cách khắc phục. Ví dụ gồm Valgrind, Paessler, AddressSanitizer, và các công cụ khác. 
  • Viết test phát hiện rò rỉ: Hãy cân nhắc bổ sung unit test để phát hiện rò rỉ bộ nhớ. Như vậy, bên cạnh lỗi chức năng và hiệu năng, rò rỉ bộ nhớ sẽ được phát hiện ngay trong quá trình phát triển. 
  • Viết kiểm thử tích hợp: Kiểm thử tích hợp cũng nên được dùng để mô phỏng các kịch bản thực tế và phát hiện rò rỉ bộ nhớ có thể nảy sinh trong môi trường sản xuất.

Thực tiễn tốt nhất để ngăn rò rỉ bộ nhớ

Rò rỉ bộ nhớ rất phổ biến trong các ứng dụng phụ thuộc vào tài nguyên bộ nhớ. Dưới đây là một vài thực tiễn bạn có thể áp dụng để ngăn chặn. 

Quản lý tài nguyên hiệu quả

Hãy thử dùng các ngôn ngữ như Java hoặc Python vốn tự động xử lý bộ nhớ bằng bộ gom rác. Còn nếu dùng C hoặc C++, hãy đảm bảo mọi vùng nhớ đã cấp phát đều được xóa rõ ràng. 

Ngoài ra, sử dụng các cấu trúc như khối finally hoặc context manager như with trong Python hay try-with-resources trong Java để dọn dẹp tài nguyên hiệu quả. Những thực tiễn này giúp ngăn rò rỉ bộ nhớ và đảm bảo quản lý tài nguyên đúng cách. 

Dùng weak reference

Khác với strong reference, weak reference không ngăn đối tượng bị gom rác. Dù chúng giữ tham chiếu đến đối tượng, chúng không tính vào khả năng tiếp cận của đối tượng. Do đó, bộ gom rác có thể thu hồi bộ nhớ gắn với đối tượng đó. Vì vậy, hãy dùng weak reference thay vì strong reference. 

Rà soát mã thường xuyên

Điều quan trọng là tiến hành rà soát mã thường xuyên để phát hiện rò rỉ bộ nhớ. Khi xem xét mã, hãy tìm các mẫu phổ biến như tài nguyên không được đóng, tham chiếu vòng, biến toàn cục hoặc tĩnh, và bộ đệm (cache) kém hiệu quả. Bằng cách này, bạn sẽ nhanh chóng tìm ra vấn đề trước khi nó ảnh hưởng đến hệ thống. 

Kết luận

Trong bài viết này, chúng ta đã tìm hiểu các khía cạnh cốt lõi của rò rỉ bộ nhớ, từ nguyên nhân và ví dụ đến kỹ thuật phát hiện. Chúng ta đã thấy các kịch bản khác nhau có thể dẫn đến rò rỉ bộ nhớ và cách bạn xử lý chúng. Ngoài ra, chúng ta đã bàn về các thực tiễn tốt nhất nên áp dụng để ngăn rò rỉ bộ nhớ trong tương lai.  

Vì chúng ta đã khám phá rò rỉ bộ nhớ trong nhiều ngôn ngữ lập trình, tôi khuyến nghị bạn tìm hiểu các khóa học sau để đào sâu kiến thức:

FAQs

Dấu hiệu phổ biến của rò rỉ bộ nhớ là gì?

Một số triệu chứng phổ biến cho thấy rò rỉ bộ nhớ đang ảnh hưởng đến hệ thống của bạn: hệ thống chậm lại, trình duyệt chậm, ứng dụng hoặc hệ điều hành bị treo. 

Ngôn ngữ nào dễ bị rò rỉ bộ nhớ hơn?

Các ngôn ngữ như C và C++ dễ bị rò rỉ bộ nhớ vì chúng không có bộ gom rác tự động quản lý bộ nhớ. Thay vào đó, bạn phải tự cấp phát và giải phóng bộ nhớ.

Rò rỉ bộ nhớ có thể xảy ra trong các ngôn ngữ lập trình hiện đại không?

Có! Bộ gom rác tự động trong các ngôn ngữ lập trình hiện đại không thể xử lý các tình huống như đăng ký listener sự kiện không đúng cách và biến tĩnh hoặc toàn cục.

Làm thế nào để khắc phục rò rỉ bộ nhớ?

Để khắc phục rò rỉ bộ nhớ, hãy làm theo các bước sau:

  • Xác định chỗ rò rỉ bằng công cụ profiling.
  • Phân tích nguyên nhân, như file handle không đóng hoặc tham chiếu còn sót lại.
  • Giải phóng bộ nhớ một cách tường minh trong các ngôn ngữ như C/C++ hoặc quản lý tham chiếu đúng cách trong các ngôn ngữ có gom rác.
  • Tối ưu quản lý bộ nhớ bằng các thực tiễn như weak reference và xử lý vòng đời đối tượng đúng cách.
Srujana Maddula

Tác giả

Srujana Maddula

Srujana là một cây bút công nghệ tự do với bằng Cử nhân Khoa học Máy tính. Việc viết về nhiều chủ đề như khoa học dữ liệu, điện toán đám mây, phát triển, lập trình, bảo mật và nhiều lĩnh vực khác đến với cô một cách tự nhiên. Cô yêu thích văn học kinh điển và khám phá những điểm đến mới.

Chủ đề

Liên quan