Khắc phục lỗi java.lang.OutOfMemoryError: Java heap space

trung bình Java2026-03-16| Bất kỳ hệ điều hành nào (Windows, Linux, macOS) chạy ứng dụng Java, bao gồm các ứng dụng độc lập, máy chủ web (ví dụ: Tomcat, JBoss, Jetty) hoặc môi trường được container hóa (Docker, Kubernetes).

Error Message

java.lang.OutOfMemoryError: Java heap space
#java#bộ nhớ#heap#jvm

java.lang.OutOfMemoryError: Java heap space

Khi bạn gặp lỗi java.lang.OutOfMemoryError: Java heap space, điều đó có nghĩa là Máy ảo Java (JVM) đã hết bộ nhớ trong vùng heap của nó. Đây là nơi các đối tượng và mảng được cấp phát động. Nếu một ứng dụng Java cố gắng tạo một đối tượng mới nhưng không có đủ không gian trống, JVM sẽ đột ngột dừng quá trình bằng cách ném ra ngoại lệ nghiêm trọng này. Điều này thường gây ra sự cố ứng dụng, hiệu suất chậm hoặc các hành vi không mong muốn khác.

Tìm hiểu về vùng nhớ Heap trong Java và các giới hạn của nó

Vùng nhớ heap của Java là nền tảng cho việc quản lý bộ nhớ của JVM. Đây là nơi lưu trữ chính cho tất cả các đối tượng và thể hiện lớp được tạo bởi ứng dụng của bạn, được chia sẻ bởi mọi luồng.

Tổng kích thước của heap được đặt trong quá trình khởi động JVM, được xác định bởi các tham số bộ nhớ ban đầu và tối đa. Bộ thu gom rác đóng vai trò quan trọng bằng cách xác định và loại bỏ các đối tượng không được tham chiếu, từ đó giải phóng không gian. Tuy nhiên, nếu nhu cầu bộ nhớ của ứng dụng vượt quá dung lượng tối đa đã cấu hình của heap, và bộ thu gom rác không thể lấy lại đủ không gian đủ nhanh, lỗi OutOfMemoryError đáng sợ sẽ xuất hiện.

Một số lý do phổ biến dẫn đến lỗi này:

- **Cấu hình Heap không đủ:** Thông thường, JVM khởi động với kích thước heap mặc định (ban đầu và tối đa) quá nhỏ so với nhu cầu bộ nhớ thực tế của ứng dụng của bạn.
- **Rò rỉ bộ nhớ (Memory Leaks):** Vấn đề này tinh tế và khó theo dõi hơn. Nó xảy ra khi các đối tượng không còn cần thiết nhưng vẫn được tham chiếu bởi các phần khác của ứng dụng. Điều này ngăn bộ thu gom rác không thể thu hồi bộ nhớ của chúng. Theo thời gian, các đối tượng 'bị rò rỉ' này tích tụ, từ từ nhưng chắc chắn tiêu tốn toàn bộ heap.
- **Xử lý đối tượng không hiệu quả:** Ứng dụng của bạn có thể tạo quá nhiều đối tượng tạm thời, các đối tượng quá lớn hoặc giữ các đối tượng trong các bộ sưu tập lâu hơn mức cần thiết.
- **Xử lý tập dữ liệu lớn:** Các tác vụ như đọc các tệp lớn, tải kết quả truy vấn cơ sở dữ liệu khổng lồ (ví dụ: hàng triệu hàng) hoặc thao tác với các cấu trúc dữ liệu rất lớn trong bộ nhớ mà không có phân trang hoặc truyền tải phù hợp có thể nhanh chóng làm đầy heap đến giới hạn của nó.

Các bước khắc phục lỗi Java OutOfMemoryError: Java heap space

1. Tăng kích thước Heap của JVM

Đây thường là cách khắc phục nhanh nhất và dễ dàng nhất, đặc biệt nếu ứng dụng của bạn xử lý nhiều dữ liệu hơn hoặc hỗ trợ nhiều người dùng hơn so với kế hoạch ban đầu. Bạn có thể kiểm soát kích thước heap của JVM bằng cách sử dụng các đối số dòng lệnh -Xms (kích thước heap ban đầu) và -Xmx (kích thước heap tối đa).

Đối với các ứng dụng Java độc lập:

java -Xms512m -Xmx1024m -jar YourApplication.jar

- `-Xms512m`: Đặt kích thước heap Java ban đầu là 512 megabyte.
- `-Xmx1024m`: Đặt kích thước heap Java tối đa là 1024 megabyte (1GB).

Điều chỉnh các giá trị này dựa trên nhu cầu bộ nhớ thực tế của ứng dụng và bộ nhớ vật lý có sẵn trên hệ thống của bạn. Một điểm khởi đầu thực tế là đặt -Xms bằng một phần tư hoặc một nửa giá trị -Xmx.

Đối với các máy chủ ứng dụng (ví dụ: Tomcat, JBoss, WildFly):

