Java: Bộ dọn rác (Garbage Collector)

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Quản lý bộ nhớ là quá trình xác định các đối tượng trong bộ nhớ mà không còn được sử dụng đến nữa rồi tiến hành thu hồi vùng nhớ đó để sử dụng cho những công việc tiếp theo. Trong một số ngôn ngữ lập trình (ví dụ như ngôn ngữ C) thì việc quản lý vùng nhớ là trách nhiệm của lập trình viên. Tuy nhiên, sự phức tạp của việc quản lý vùng nhớ sẽ dẫn đến phát sinh một số lỗi phổ biến dẫn đến những hành vi bất ngờ hoặc bị treo. Và ta sẽ phải bỏ thời gian để gỡ cũng như sửa lỗi.

Một vấn đề lớn có thể xảy ra trong một chương trình quản lý bộ nhớ tường minh đó là treo tham chiếu. Điều này có thể khiến các không gian được phân bổ cho một số đối tượng mặc dù vẫn được thu hồi nhưng một số đối tượng khác vẫn đang tham chiếu đến nó. Nếu đối tượng 'đang lở lửng' với tham chiếu mà cố gắng truy cập vào các đối tượng ban đầu thì kết quả sẽ là không thể đoán trước được vùng nhớ phân bổ lại cho một đối tượng mới sẽ là như thế nào.

Một vấn đề thường gặp khác trong việc quản lý vùng nhớ tường minh đó là rò rỉ (leak) vùng nhớ. Hiện tượng này xảy ra khi vùng nhớ được cấp phát cho một đối tượng không còn được tham chiếu đến nữa, nhưng lại không được giải phóng. Ví dụ, nếu trong khi mong muốn giải phóng vùng nhớ của một danh sách các liên kết, thì lập trình viên đơn giản là giải phóng vùng nhớ của phần tử đầu tiên trong danh sách. Trong trường hợp này thì những phần tử còn lại trong danh sách sẽ không được tham chiếu đến nữa. Ngoài ra, chúng vượt khỏi tầm kiểm soát của chương trình và dẫn đến có thể không được sử dụng cũng nhưng cũng không bị thu hồi. Nếu lỗ hổng này tiếp tục tồn tại, thì chúng có thể gây tiêu hao bộ nhớ cho đến khi tất cả các bộ nhớ không còn sẵn nữa.

Một giải pháp thay thế để quản lý vùng nhớ tường minh được nhiều ngôn ngữ lập trình hướng đối tượng áp dụng là tự động quản lý vùng nhớ bởi một chương trình gọi là Bộ dọn rác (hay Trình thu dọn rác). Bộ dọn rác sẽ giúp tránh được vấn đề treo tham chiếu bởi vì một đối tượng vẫn được tham chiếu tới đâu đó sẽ không bao giờ được dọn rác và sẽ không được xem xét để giải phóng vùng nhớ của nó. Bộ dọn rác cũng giải quyết vấn đề rò rỉ bởi vì nó tự động giải phóng tất cả các vùng nhớ mà không còn được tham chiếu đến nữa.

Như vậy, một bộ rọn rác có trách nhiệm:

- Một: Cấp phát bộ nhớ.

- Hai: Đảm bảo rằng bất kỳ đối tượng nào có các tham chiếu thì cần phải được duy trì trong bộ nhớ.

- Ba: Lấy lại bộ nhớ được sử dụng bởi các đối tượng mà không còn tham chiếu.

Đối tượng nào còn được tham chiếu thì được coi là 'còn sống', trong khi các đối tượng không còn được tham chiếu đến nữa thì được coi là 'đã chết' và được coi là rác. Quá trình xác định và lấy lại không gian nhớ của các đối tượng này được gọi là thu dọn rác.

Bộ dọn rác có thể giải quyết nhiều, nhưng không phải là tất cả, các vấn đề về cấp phát vùng nhớ. Nó cũng là một quá trình phức tạp, mất thời gian và nguồn lực của riêng nó. Các thuật toán chính xác được sử dụng cho việc tổ chức bộ nhớ và cấp phát/thu gom bộ nhớ đã cấp phát được xử lý bởi bộ rọn rác và trong suốt so với các lập trình viên. Không gian nhớ được phân bổ chủ yếu từ một vùng nhớ lớn của bộ nhớ và được gọi là Heap.

