Bài viết này trong tiếng Việt:

# Java - Bộ nhớ Rác

Chào bạn, những nhà thuật toán Java tương lai! Hôm nay, chúng ta sẽ bắt đầu cuộc hành trình thú vị vào thế giới của Bộ nhớ Rác Java. Đừng lo nếu bạn mới bắt đầu học lập trình – tôi sẽ là người hướng dẫn bạn, và chúng ta sẽ khám phá chủ đề này bước به bước. Vậy, hãy lấy ra cây sọ sạch và để's làm sạch một chút bộ nhớ!

Java - Garbage Collection

Giới thiệu về Bộ nhớ Rác Java

Hãy tưởng tượng bạn đang ở một buổi tiệc không kết thúc ( có vẻ vui chứ?). Khi mọi người thưởng thức, họ để lại nhiều chén đạn và đĩa trống khắp nơi. Ở một buổi tiệc bình thường, bạn sẽ phải dọn dẹp thủ công. Nhưng điều gì nếu có một hệ thống dọn dẹp thần kỳ tự động loại bỏ rác mà bạn không thể nhận ra? Đó chính là điều gì Bộ nhớ Rác Java làm cho các chương trình của bạn!

Trong lập trình, Bộ nhớ Rác (GC) là một hệ thống quản lý bộ nhớ tự động. Nó nhận diện các đối tượng trong chương trình Java mà không còn cần thiết và loại bỏ chúng để giải phóng bộ nhớ. Quá trình này diễn ra trong nền, cho phép các nhà phát triển tập trung vào việc viết mã thay vì quản lý bộ nhớ thủ công.

Hãy xem một ví dụ đơn giản:

public class GarbageCollectionExample {
    public static void main(String[] args) {
        // Tạo một đối tượng mới
        String name = new String("John Doe");

        // Biến 'name' giờ trỏ đến null
        name = null;

        // Tại thời điểm này, đối tượng chuỗi ban đầu "John Doe" 
        // đủ điều kiện để bị thu hồi bộ nhớ

        // Đề xuất chạy bộ nhớ rác (không đảm bảo)
        System.gc();
    }
}

Trong ví dụ này, chúng ta tạo một đối tượng String "John Doe" và gán nó cho biến 'name'. Khi chúng ta đặt 'name' thành null, đối tượng String ban đầu trở thành không thể tiếp cận. Bộ thu hồi Rác sẽ sớm dọn dẹp đối tượng này, giải phóng bộ nhớ mà nó chiếm.

Các loại Bộ nhớ Rác

Java cung cấp một số loại bộ nhớ rác khác nhau, mỗi loại có ưu điểm riêng. Nó như có nhiều đội dọn dẹp khác nhau cho các tình huống khác nhau. Hãy gặp gỡ các đội dọn dẹp của chúng ta:

  1. Bộ nhớ Rác Serial
  2. Bộ nhớ Rác Parallel
  3. Bộ nhớ Rác Concurrent Mark Sweep (CMS)
  4. Bộ nhớ Rác G1

Dưới đây là bảng tóm tắt các bộ nhớ rác này:

Bộ nhớ Rác Tốt nhất cho Tính năng chính
Serial GC Môi trường đơn luồng, dữ liệu nhỏ Đơn giản và hiệu quả cho các ứng dụng nhỏ
Parallel GC Môi trường đa luồng, dữ liệu lớn Tập trung vào khối lượng xử lý
CMS GC Ứng dụng yêu cầu thời gian tạm dừng thấp Hoạt động song song để giảm thiểu thời gian tạm dừng
G1 GC Kích thước heap lớn, thời gian tạm dừng dễ dàng đo lường Chia heap thành các khu vực để thu hồi hiệu quả
Các thế hệ trong Bộ nhớ Rác

Bây giờ, hãy nói về cách Java tổ chức các đối tượng cho bộ nhớ rác. Java sử dụng mô hình bộ nhớ rác theo thế hệ, dựa trên quan sát rằng hầu hết các đối tượng có thời gian sống ngắn.

Heap (nơi các đối tượng sống) được chia thành ba thế hệ:

  1. Thế hệ Trẻ (Young Generation)
  2. Thế hệ Cũ (Old Generation)
  3. Thế hệ Vĩnh viễn (Permanent Generation - thay thế bởi Metaspace trong Java 8+)

Dưới đây là cách nghĩ về nó một cách thú vị: Tưởng tượng một thành phố có ba khu phố - Youngstown, Oldville và Permanentburg.

Thế hệ Trẻ (Youngstown)

Đây là nơi các đối tượng mới được tạo ra. Nó là một khu phố rộng lớn và động với tỷ lệ chuyển đổi cao. Hầu hết các đối tượng sống và chết ở đây mà không bao giờ di chuyển đến các khu vực khác.

Thế hệ Trẻ được chia thành ba không gian:

  • Eden Space: Nơi các đối tượng mới được phân bổ.
  • Survivor Space 0 và Survivor Space 1: Nơi các đối tượng sống sót qua một lần thu hồi bộ nhớ được di chuyển.

