Sửa lỗi java.lang.OutOfMemoryError: Metaspace trong ứng dụng Java

intermediate Java2026-04-08| Java 8+, JVM (HotSpot), Linux/macOS/Windows, Spring Boot, Tomcat, mọi ứng dụng Java chạy lâu dài

Error Message

java.lang.OutOfMemoryError: Metaspace
#java#jvm#metaspace#bộ nhớ#classloader

Lỗi

Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:756)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)

Lỗi này hiếm khi xuất hiện lúc khởi động. Thông thường, ứng dụng chạy bình thường nhiều giờ — đôi khi vài ngày — rồi mới crash. Bạn cũng có thể bắt gặp nó ẩn bên trong một WrappedException từ Hibernate hoặc Spring, khiến nó dễ bị bỏ qua.

Metaspace Thực Sự Là Gì

Java 8 đã thay thế PermGen bằng Metaspace. Metaspace lưu trữ class metadata — biểu diễn nội bộ của JVM cho mọi class đã được nạp. Khác với heap, nó nằm trong bộ nhớ native. Mặc định không có giới hạn, nhưng vẫn có thể cạn kiệt bộ nhớ khả dụng nếu hệ thống chịu tải hoặc nếu bạn đã đặt -XX:MaxMetaspaceSize.

Thường có hai nguyên nhân:

  • Giới hạn Metaspace quá nhỏ — bạn đặt -XX:MaxMetaspaceSize thấp hơn mức thực tế mà ứng dụng cần để chứa các class.
  • Rò rỉ Classloader — các class liên tục được nạp nhưng không bao giờ được giải phóng. Dynamic proxy, scripting engine và code hot-reload là những thủ phạm thường gặp.

Bước 1 — Kiểm Tra Mức Sử Dụng Metaspace Hiện Tại

Trước khi chỉnh sửa bất kỳ flag nào, hãy lấy số liệu cơ sở:

# Trong khi ứng dụng đang chạy:
jcmd <PID> VM.native_memory summary

# Hoặc qua jstat (số class đã nạp):
jstat -class <PID> 1000 10

# Snapshot nhanh theo từng classloader:
jmap -clstats <PID> | sort -k3 -rn | head -20

Trong kết quả jcmd, tìm phần Metaspace. Nếu committed gần bằng MaxMetaspaceSize, bạn chỉ cần tăng giới hạn lên. Nhưng nếu jstat cho thấy số lượng class tăng liên tục không dừng — kể cả trong thời gian nhàn rỗi — đó là rò rỉ, không phải vấn đề cấu hình.

Bước 2 — Tăng Giới Hạn Metaspace (Sửa Nhanh)

-XX:MaxMetaspaceSize đặt quá thấp là trường hợp đơn giản nhất. Tăng nó lên:

# Thêm hoặc điều chỉnh trong JVM args:
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

MetaspaceSize đặt ngưỡng ban đầu — đặt cao hơn để tránh GC không cần thiết lúc khởi động. MaxMetaspaceSize là giới hạn cứng. Với hầu hết ứng dụng Spring Boot, 256–512m là đủ. Ứng dụng với kiến trúc plugin nặng hoặc sinh class động (như Groovy nhiều hoặc OSGi) có thể cần 768m hoặc hơn.

Trong application.properties (Spring Boot qua Maven plugin):

spring-boot.run.jvmArguments=-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m

Trong Dockerfile:

ENV JAVA_OPTS="-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"
CMD java $JAVA_OPTS -jar app.jar

Trong catalina.sh của Tomcat:

export JAVA_OPTS="$JAVA_OPTS -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"

Bước 3 — Chẩn Đoán Rò Rỉ Classloader

Tăng giới hạn đã giúp ích, nhưng Metaspace vẫn tiếp tục tăng? Bạn đang bị rò rỉ. Phân tích heap dump là cách đáng tin cậy nhất để tìm ra nguyên nhân.

# Kích hoạt heap dump:
jmap -dump:format=b,file=heap.hprof <PID>

# Hoặc cấu hình JVM tự động dump khi OOM:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/oom.hprof

Mở dump bằng Eclipse MAT hoặc VisualVM. Vào Class Loader Explorer. Thấy 500 instance của cùng một classloader cho một class duy nhất? Đó chính là điểm rò rỉ.