Một đối tượng sẽ đủ điều kiện để trở thành rác nếu nó không thể truy cập từ bất kỳ tham chiếu trực tiếp nào hoặc từ bất kỳ tham chiếu tĩnh nào. Nói cách khác, nó sẽ trở thành rác nếu tất cả tham chiếu của nó là null. Cần lưu ý rằng, việc phụ thuộc chu kỳ không được coi là tham chiếu. Do đó, nếu đối tượng A tham chiếu tới một đối tượng B và B cũng tham chiếu tới A, nhưng chúng không có bất kỳ tham chiếu trực tiếp nào khác, thì trong trường hợp này, cả hai đối tượng A và B sẽ có đủ điều kiện để trở thành rác.

Vì vậy, một đối tượng sẽ trở thành rác nếu nó rơi vào các trường hợp sau:

- Tất cả các tham chiếu của đối tượng đều được đặt là null, vì dụ như object = null.

- Một đối tượng được tạo bên trong một khối và tham chiếu của nó nằm ngoài phạm vi mỗi khi quyền kiểm soát được chuyển khỏi khối đó.

- Đối tượng cha là một tập null. Nếu một đối tượng có tham chiếu tới một đối tượng khác và khi bộ chứa tham chiếu của đối tượng được đăt là null, thì đối tượng con sẽ tự động trở thành rác.

Thời gian thu dọn rác được quết định bởi bộ dọn rác. Thông thường thì toàn bộ Heap hoặc một phần của nó được thu dọn khi nó đã được lấp đầy hoặc nó đạt đến một ngưỡng nào đó.

Dưới đây là một vài đặc điểm của bộ dọn rác:

· Phải an toàn và toàn diện.

· Cần đảm bảo rằng phải luôn phân biệt được những dữ liệu đang được sử dụng với những dữ liệu khác và rác phải được chấp nhận trong hơn một số lượng nhỏ của chu kỳ thu dọn.

· Phải hoạt động hiệu quả, mà không cần dùng biện pháp tạm dừng trong thời gian dài dẫn đến các ứng dụng không chạy. Tuy nhiên, thường có sự đánh đổi giữa thời gian, không gian nhớ và tần suất. Ví dụ, đối với một kích thước Heap nhỏ thì việc dọn rác sẽ được nhanh chóng nhưng Heap cũng sẽ được điền đầy dữ liệu một cách nhanh chóng hơn, do đó yêu cầu việc dọn rác cần thường xuyên hơn. Ngược lại, một Heap lớn sẽ mất nhiều thời gian với việc điền đầy và do đó tần xuất dọn rác sẽ giảm đi, nhưng chúng có thể mất nhiều thời gian.

· Nên hạn chế sự phân mảnh bộ nhớ khi bộ nhớ cho đối tượng rác được giải phóng. Điều này là bởi vì nếu bộ nhớ được giải phóng có kích thước nhỏ, thì nó có thể không đủ để phân bổ cho một đối tượng có kích thước lớn.

· Cũng nên xử lý các vấn đề về khả năng mở rộng ứng dụng đa luồng trên hệ thống chức năng.

Dưới đây là một số thông số cần phải được tính đến khi thiết kế hoặc lựa chọn một thuật toán thu gom rác:

- Nối tiếp so với Song song

Với việc thu dọn nối tiếp, chỉ có một điều sẽ xảy ra tại một thời điểm. Ví dụ, ngay cả khi có nhiều CPU thì chỉ có một sẽ được sử dụng để thực hiện việc thu dọn. Trong khi đó, với việc thu dọn song song, công tác thu gom rác được chia thành các phần nhỏ và được thực hiện đồng thời trên các CPU khác nhau. Việc thu gom đồng thời thì sẽ nhanh chóng hơn nhưng dẫn đến phức tạp hơn và có thể gây ra hiện tượng phân mảnh.

- Đồng thời so với Stop-the-world

Trong cách tiếp cận thu dọn rác Stop-the-world, trong thời gian thu gom rác, việc thực hiện ứng dụng là hoàn toàn bị ngưng trệ. Trong khi đó, trong cách tiếp cận Đồng thời, một hoặc nhiều nhiệm vụ thu dọn rác có thể được thực hiện đồng thời, tức là cùng lúc với việc thực hiện các ứng dụng. Tuy nhiên, điều này phát sinh một số chi phí trên bộ thu dọn Đồng thời và ảnh hướng đến hiệu quả do kích thước yêu cầu đối với Heap phải lớn.

- Nén so với Không nén và so với Sao chép

