Cách khắc phục lỗi 'java.lang.OutOfMemoryError: GC overhead limit exceeded' trong Java

intermediate Java2026-05-02| Java JDK 8, 11, 17, hoặc 21. Thường thấy trong các ứng dụng Spring Boot thông lượng cao, máy chủ Tomcat và các tiến trình xử lý batch cũ trên Linux hoặc Windows.

Error Message

java.lang.OutOfMemoryError: GC overhead limit exceeded
#java#jvm#memory-leak#garbage-collection#performance-tuning#eclipse-mat

Khi JVM bị "nghẹt" bởi Garbage CollectionGần đây tôi đã gặp phải vấn đề này trong một tác vụ di chuyển dữ liệu lớn. Trong 10 phút đầu, các bản ghi log trông rất ổn. Sau đó, thông lượng giảm mạnh, quạt CPU bắt đầu kêu to và tiến trình cuối cùng bị sập với một thông báo lỗi quen thuộc và gây ức chế:

java.lang.OutOfMemoryError: GC overhead limit exceeded

Đây không phải là lỗi "Heap Space" thông thường. Đó là cách JVM thông báo rằng nó đã "đầu hàng". Lỗi này xảy ra khi trình dọn rác Garbage Collector (GC) làm việc quá tải nhưng hầu như không giải phóng được gì, khiến ứng dụng của bạn bị thiếu hụt tài nguyên trầm trọng.

Quy tắc 98/2Theo mặc định, JVM sẽ kích hoạt lỗi này nếu nó dành hơn 98% thời gian để thực hiện dọn rác trong khi chỉ thu hồi được ít hơn 2% dung lượng heap. Đây là một cơ chế phòng vệ quan trọng. Nếu không có giới hạn này, ứng dụng của bạn sẽ bị treo vô thời hạn trong khi CPU luôn ở mức 100% chỉ để giải phóng một vài byte ít ỏi.

Giải pháp tạm thời: Mở rộng HeapNếu ứng dụng của bạn chỉ đơn giản là đang xử lý một tập dữ liệu lớn hơn bình thường—ví dụ như xử lý tệp XML 2GB khi bạn chỉ cấp phát 1GB—bạn có thể cung cấp thêm không gian cho nó thông qua tham số -Xmx.

# Tăng kích thước heap tối đa lên 4GB
java -Xmx4g -jar high-load-app.jar

Cờ hiệu "Nút khẩn cấp"Về mặt kỹ thuật, bạn có thể tắt kiểm tra an toàn này. Tôi đã từng sử dụng nó một lần để cho phép một quá trình di chuyển kéo dài 5 giờ hoàn thành 10% cuối cùng, nhưng hãy sử dụng nó cực kỳ thận trọng. Việc tắt nó thường dẫn đến việc ứng dụng bị đóng băng hoàn toàn hoặc bị sập với lỗi Java heap space tiêu chuẩn ngay sau đó.

-XX:-UseGCOverheadLimit

Truy tìm nguyên nhân gốc rễThêm bộ nhớ thường chỉ là trì hoãn điều tất yếu. Nếu bạn bị rò rỉ bộ nhớ, heap 16GB cuối cùng cũng sẽ đầy nhanh như heap 2GB. Bạn cần xem điều gì thực sự đang chiếm dụng RAM.

1. Ghi lại sự cố bằng Heap DumpĐừng cố đoán mò. Hãy yêu cầu JVM lưu lại trạng thái của nó ngay khi gặp sự cố để bạn có thể kiểm tra "hiện trường vụ án" sau đó:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./diagnostics/crash_dump.hprof

Nếu ứng dụng hiện đang bị lag nhưng chưa sập, hãy lấy một bản dump trực tiếp bằng ID tiến trình (PID):

# Lấy PID từ lệnh 'jps' trước
jmap -dump:live,format=b,file=debug_dump.hprof [PID]

2. Phân tích bằng chứngTải tệp .hprof đó vào Eclipse Memory Analyzer (MAT) hoặc VisualVM. Chạy báo cáo "Leak Suspects". Theo kinh nghiệm của tôi, thủ phạm thường là một trong bốn trường hợp sau:

  • Abandoned Collections: Một static HashMap được dùng để lưu bộ nhớ đệm (cache) nhưng không bao giờ xóa các mục cũ.- Resource Leaks: Database ResultSets hoặc FileStreams không được đặt trong khối try-with-resources.- Object Churn: Tạo 500.000 đối tượng String trong một vòng lặp thay vì sử dụng StringBuilder.- Missing Pagination: Lấy 200.000 hàng từ cơ sở dữ liệu vào một List<User> thay vì xử lý chúng theo từng đợt (batch) 500 hàng.### 3. Sửa lỗi mã nguồn thực tế: Stream thay vì Load toàn bộĐừng tải toàn bộ tập dữ liệu vào RAM. Nếu bạn đang sử dụng Spring Data JPA, hãy thay thế việc lấy danh sách (list) thông thường bằng một stream. Điều này cho phép GC dọn dẹp các đối tượng đã xử lý trong khi vòng lặp vẫn đang chạy.
// NÊN TRÁNH: Tải tất cả bản ghi vào bộ nhớ cùng lúc
List<Transaction> history = repository.findAll(); 
history.forEach(this::calculateTax);

// TỐT HƠN: Xử lý từng bản ghi một
try (Stream<Transaction> stream = repository.streamAll()) {
    stream.forEach(this::calculateTax);
}

Xác minh: Lỗi đã thực sự được khắc phục chưa?Đừng chỉ cầu may. Hãy sử dụng jstat để theo dõi sức khỏe của GC trong thời gian thực khi ứng dụng đang chạy:

# Theo dõi thống kê GC mỗi 2 giây
jstat -gcutil [PID] 2000

Hãy để mắt đến GCT (Tổng thời gian GC). Nếu nó tăng đều đặn trong khi O (Old Generation) vẫn ở mức 99%, rò rỉ của bạn vẫn còn đó. Một ứng dụng khỏe mạnh sẽ hiển thị mô hình "răng cưa": bộ nhớ tăng lên, GC kích hoạt và mức sử dụng giảm xuống mức cơ sở sạch sẽ.

Related Error Notes