Các cài đặt heap thường được cấu hình thông qua các biến môi trường hoặc tệp cấu hình. Ví dụ, với Tomcat, bạn thường đặt chúng trong setenv.sh (cho Linux/macOS) hoặc setenv.bat (cho Windows). Các tệp này nằm trong thư mục bin của cài đặt Tomcat của bạn, sử dụng biến CATALINA_OPTS:

# setenv.sh ví dụ cho các JVM cũ hơn (Java 7 trở xuống)
export CATALINA_OPTS="-Xms512m -Xmx1024m -XX:MaxPermSize=256m"

Đối với các JVM gần đây hơn (Java 8 trở lên), PermGen đã được thay thế bằng Metaspace. Metaspace được quản lý động theo mặc định, mặc dù bạn vẫn có thể đặt giới hạn bằng cách sử dụng -XX:MaxMetaspaceSize nếu cần.

# setenv.sh ví dụ cho Java 8 trở lên
export CATALINA_OPTS="-Xms512m -Xmx1024m"

Đối với các máy chủ khác, hãy tham khảo tài liệu của chúng để biết các cài đặt cấu hình bộ nhớ cụ thể hoặc tìm JAVA_OPTS.

2. Phân tích Heap Dumps để tìm rò rỉ bộ nhớ

Nếu việc đơn giản tăng kích thước heap chỉ làm trì hoãn vấn đề, hoặc nếu lỗi xuất hiện trở lại ngay cả khi cấp phát bộ nhớ hào phóng, rất có thể bạn bị rò rỉ bộ nhớ hoặc sử dụng bộ nhớ không hiệu quả. Một heap dump ghi lại ảnh chụp nhanh tất cả các đối tượng trong heap của JVM tại một thời điểm cụ thể. Phân tích ảnh chụp nhanh này có thể tiết lộ đối tượng nào đang tiêu tốn nhiều bộ nhớ nhất và tại sao bộ thu gom rác chưa thu hồi chúng.

Tạo Heap Dump:

Bạn có thể cấu hình JVM để tự động tạo heap dump khi xảy ra lỗi OutOfMemoryError:

java -Xms512m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump/ YourApplication.jar

- `-XX:+HeapDumpOnOutOfMemoryError`: Hướng dẫn JVM tạo heap dump khi xảy ra lỗi OOM.
- `-XX:HeapDumpPath=/path/to/dump/`: Chỉ định thư mục nơi tệp heap dump (ví dụ: `java_pid12345.hprof`) sẽ được lưu.

Ngoài ra, bạn có thể tạo heap dump thủ công từ một tiến trình JVM đang hoạt động bằng cách sử dụng jmap, một phần của JDK:

# Đầu tiên, tìm ID tiến trình Java (PID)
jps -l
# Sau đó, tạo heap dump
jmap -dump:format=b,file=heapdump.hprof <pid>

Phân tích Heap Dump:

Sử dụng các công cụ chuyên dụng để mở và phân tích tệp .hprof. Các lựa chọn phổ biến bao gồm:

- **Eclipse Memory Analyzer (MAT):** Một công cụ mạnh mẽ, miễn phí để phân tích heap dump. Nó rất xuất sắc trong việc xác định các đối tượng nghi ngờ rò rỉ bộ nhớ, cung cấp chế độ xem 'cây dominator' (hiển thị các đối tượng ngăn không cho các đối tượng khác được thu gom) và trực quan hóa việc tiêu thụ bộ nhớ.
- **VisualVM:** Một công cụ miễn phí khác đi kèm với JDK. Nó cung cấp khả năng giám sát trực tiếp và phân tích heap dump cơ bản.
- **YourKit Java Profiler / JProfiler:** Các công cụ thương mại này cung cấp các tính năng profiling và phân tích heap nâng cao.

Khi phân tích heap dump, hãy chú ý kỹ đến:

- Các thể hiện đối tượng hoặc mảng có kích thước lớn bất thường, có thể chứa hơn 100MB dữ liệu.
- Số lượng thể hiện của một lớp cụ thể cao bất thường.
- Các đối tượng tạo thành một "cây dominator," ngăn chặn các phần đáng kể của heap không được thu gom rác.
- Các trường tĩnh vẫn giữ tham chiếu đến các đối tượng lớn lẽ ra đã được giải tham chiếu từ lâu.

3. Tối ưu hóa mã ứng dụng và quản lý tài nguyên

Ngay cả với một heap có kích thước tốt và không có rò rỉ rõ ràng, mã được viết kém vẫn có thể nhanh chóng làm cạn kiệt heap. Hãy xem xét mã của bạn để tìm:

- **Bộ sưu tập lớn (Large Collections):** Bạn có đang tải toàn bộ cơ sở dữ liệu hoặc các tệp lớn (ví dụ: tệp CSV 5GB) vào một `List` hoặc `Map` duy nhất không? Thay vào đó, hãy cân nhắc xử lý dữ liệu theo các phần nhỏ hơn, sử dụng phân trang hoặc sử dụng các cấu trúc dữ liệu hiệu quả bộ nhớ hơn.
- **Đóng tài nguyên không đúng cách:** Luôn đảm bảo rằng các tài nguyên như kết nối cơ sở dữ liệu, luồng tệp và ổ cắm mạng được đóng đúng cách. Sử dụng khối `finally` hoặc câu lệnh try-with-resources tiện lợi của Java để ngăn chặn rò rỉ tài nguyên, điều này có thể gián tiếp dẫn đến các vấn đề về bộ nhớ.
- **Tốc độ tạo đối tượng:** Tạo quá nhiều đối tượng trong các vòng lặp hoặc các phương thức được gọi thường xuyên có thể gây áp lực lớn lên bộ thu gom rác. Tái cấu trúc mã của bạn để tái sử dụng các đối tượng khi thích hợp (ví dụ: sử dụng pool đối tượng) hoặc giảm thiểu việc cấp phát đối tượng tạm thời.
- **Thao tác chuỗi (String Manipulation):** Các phép nối chuỗi thường xuyên bên trong các vòng lặp (ví dụ: hàng trăm hoặc hàng nghìn lần) có thể tạo ra nhiều đối tượng `String` trung gian. Sử dụng `StringBuilder` hoặc `StringBuffer` để thao tác chuỗi hiệu quả hơn.
- **Chiến lược bộ nhớ đệm (Caching Strategies):** Nếu ứng dụng của bạn sử dụng bộ nhớ đệm, hãy đảm bảo chúng có các chính sách loại bỏ hiệu quả (ví dụ: LRU - Least Recently Used) để ngăn chúng phát triển vô hạn và tiêu tốn tất cả bộ nhớ có sẵn.

Cách xác minh bản sửa lỗi

Sau khi triển khai một hoặc nhiều giải pháp này, điều cần thiết là phải xác nhận rằng lỗi OutOfMemoryError đã biến mất và ứng dụng của bạn chạy đáng tin cậy. Đây là cách thực hiện:

- **Giám sát bộ nhớ JVM:** Sử dụng các công cụ như JConsole, VisualVM hoặc các tiện ích dòng lệnh như `jstat` để theo dõi việc sử dụng heap theo thời gian. Bạn muốn thấy các mẫu sử dụng bộ nhớ ổn định hoặc các biến động hợp lý, chứ không phải một xu hướng tăng liên tục.
    ```bash

jstat -gc 1000 # Giám sát thống kê thu gom rác mỗi 1 giây

    
    - **Kiểm tra nhật ký ứng dụng:** Trong các điều kiện tải khác nhau, hãy xác minh rằng thông báo `java.lang.OutOfMemoryError: Java heap space` không còn xuất hiện trong nhật ký ứng dụng của bạn.
    - **Kiểm tra tải (Load Testing):** Nếu có thể, hãy đặt ứng dụng của bạn vào các điều kiện tải điển hình hoặc cao điểm. Điều này xác nhận sự ổn định và hiệu quả bộ nhớ của nó dưới áp lực.
    - **Kiểm tra hồi quy (Regression Testing):** Luôn đảm bảo các thay đổi của bạn không vô tình gây ra lỗi mới hoặc suy giảm hiệu suất.

### Mẹo để ngăn chặn các vấn đề OutOfMemoryError trong tương lai
Để tránh các lỗi bộ nhớ tương tự trong tương lai, hãy xem xét các biện pháp chủ động sau:

    - **Giám sát chủ động:** Tích hợp giám sát bộ nhớ JVM vào các bảng điều khiển hoạt động của ứng dụng của bạn. Thiết lập cảnh báo cho việc sử dụng heap cao (ví dụ: 80% sử dụng) hoặc thu gom rác đầy đủ thường xuyên.
    - **Đánh giá mã (Code Reviews):** Thực hiện đánh giá mã thường xuyên với trọng tâm vào các hoạt động tiêu tốn nhiều bộ nhớ, quản lý tài nguyên và các mẫu rò rỉ bộ nhớ tiềm ẩn.
    - **Profile thường xuyên:** Định kỳ sử dụng các công cụ profiling trong cả giai đoạn phát triển và thử nghiệm. Điều này giúp xác định các tắc nghẽn bộ nhớ trước khi chúng đến sản xuất.
    - **Hiểu luồng dữ liệu:** Duy trì sự hiểu biết rõ ràng về cách dữ liệu di chuyển qua ứng dụng của bạn và lượng bộ nhớ mà nó thường tiêu thụ ở các giai đoạn khác nhau.
    - **Đào tạo nhà phát triển:** Đảm bảo nhóm phát triển của bạn hiểu đầy đủ các nguyên tắc cơ bản về quản lý bộ nhớ JVM và các cạm bẫy phổ biến dẫn đến lỗi `OutOfMemoryError`.

Related Error Notes