Với phương pháp Nén, mỗi khi bộ dọn rác xác nhận các đối tượng trong bộ nhớ là rác thì nó có thể nén bộ nhớ bằng cách di chuyển tất cả các đối tượng đang được sử dụng lại với nhau và lấy lại bộ nhớ của những đối tượng không còn được tham chiếu đến nữa. Sau khi nén thì việc cấp phát vùng nhớ cho đối tượng trở nên dễ dàng và nhanh chóng hơn tại vùng nhớ trống đầu tiên. Điều này có thể được thực hiện bằng cách sử dụng một con trỏ đơn để lưu lại vùng nhớ hợp lệ tiếp theo cho việc cấp phát đối tượng.

Ngược lại, bộ dọn Không nén tiến hành giải phóng không gian nhớ đã sử dụng bởi đối tượng rác tại chỗ. Cụ thể là, nó không di chuyển các đối tượng còn đang được dùng lại với nhau để giải phóng vùng nhớ lớn giống như bộ dọn Nén làm. Ưu điểm của cách thức này là việc thu dọn rác nhanh hơn, nhưng nhược điểm là tạo nguy cơ phân mảnh vùng nhớ.

Nói chung, việc cấp phát vùng nhớ bằng biện pháp giải phóng vùng nhớ tại chỗ trở nên tốn kém hơn so với phương pháp Nén. Đó là bởi vì cần phải tìm trong toàn bộ Heap một vùng nhớ tiếp giáp đủ lớn để chứa đối tượng mới.

Còn đối với phương pháp thu dọn Sao chép, bộ dọn có thể sao chép  hoặc di chuyển các đối tượng đang được sử dụng tới một vùng nhớ khác. Ưu điểm của phương pháp này là vùng nhớ nguồn sẽ được giải phóng và có thể được sử dụng để cấp phát sau đó nhanh và dễ dàng hơn. Tuy nhiên, nhược điểm là tốn thêm thời gian cho việc sao chép cũng như cần thêm vùng nhớ cho việc di chuyển các đối tượng.

Các phép đo sau đây có thể được sử dụng để đánh giá hiệu suất của bộ thu dọn rác:

- Thông lượng: Là tỷ lệ phần trăm của tổng thời gian không dùng đến khi thu dọn rác, xét trong một thời gian dài.

- Thu dọn rác overhead: Là nghịch đảo của thông lượng. Đó là tỷ lệ phần trăm của tổng thời gian dành cho việc dọn rác.

- Thời gian dừng: Đó là lượng thời gian trong đó việc thực thi ứng dụng bị treo trong khi thu dọn rác.

- Tần suất thu dọn: Đây là thước đo mức độ thường xuyên thu dọn xảy ra liên quan đến việc thực thi ứng dụng.

- Footprint: Đây là một phép đo kích thước, chẳng hạn như kích thước của Heap.

- Promptness: Là khoảng thời gian giữa thời gian một đối tượng trở thành rác và thời gian khi vùng nhớ được giải phóng.

Một phương thức quan trọng dùng để dọn rác đó là phương thức finalize().

Phương thức finalize()

Phương thức này được gọi bởi bộ dọn rác trên đối tượng khi nó được xác định sẽ không có thêm tham chiếu trỏ tới nó. Một lớp con ghi đè phương thức finalize() để thực hiện tài nguyên hệ thống hoặc thực hiện việc thu dọn khác.

Cú pháp của phương thức finalize() là như sau:

protected void finalize() throws Throwable

Mục đích thông thường của phướng thức finalize() là để thực hiện các hành động dọn dẹp trước khi đối tượng bị loại bỏ hoàn toàn. Tuy nhiên, phương thức này có thể thực hiện thêm bất kỳ hành động nào, bao gồm cả việc khôi phục lại đối tượng hiện thời lẫn các hoạt động khác. Ví dụ, finalize() của một đối tượng thể hiện một kết nối vào/ra có thể thực hiện các giao dịch I/O một cách minh bạch cho việc hủy kết nối trước khi đối tượng bị loại bỏ hoàn toàn. Riêng phương thức finalize() của lớp Object không thực hiện bất kỳ hoạt động đặc biệt nào mà đơn thuần chỉ là trả về giá trị một cách thông thường. Tuy nhiên, các lớp con của lớp Object lại có thể ghi đè định nghĩa này theo yêu cầu.

Sau khi phương thức finalize() được gọi cho một đối tượng, thì không có thêm hành động nào được thực hiện cho đến khi JVM xác định rằng không có luồng nào đang cố gắng truy cập vào đối tượng tương ứng và sau đó, đối tượng có thể bị hủy. Phương thức finalize() không bao giờ được gọi nhiều hơn một lần bởi JVM cho bất kỳ một đối tượng nào. Ngoài ra, bất kỳ một ngoại lệ nào được ném ra bởi fianlize() đều dẫn đến sự treo của đối tượng này. finalize() ném ra ngoại lệ Throwable.