Hãy xem một ví dụ về việc tạo đối tượng trong Thế hệ Trẻ:

public class YoungGenerationExample {
    public static void main(String[] args) {
        for (int i = 0; i < 1000; i++) {
            // Các đối tượng này được tạo trong Eden space
            Object obj = new Object();
        }
        // Hầu hết các đối tượng này sẽ được thu hồi trong lần Minor GC tiếp theo
    }
}

Trong ví dụ này, chúng ta tạo 1000 đối tượng. Những đối tượng này ban đầu được phân bổ trong Eden space của Thế hệ Trẻ.

Thế hệ Cũ (Oldville)

Các đối tượng sống sót qua nhiều lần thu hồi bộ nhớ trong Thế hệ Trẻ được thăng hạng vào Thế hệ Cũ. Nó như một cộng đồng hưu trí cho các đối tượng có thời gian sống dài.

Dưới đây là một ví dụ về đối tượng có thể kết thúc ở Thế hệ Cũ:

public class OldGenerationExample {
    private static final ArrayList<String> longLivedList = new ArrayList<>();

    public static void main(String[] args) {
        for (int i = 0; i < 100000; i++) {
            longLivedList.add("Item " + i);
            // Danh sách này có thể được thăng hạng vào Thế hệ Cũ
            // khi nó phát triển và sống sót qua nhiều vòng lặp GC
        }
    }
}

Trong trường hợp này, longLivedList có thể được thăng hạng vào Thế hệ Cũ khi nó phát triển và sống sót qua nhiều vòng lặp thu hồi bộ nhớ.

Thu hồi Bộ nhớ Minor

Thu hồi Bộ nhớ Minor giống như việc dọn dẹp nhanh chóng Youngstown. Nó nhanh và diễn ra thường xuyên. Khi Thế hệ Trẻ đầy, Minor GC bắt đầu dọn dẹp nó.

Dưới đây là những gì xảy ra trong lần Minor GC:

  1. Tất cả các đối tượng trong Eden đều được di chuyển đến một trong hai Survivor space.
  2. Các đối tượng từ Survivor space hiện tại được di chuyển đến Survivor space còn lại.
  3. Các đối tượng sống sót qua nhiều lần Minor GC được thăng hạng vào Thế hệ Cũ.
Thu hồi Bộ nhớ Full

Thu hồi Bộ nhớ Full là việc dọn dẹp một cách kỹ lưỡng bao gồm cả Youngstown và Oldville. Nó chậm hơn và diễn ra ít thường xuyên hơn so với Minor GC.

Một lần Full GC được kích hoạt khi:

  • Thế hệ Cũ đầy
  • Metaspace đầy

Dưới đây là một ví dụ có thể kích hoạt Full GC:

public class FullGCExample {
    public static void main(String[] args) {
        List<byte[]> list = new ArrayList<>();
        while (true) {
            // Liên tục phân bổ bộ nhớ
            byte[] b = new byte[1024 * 1024]; // 1MB
            list.add(b);
        }
    }
}

Chương trình này liên tục phân bổ bộ nhớ mà không giải phóng. Cuối cùng, nó sẽ đầy Thế hệ Cũ và kích hoạt Full GC.

Điều chỉnh Bộ nhớ Rác

Điều chỉnh bộ nhớ rác như điều chỉnh lịch dọn dẹp của bạn. Đây là một chủ đề nâng cao, nhưng dưới đây là một số mẹo cơ bản:

  1. Chọn bộ nhớ rác phù hợp cho ứng dụng của bạn
  2. Điều chỉnh kích thước heap và các thế hệ
  3. Đặt các thông số ghi nhật ký GC phù hợp

Dưới đây là ví dụ về cách đặt bộ nhớ rác và kích thước heap khi chạy ứng dụng Java:

java -XX:+UseG1GC -Xmx4g -Xms4g MyApplication

Lệnh này sử dụng Bộ nhớ Rác G1 và đặt cả kích thước heap tối đa và ban đầu là 4GB.

Nhớ rằng, điều chỉnh bộ nhớ rác phụ thuộc vào ứng dụng và môi trường cụ thể của bạn. Nó thường là một quá trình thử nghiệm và giám sát.

Và đó là tất cả, các bạn nhà thuật toán tương lai! Chúng ta đã đi qua một chuyến du lịch nhẹ nhàng qua thế giới của Bộ nhớ Rác Java. Từ hiểu rõ điều gì là Bộ nhớ Rác, đến khám phá các loại bộ nhớ rác và thế hệ, đến việc hiểu cách Minor và Full GC hoạt động, bạn đã có kiến thức cơ bản về một khía cạnh quan trọng của lập trình Java.

Nhớ rằng, Bộ nhớ Rác như một đội dọn dẹp tận tụy làm việc trong nền của các chương trình Java của bạn. Trong khi nó đang làm việc, bạn có thể tập trung vào việc viết mã tuyệt vời. Chúc mừng mã lạnh sạch và luôn được giữ đẹp!

Credits: Image by storyset