Các thủ phạm thường gặp:

  • Thư viện tạo mới ClassLoader cho mỗi request (một số XML parser, scripting engine)
  • Cache script Groovy/BeanShell/Velocity không có giới hạn kích thước
  • Spring DevTools hot-reload đang chạy trên production (không nên đưa vào production)
  • CGLIB hoặc Javassist sinh proxy class trong vòng lặp mà không cache lại
  • JDBC driver được đăng ký khi deploy nhưng không được hủy đăng ký khi undeploy

Bước 4 — Khắc Phục Rò Rỉ

CGLIB / Dynamic Proxy

Mỗi lần gọi Enhancer.create() sẽ tạo ra một class mới. Gọi theo từng request sẽ làm tràn Metaspace. Hãy cache proxy lại:

// XẤU — tạo class mới mỗi lần gọi
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyService.class);
MyService proxy = (MyService) enhancer.create();

// TỐT — cache lại proxy class
private static final MyService PROXY = createProxy();
private static MyService createProxy() {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(MyService.class);
    return (MyService) enhancer.create();
}

Groovy Script Engine

Tạo mới GroovyShell cho mỗi lần đánh giá là lỗi rò rỉ kinh điển. Hãy tái sử dụng một shell và cache các script đã biên dịch:

// XẤU
new GroovyShell().evaluate(script);

// TỐT HƠN — tái sử dụng shell và cache script đã biên dịch
private final GroovyShell shell = new GroovyShell();
private final Map<String, Script> cache = new ConcurrentHashMap<>();

public Object eval(String src) {
    return cache.computeIfAbsent(src, shell::parse).run();
}

JDBC Driver Khi Undeploy Tomcat

Thêm logic dọn dẹp vào ServletContextListener của webapp:

@Override
public void contextDestroyed(ServletContextEvent sce) {
    Enumeration<Driver> drivers = DriverManager.getDrivers();
    while (drivers.hasMoreElements()) {
        Driver driver = drivers.nextElement();
        try {
            DriverManager.deregisterDriver(driver);
        } catch (SQLException e) {
            log.warn("Failed to deregister driver", e);
        }
    }
}

Bước 5 — Xác Nhận Bản Sửa Lỗi

Theo dõi Metaspace theo thời gian sau khi áp dụng thay đổi:

# Theo dõi số lượng class mỗi 5 giây trong 2 phút:
jstat -class <PID> 5000 24

# Hoặc bật GC logging và grep Metaspace:
-Xlog:gc*:file=/tmp/gc.log:time,uptime:filecount=5,filesize=20m
grep -i metaspace /tmp/gc.log | tail -20

Ứng dụng sạch sẽ ổn định sau khi khởi động — số lượng class dừng lại và giữ nguyên. Vẫn thấy 100+ class mới mỗi phút trong quá trình hoạt động bình thường? Rò rỉ vẫn chưa được khắc phục.

Với bản sửa tăng giới hạn: chạy ứng dụng qua nhiều chu kỳ request đầy đủ và xác nhận mức sử dụng Metaspace ổn định, thấp hơn nhiều so với giới hạn mới. Nếu vẫn tiếp tục tăng, bạn đang gặp cả hai vấn đề cùng lúc.

Tóm Tắt Các JVM Flag Hữu Ích

# Đặt Metaspace ban đầu + tối đa
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m

# Dump heap khi OOM để phân tích sau sự cố
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/myapp/oom.hprof

# Bật theo dõi bộ nhớ native (nhẹ)
-XX:NativeMemoryTracking=summary

# Kích hoạt GC tích cực hơn để thu hồi class metadata
-XX:+CMSClassUnloadingEnabled   # Java 8 CMS only
-XX:+ClassUnloadingWithConcurrentMark  # G1/ZGC

Danh Sách Kiểm Tra Nhanh

  • Kiểm tra xem MaxMetaspaceSize có đặt quá thấp không — tăng lên trước
  • Theo dõi số lượng class bằng jstat -class — ổn định = tốt, vẫn tăng = rò rỉ
  • Heap dump + MAT Class Loader Explorer để xác định nguồn rò rỉ
  • Tìm kiếm việc sử dụng sai dynamic proxy hoặc scripting engine
  • Hủy đăng ký JDBC driver khi webapp tắt
  • Không bao giờ chạy Spring DevTools trên production

Related Error Notes