Đôi khi, một đối tượng có thể cần phải giải quyết một số công việc khi nó bị hủy. Ví dụ như nếu một đối tượng đang mang một số tài nguyên không phải Java như là một tập tin xử lý hoặc một chữ ký số. Trong trường hợp này, đối tượng có thể muốn đảm bảo những tài nguyên này được giải phóng trước khi nó bị hủy bỏ. Để xử lý điều này thì ta có thể định nghĩa các hành động đặc biệt trong phương thức finalize(). Khi JVM gọi đến nó trong khi xóa một đối tượng của lớp đó, thì các hành động đặc biệt trong các phương thức finalize() sẽ được giải quyết trước khi đối tượng được hủy bỏ.

Tuy nhiên, cần lưu ý một điều quan trọng rằng phương thức finalize() chỉ có thể được gọi trước khi tiến hành dọn rác nhưng không phải là khi một đối tượng ra khỏi phạm vi của nó. Điều này có nghĩa rằng ta không biết một cách chính xác là khi nào phương thức finalize() được thực hiện. Do đó, bạn nên cung cấp các phương tiện giải phóng tài nguyên hệ thống được sử dụng bởi đối tượng thay vì phụ thuộc vào phương thức finalize().

Để hiểu được khả năng tự động thu dọn của Bộ dọn rác, ta xét ví dụ sau đây:

class TestBoDonRac {
  int so1;
  int so2;

  public void setSo(int so1, int so2) {
    this.so1 = so1;
    this.so2 = so2;
  }

  public void inSo() {
    System.out.println("S th nht: " + so1);
    System.out.println("S th hai: " + so2);
  }

  public static void main(String args[]) {
    TestBoDonRac objTest1 = new TestBoDonRac();
    TestBoDonRac objTest2 = new TestBoDonRac();
    objTest1.setSo(6, 7);
    objTest2.setSo(8, 9);
    objTest1.inSo();
    objTest2.inSo();
    //TestBoDonRac objTest3; // dòng 1
    //objTest3 = objTest2; // dòng 2
    //TestBoDonRac.inSo(); // dòng 3
    //objTest2 = null; // dòng 4
    //objTest3.inSo(); // dòng 5
    //objTest3 = null; // dòng 6
    //objTest3.inSo(); // dòng 7
  }
}

Đoạn mã cho thấy lớp có tên TestBoDonRac gồm hai biến được khởi tạo trong phương thức setSo() và được hiển thị sử dụng phương thức inSo(). Tiếp đến, hai đối tượng objTest1 và objTest2 của lớp TestBoDonRac được tạo. Bây giờ, để hiểu được việc thu dọn rác thì ta thực thi đoạn mã trên.

Hình sau biểu diễn bộ nhớ trong của các đối tượng được tạo khi thực thi.

Memory Heap

Bây giờ, nếu bỏ chú thích ở các câu lệnh tại các dòng 1, 2 và 3 đi và chạy lại chương trình thì hai biến tham chiếu sẽ trỏ tới cùng một đối tượng như hình dưới đây.

Trỏ tới cùng 1 đối tượng

Tiếp theo, khi bỏ chú thích ở dòng 4 và 5 đi và thực hiện lại chương trình, thì objTest2 trở thành null, nhưng obj3 vẫn trỏ tới đối tượng như hình dưới. Do đó, đối tượng vẫn chưa hội đủ điều kiện để trở thành rác.

Trỏ tới null

Bây giờ, ta bỏ chú thích dòng 6 và 7 đi và chạy lại chương trình thì objTest2 cũng trở thành null. Lúc này thì không còn tham chiếu nào trỏ tới đối tượng nữa và vì vậy, nó đủ điều kiện để trở thành rác. Nó sẽ bị hủy khỏi bộ nhớ và không thể được truy xuất lại như hình dưới.

Truy xuất lại

Như vậy, ta có một số điều quan trọng về việc dọn rác trong Java như sau:

- Để biến một đối tượng thành rác, thì đặt biến tham chiếu của nó là null.

- Cần lưu ý rằng, các kiểu nguyên thủy không phải là các đối tượng. Vì thế, chúng không thể được gán là null, ví dụ như int x = null; là sai.

» Tiếp: Các lớp Wrapper
« Trước: Lớp Object
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!