Quản lý và thu hồi bộ nhớ ngoài heap trong JVM

Quản lý và thu hồi bộ nhớ ngoài heap trong JVM

1. Phân bổ và thu gom rác bộ nhớ trong JVM

Bộ nhớ của JVM thường được chia thành các khu vực cơ bản:

Khu vực trẻ (Young Generation): Các đối tượng mới tạo ra thường nằm ở đây.

Khu vực già (Old Generation): Sau vài lần thu gom rác, các đối tượng từ khu vực trẻ sẽ chuyển sang khu vực già và tồn tại lâu hơn.

Khu vực vĩnh cửu (Permanent Generation): Chứa thông tin liên quan đến class, ít khi bị thu gom rác.

Thu gom rác trong JVM

JVM tự động thực hiện việc thu gom rác, do đó lập trình viên không cần lo lắng về việc giải phóng đối tượng. Tuy nhiên, nếu không hiểu rõ quy trình này, dễ dẫn đến rò rỉ bộ nhớ.

Các loại thu gom rác bao gồm:

Thu gom nhỏ (Minor GC): Khi không đủ không gian cho các đối tượng mới, Minor GC sẽ được kích hoạt. Thường sử dụng cơ chế sao chép để tối ưu hóa.

Thu gom lớn (Major GC): Dọn dẹp bộ nhớ trong khu vực già, sử dụng kết hợp đánh dấu-xóa và đánh dấu-sắp xếp.

Thu gom toàn diện (Full GC): Có thể xem như là một phiên bản mở rộng của Major GC hoặc sự kết hợp giữa Minor và Major GC.

2. Tràn bộ nhớ ngoài heap

Từ thời kỳ NIO, có thể sử dụng các lớp như ByteBuffer để thao tác với bộ nhớ ngoài heap. Ví dụ, phân bổ bộ nhớ trực tiếp bằng cách sử dụng ByteBuffer.allocateDirect(10 * 1024 * 1024):

ByteBuffer buffer = ByteBuffer.allocateDirect(numBytes);

Nhiều khung làm cache như Memcached cũng sử dụng bộ nhớ ngoài heap để cải thiện hiệu suất. Đặt giới hạn cho bộ nhớ ngoài heap thông qua tham số JVM:

-XX:MaxDirectMemorySize=512m

Nếu không đủ bộ nhớ ngoài heap, lỗi sau sẽ xuất hiện:

java.lang.OutOfMemoryError: Direct buffer memory

Để kiểm tra tình trạng bộ nhớ trong heap, có thể dùng lệnh jmap -heap:

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC
 
Heap Configuration:
   MinHeapFreeRatio = 40
   MaxHeapFreeRatio = 70
   MaxHeapSize      = 2147483648 (2048.0MB)
   NewSize          = 16777216 (16.0MB)
   MaxNewSize       = 33554432 (32.0MB)
   OldSize          = 50331648 (48.0MB)
   NewRatio         = 7
   SurvivorRatio    = 8
   PermSize         = 16777216 (16.0MB)
   MaxPermSize      = 67108864 (64.0MB)
 
Heap Usage:
...

Lỗi tràn bộ nhớ ngoài heap thường xuất hiện khi phân bổ quá mức:

java.lang.OutOfMemoryError
 at sun.misc.Unsafe.allocateMemory(Native Method)
 at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:101)
 at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
 ...

Để xử lý vấn đề này, có thể chạy lệnh jmap -histo:live để ép kích hoạt Full GC. Nếu bộ nhớ ngoài heap giảm đáng kể, rất có thể nguyên nhân là do bộ nhớ ngoài heap vượt quá giới hạn.

Nếu gặp lỗi khi chạy jmap:

Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process

Cần chỉnh sửa /etc/sysctl.d/10-ptrace.conf:

kernel.yama.ptrace_scope = 0

3. Thu hồi bộ nhớ ngoài heap

3.1 Thu hồi bộ nhớ của ByteBuffer

Sử dụng ByteBuffer.allocateDirect(10 * 1024 * 1024) để phân bổ bộ nhớ ngoài heap. Giống như C, Java cũng cần quản lý cẩn thận để tránh rò rỉ bộ nhớ.

Do bộ nhớ ngoài heap không được JVM kiểm soát trực tiếp, nên chỉ có thể thu hồi khi xảy ra Full GC.

Ví dụ minh họa:

public static void TestDirectByteBuffer() {
    List<ByteBuffer> list = new ArrayList<>();
    while(true) {
        ByteBuffer buffer = ByteBuffer.allocateDirect(1 * 1024 * 1024);
        //list.add(buffer);
    }
}

Trong trường hợp không tắt Full GC, chương trình sẽ chạy ổn định mà không báo lỗi OutOfMemoryError. Tuy nhiên, nếu thêm -XX:+DisableExplicitGC, sẽ xảy ra lỗi tràn bộ nhớ ngoài heap.

3.2 Giải phóng bộ nhớ phân bổ bởi Unsafe

Giải quyết rò rỉ bộ nhớ bằng cách ghi đè phương thức finalize():

import sun.misc.Unsafe;

public class MemoryManager {
    private long address = 0;
    private Unsafe unsafe = GetUnsafeInstance.getUnsafeInstance();
    private byte[] bytes = null;

    public MemoryManager() {
        address = unsafe.allocateMemory(2 * 1024 * 1024);
        bytes = new byte[1024 * 1024];
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("Finalizing. " + bytes.length);
        unsafe.freeMemory(address);
    }

    public static void main(String[] args) {
        while (true) {
            MemoryManager manager = new MemoryManager();
            System.out.println("Memory address=" + manager.address);
        }
    }
}

Việc ghi đè phương thức finalize() giúp giải phóng bộ nhớ ngoài heap khi đối tượng trong heap bị thu gom rác.

Thẻ: JVM Bộ nhớ ngoài heap Thu gom rác ByteBuffer Unsafe

Đăng vào ngày 22 tháng 5 lúc 07:08