- 1 1. Giới thiệu
- 2 2. Heap Java là gì?
- 3 3. Nguyên nhân phổ biến của lỗi “java heap space”
- 3.1 3-1. Áp lực bộ nhớ từ việc tải dữ liệu lớn
- 3.2 3-2. Tích lũy dữ liệu quá mức trong các Collection
- 3.3 3-3. Rò rỉ bộ nhớ (Giữ lại đối tượng không mong muốn)
- 3.4 3-4. Kích thước Heap JVM quá nhỏ (Mặc định là nhỏ)
- 3.5 3-5. Các mẫu chạy lâu mà đối tượng liên tục tích lũy
- 3.6 3-6. Hiểu sai giới hạn trong Container (Docker / Kubernetes)
- 4 4. Cách Kiểm Tra Kích Thước Heap
- 4.1 4-1. Kiểm Tra Kích Thước Heap Từ Dòng Lệnh
- 4.2 4-2. Kiểm Tra Kích Thước Heap Từ Bên Trong Chương Trình Đang Chạy
- 4.3 4-3. Kiểm Tra Bằng Các Công Cụ Như VisualVM Hoặc Mission Control
- 4.4 4-4. Kiểm Tra Trong IDE (Eclipse / IntelliJ)
- 4.5 4-5. Kiểm Tra Trong Máy Chủ Ứng Dụng (Tomcat / Jetty)
- 4.6 4-6. Kiểm tra Heap trong Docker / Kubernetes (Quan trọng)
- 4.7 4-7. Tóm tắt: Kiểm tra Heap là Bước Bắt buộc Đầu tiên
- 5 5. Giải pháp #1: Tăng Kích thước Heap
- 5.1 5-1. Tăng Kích thước Heap từ Dòng Lệnh
- 5.2 5-2. Cấu hình cho Ứng dụng Server Định cư (Tomcat / Jetty, v.v.)
- 5.3 5-3. Cài đặt Heap cho Ứng dụng Spring Boot
- 5.4 5-4. Cài đặt Heap trong Docker / Kubernetes (Quan trọng)
- 5.5 5-5. Ví dụ Cài đặt Heap cho Môi trường CI/CD và Cloud
- 5.6 5-6. Tăng Heap Có Luôn Giải Quyết Được? → Có Giới Hạn
- 6 6. Giải Pháp #2: Tối Ưu Hoá Mã
- 6.1 6-1. Suy Nghĩ Lại Cách Bạn Sử Dụng Collections
- 6.2 6-2. Tránh Xử Lý Hàng Loạt Dữ Liệu Lớn (Xử Lý Theo Đoạn)
- 6.3 6-3. Tránh Tạo Đối Tượng Không Cần Thiết
- 6.4 6-4. Cẩn Thận Khi Nối Chuỗi
- 6.5 6-5. Đừng Xây Dựng Quá Nhiều Cache
- 6.6 6-6. Đừng Tạo Lại Đối Tượng Trong Vòng Lặp Lớn
- 6.7 6-7. Chia Công Việc Nặng Bộ Nhớ Thành Các Tiến Trình Riêng
- 6.8 6-8. Tối Ưu Hóa Mã Là Bước Quan Trọng Để Ngăn Ngừa Sự Lặp Lại
- 7 7. Giải Pháp #3: Tinh Chỉnh GC (Garbage Collection)
- 8 7-1. GC (Garbage Collection) Là Gì?
- 9 7-2. Các Loại GC và Đặc Điểm (Cách Chọn)
- 10 7-3. Cách Chuyển Đổi GC Một Cách Rõ Ràng
- 11 7-4. Xuất GC Logs và Kiểm tra Vấn đề Bằng Mắt Thường
- 12 7-5. Các Trường Hợp GC Latency Kích Hoạt “java heap space”
- 13 7-6. Các Điểm Chính Khi Tối Ưu Hóa G1GC
- 14 7-7. Tóm Tắt Về Tối Ưu Hóa GC
- 15 8. Giải Pháp #4: Phát Hiện Rò Rỉ Bộ Nhớ
- 16 8-1. Rò Rỉ Bộ Nhớ Là Gì? (Có, Nó Xảy Ra Trong Java)
- 17 8-2. Các Mẫu Rò Rỉ Bộ Nhớ Điển Hình
- 18 8-3. Các điểm kiểm tra để phát hiện “Dấu hiệu” rò rỉ bộ nhớ
- 19 8-4. Công cụ #1: Kiểm tra rò rỉ một cách trực quan với VisualVM
- 20 8-5. Công cụ #2: Phân tích sâu với Eclipse MAT (Memory Analyzer Tool)
- 21 8-6. Nếu bạn hiểu Dominator Tree, việc phân tích sẽ nhanh chóng tăng lên đáng kể
- 22 8-7. Cách ghi lại Heap Dump (Dòng lệnh)
- 23 8-8. Việc sửa rò rỉ thực sự đòi hỏi thay đổi mã nguồn
- 24 8-9. Cách phân biệt “Thiếu heap” vs “Rò rỉ bộ nhớ”
- 25 8-10. Tóm tắt: Nếu việc tinh chỉnh heap không khắc phục OOM, hãy nghi ngờ rò rỉ
- 26 9. Các vấn đề “java heap space” trong Docker / Kubernetes và cách khắc phục chúng
- 27 9-1. Tại sao lỗi Heap Space lại phổ biến trong Container
- 28 9-2. Cải tiến trong Java 8u191+ và Java 11+
- 29 9-3. Đặt kích thước Heap một cách rõ ràng trong Container (Bắt buộc)
- 30 9-4. Những cạm bẫy trong cài đặt bộ nhớ trên Kubernetes (K8s)
- 31 9-5. Tỷ lệ Heap tự động trong Java 11+ (MaxRAMPercentage)
- 32 9-6. Tại sao OOMKilled xảy ra thường xuyên trong Container (Mô hình thực tế)
- 33 9-7. Các điểm kiểm tra đặc thù cho Container bằng Log GC và Metrics
- 34 9-8. Tóm tắt: “Cài đặt Rõ ràng” là Mặc định trong Container
- 35 10. Các Anti‑Pattern Cần Tránh (Mã Xấu / Cài Đặt Sai)
- 36 10-1. Để Các Collection Không Giới Hạn Tăng Vô Hạn
- 37 10-2. Tải Tập Tin hoặc Dữ Liệu Khổng Lồ Một Lần
- 38 10-3. Tiếp Tục Giữ Dữ Liệu trong Biến static
- 39 10-4. Lạm dụng Stream / Lambda và Tạo Ra Các List Trung Gian Khổng Lồ
- 40 10-5. Nối Chuỗi Lớn Bằng Toán tử +
- 41 10-6. Tạo Quá Nhiều Cache và Không Quản Lý Chúng
- 42 10-7. Giữ Log hoặc Thống kê trong Bộ nhớ liên tục
- 43 10-8. Không chỉ định -Xmx trong Container Docker
- 44 10-9. Điều chỉnh quá mức cài đặt GC
- 45 10-10. Tóm tắt: Hầu hết các Anti-Pattern xuất phát từ “Lưu trữ quá nhiều”
- 46 11. Ví dụ Thực tế: Đoạn mã này nguy hiểm (Các mẫu vấn đề bộ nhớ điển hình)
- 47 11-1. Tải Dữ liệu Lớn Hàng loạt
- 48 11-2. Mẫu Phình to Collection
- 49 11-3. Tạo quá nhiều đối tượng trung gian qua Stream API
- 50 11-4. Phân tích JSON hoặc XML một lần
- 51 11-5. Tải toàn bộ hình ảnh / dữ liệu nhị phân vào bộ nhớ
- 52 11-6. Giữ lại vô hạn qua bộ nhớ đệm tĩnh
- 53 11-7. Sử dụng sai ThreadLocal
- 54 11-8. Tạo quá nhiều Exception
- 55 11-9. Tóm tắt: Mã nguy hiểm “lặng lẽ” ăn hết heap của bạn
- 56 12. Các thực hành tốt nhất về quản lý bộ nhớ Java (Cần thiết để ngăn ngừa tái diễn)
- 57 12-1. Đặt kích thước Heap một cách rõ ràng (đặc biệt trong môi trường Production)
- 58 12-2. Giám sát đúng cách (GC, sử dụng bộ nhớ, OOM)
- 59 12-3. Sử dụng “Cache Kiểm soát”
- 60 12-4. Cẩn thận khi lạm dụng Stream API và Lambda
- 61 12-5. Chuyển các tệp / dữ liệu lớn sang Streaming
- 62 12-6. Đối xử cẩn thận với ThreadLocal
- 63 12-7. Thường xuyên chụp Heap Dump để phát hiện rò rỉ sớm
- 64 12-8. Giữ việc tinh chỉnh GC ở mức tối thiểu
- 65 12-9. Xem xét tách kiến trúc
- 66 12-10. Tóm tắt: Quản lý bộ nhớ Java là về tối ưu hoá lớp
- 67 13. Tóm tắt: Các điểm chính để ngăn ngừa lỗi “java heap space”
- 68 13-1. Vấn đề thực sự không phải “Heap quá nhỏ” mà là “Tại sao nó lại hết?”
- 69 13-2. Các bước đầu tiên để điều tra
- 70 13-3. Các mẫu rủi ro cao thường gặp trong môi trường production
- 71 13-4. Các giải pháp cơ bản liên quan đến thiết kế hệ thống và xử lý dữ liệu
- 72 13-5. Ba thông điệp quan trọng nhất
- 73 13-6. Quản lý bộ nhớ Java là kỹ năng tạo lợi thế thực sự
- 74 14. FAQ
- 75 Q1. Sự khác nhau giữa java.lang.OutOfMemoryError: Java heap space và GC overhead limit exceeded là gì?
- 76 Q2. Nếu tôi chỉ tăng heap lên, vấn đề sẽ được giải quyết?
- 77 Q3. Tôi có thể tăng heap của Java bao nhiêu?
- 78 Q4. Tại sao Java lại bị OOMKilled trong container (Docker/K8s)?
- 79 Q5. Có cách nào dễ dàng để xác định đây có phải là rò rỉ bộ nhớ không?
- 80 Q6. Cài đặt heap trong Eclipse / IntelliJ không được áp dụng
- 81 Q7. Có đúng là Spring Boot tiêu tốn rất nhiều bộ nhớ không?
- 82 Q8. Nên dùng GC nào?
- 83 Q9. Làm sao xử lý heap trong môi trường serverless (Cloud Run / Lambda)?
- 84 Q10. Làm sao ngăn ngừa các vấn đề về heap Java tái diễn?
- 85 Summary: Sử dụng FAQ để Xóa Tan Thắc Mắc và Áp Dụng Các Biện Pháp Kiểm Soát Bộ Nhớ Thực Tiễn
1. Giới thiệu
Khi bạn phát triển bằng Java, bạn đã bao giờ ứng dụng của mình đột ngột gặp sự cố và console hiển thị:
java.lang.OutOfMemoryError: Java heap space
Lỗi này có nghĩa là “Java đã hết bộ nhớ có thể sử dụng (heap).”
Tuy nhiên, chỉ dựa trên thông báo lỗi, không ngay lập tức rõ ràng:
- Nguyên nhân khiến heap cạn kiệt
- Những gì bạn nên điều chỉnh, và cách thực hiện
- Liệu vấn đề nằm ở mã nguồn hay trong cấu hình
Do đó, mọi người thường dùng các “khắc phục nhanh” như “chỉ cần tăng -Xmx” hoặc “thêm bộ nhớ cho máy chủ.”
Nhưng việc tăng kích thước heap mà không hiểu nguyên nhân gốc rễ không chỉ không phải là giải pháp thực sự—nó còn có thể gây ra các vấn đề khác.
- GC (garbage collection) trở nên nặng hơn và thời gian phản hồi giảm
- Toàn bộ bộ nhớ máy chủ trở nên chật và ảnh hưởng đến các tiến trình khác
- Rò rỉ bộ nhớ thực sự vẫn tồn tại, và OutOfMemoryError lại xuất hiện
Đó là lý do tại sao “java heap space” không chỉ là “bộ nhớ thấp”.
Bạn nên xem nó như một dấu hiệu của vấn đề phức hợp liên quan đến thiết kế ứng dụng, triển khai và cài đặt hạ tầng.
1-1. Đối tượng hướng tới
Bài viết này dành cho những người đọc:
- Hiểu các kiến thức cơ bản về Java (lớp, phương thức, collection, v.v.)
- Nhưng không hoàn toàn hiểu cách bộ nhớ được quản lý bên trong JVM
- Đã gặp “java heap space” hoặc
OutOfMemoryErrortrong phát triển/kiểm thử/sản xuất—hoặc muốn chuẩn bị - Chạy Java trên Docker/container/cloud và cảm thấy hơi không chắc chắn về các cài đặt bộ nhớ
Số năm kinh nghiệm Java của bạn không quan trọng.
Nếu bạn muốn “hiểu đúng lỗi và học cách tự mình xác định nguyên nhân,” hướng dẫn này nhằm cung cấp lợi ích trực tiếp trong công việc thực tế.
1-2. Những gì bạn sẽ học trong bài viết này
Trong bài viết này, chúng tôi giải thích lỗi “java heap space” từ cơ chế lên trên—không chỉ là danh sách các cách khắc phục.
Các chủ đề chính bao gồm:
Heap Java là gì wp:list /wp:list
- Cách nó khác với stack
- Nơi các đối tượng được cấp phát
Các mẫu phổ biến gây ra “java heap space” wp:list /wp:list
Tải dữ liệu lớn hàng loạt
- Các collection và cache phát triển quá mức
- Rò rỉ bộ nhớ (code giữ tham chiếu còn tồn tại)
Cách kiểm tra và tăng kích thước heap wp:list /wp:list
Các tùy chọn dòng lệnh (
-Xms,-Xmx)- Cài đặt IDE (Eclipse / IntelliJ, v.v.)
- Các điểm cấu hình máy chủ ứng dụng (Tomcat, v.v.)
Kỹ thuật tiết kiệm bộ nhớ trong code wp:list /wp:list
Xem lại cách bạn sử dụng collection
- Những bẫy khi dùng streams và lambdas
- Chiến lược chia thành khối cho dữ liệu lớn
Mối quan hệ giữa GC và heap wp:list /wp:list
Cách GC hoạt động cơ bản
- Cách đọc log GC ở mức cơ bản
Phát hiện rò rỉ bộ nhớ và sử dụng công cụ wp:list /wp:list
Lấy dump heap
- Bắt đầu phân tích bằng VisualVM hoặc Eclipse MAT
Những điều cần chú ý trong môi trường container (Docker / Kubernetes) wp:list /wp:list
Mối quan hệ giữa container và
-Xmx- Giới hạn bộ nhớ qua cgroups và OOM Killer
Trong phần sau của bài viết, chúng tôi cũng trả lời các câu hỏi thường gặp theo định dạng FAQ, chẳng hạn như:
- “Có nên chỉ tăng heap ngay bây giờ không?”
- “Tôi có thể tăng heap bao xa một cách an toàn?”
- “Làm sao tôi có thể ước chừng xem có phải rò rỉ bộ nhớ không?”
1-3. Cách đọc bài viết này
Lỗi “java heap space” quan trọng đối với cả những người:
- Cần khắc phục sự cố sản xuất ngay lập tức
- Muốn ngăn ngừa vấn đề trước khi chúng xảy ra
Nếu bạn cần một giải pháp nhanh, bạn có thể chuyển tới các phần thực tiễn như:
- Cách thay đổi kích thước heap
- Cách kiểm tra rò rỉ bộ nhớ
Ngược lại, nếu bạn muốn hiểu sâu, hãy đọc theo thứ tự sau:
- Những kiến thức cơ bản: “Heap Java là gì”
- Nguyên nhân thường gặp
- Sau đó là các giải pháp và bước tinh chỉnh
Luồng này sẽ giúp bạn hiểu rõ cơ chế đằng sau lỗi.
2. Heap Java là gì?
.Để hiểu đúng lỗi “java heap space”, trước tiên bạn cần biết cách Java quản lý bộ nhớ.
Trong Java, bộ nhớ được chia thành nhiều khu vực theo mục đích, và trong số đó heap đóng vai trò quan trọng như không gian bộ nhớ cho các đối tượng.
2-1. Tổng quan về các khu vực bộ nhớ Java
Các ứng dụng Java chạy trên JVM (Java Virtual Machine).
JVM có nhiều khu vực bộ nhớ để xử lý các loại dữ liệu khác nhau. Ba khu vực phổ biến nhất là:
■ Các loại khu vực bộ nhớ
- Heap Khu vực nơi các đối tượng được tạo bởi ứng dụng được lưu trữ. Nếu khu vực này hết, bạn sẽ gặp lỗi “java heap space”.
- Stack Khu vực cho các lời gọi phương thức, biến cục bộ, tham chiếu và hơn thế nữa. Nếu khu vực này tràn, bạn sẽ gặp “StackOverflowError”.
- Method Area / Metaspace Lưu trữ thông tin lớp, hằng số, siêu dữ liệu và kết quả biên dịch JIT.
Trong Java, tất cả các đối tượng được tạo bằng new đều được đặt trên heap.
2-2. Vai trò của Heap
Heap của Java là nơi lưu trữ các thứ như sau:
- Các đối tượng được tạo bằng
new - Mảng (bao gồm nội dung của List/Map, v.v.)
- Các đối tượng được tạo nội bộ bởi lambda
- Chuỗi và bộ đệm được StringBuilder sử dụng
- Các cấu trúc dữ liệu được sử dụng trong framework collection
Nói cách khác, khi Java cần “giữ một thứ gì đó trong bộ nhớ”, hầu hết thời gian nó sẽ được lưu trên heap.
2-3. Điều gì xảy ra khi Heap hết bộ nhớ?
Nếu heap quá nhỏ—hoặc ứng dụng tạo quá nhiều đối tượng—Java sẽ chạy GC (garbage collection) để thu hồi bộ nhớ bằng cách loại bỏ các đối tượng không còn sử dụng.
Nhưng nếu GC lặp lại vẫn không giải phóng đủ bộ nhớ, và JVM không thể cấp phát thêm bộ nhớ, bạn sẽ nhận được:
java.lang.OutOfMemoryError: Java heap space
và ứng dụng sẽ bị buộc dừng.
2-4. “Chỉ tăng Heap” là một phần đúng và một phần sai
Nếu heap chỉ đơn giản là quá nhỏ, việc tăng nó có thể giải quyết vấn đề—ví dụ:
-Xms1024m -Xmx2048m
Tuy nhiên, nếu nguyên nhân gốc rễ là rò rỉ bộ nhớ hoặc xử lý dữ liệu khổng lồ không hiệu quả trong mã, việc tăng heap chỉ mua thêm thời gian và không khắc phục vấn đề cơ bản.
Tóm lại, hiểu “tại sao heap lại hết bộ nhớ” là điều quan trọng nhất.
2-5. Cấu trúc Heap (Eden / Survivor / Old)
Heap của Java được chia rộng ra thành hai phần:
Thế hệ trẻ (các đối tượng mới tạo) wp:list /wp:list
- Eden
- Survivor (S0, S1)
- Thế hệ cũ (các đối tượng sống lâu)
GC hoạt động khác nhau tùy vào khu vực.
Thế hệ trẻ
Các đối tượng đầu tiên được đặt vào Eden, và các đối tượng ngắn hạn được nhanh chóng loại bỏ.
GC chạy thường xuyên ở đây, nhưng nó tương đối nhẹ.
Thế hệ cũ
Các đối tượng tồn tại đủ lâu sẽ được nâng cấp từ Thế hệ trẻ lên Thế hệ cũ.
GC trong Thế hệ cũ tốn kém hơn, vì vậy nếu khu vực này tiếp tục tăng, nó có thể gây ra độ trễ hoặc tạm dừng.
Trong nhiều trường hợp, lỗi “heap space” cuối cùng xảy ra vì Thế hệ cũ bị đầy.
2-6. Tại sao thiếu Heap thường gặp ở người mới và lập trình viên trung cấp
Vì Java thực hiện garbage collection tự động, mọi người thường cho rằng “JVM xử lý toàn bộ quản lý bộ nhớ.”
Trong thực tế, có nhiều cách khiến heap hết, chẳng hạn:
- Mã liên tục tạo ra số lượng lớn đối tượng
- Các tham chiếu được giữ sống trong collection
- Streams/lambda vô tình tạo ra dữ liệu khổng lồ
- Cache quá lớn
- Hiểu sai giới hạn heap trong container Docker
- Cấu hình heap không đúng trong IDE
Đó là lý do tại sao việc học cách heap hoạt động là con đường ngắn nhất để có giải pháp đáng tin cậy.
3. Nguyên nhân phổ biến của lỗi “java heap space”
Thiếu heap là vấn đề thường gặp trong nhiều môi trường thực tế, nhưng nguyên nhân của nó chủ yếu có thể được nhóm thành ba loại: khối lượng dữ liệu, mã/thiết kế, và cấu hình sai.
Trong phần này, chúng tôi sắp xếp các mẫu điển hình và giải thích tại sao chúng dẫn đến lỗi.
3-1. Áp lực bộ nhớ từ việc tải dữ liệu lớn
.Mẫu phổ biến nhất là dữ liệu quá lớn khiến heap bị cạn kiệt.
■ Các ví dụ thường gặp
- Tải một tệp CSV/JSON/XML khổng lồ cả một lúc vào bộ nhớ
- Lấy một số lượng lớn bản ghi từ cơ sở dữ liệu trong một lần
- Một Web API trả về phản hồi rất lớn (hình ảnh, log, v.v.)
Một kịch bản đặc biệt nguy hiểm là:
Khi “chuỗi thô trước khi phân tích” và “các đối tượng sau khi phân tích” cùng tồn tại trong bộ nhớ đồng thời.
Ví dụ, nếu bạn tải một tệp JSON 500 MB dưới dạng một chuỗi duy nhất rồi sau đó giải tuần tự hoá nó bằng Jackson, tổng lượng bộ nhớ sử dụng có thể dễ dàng vượt quá 1 GB.
■ Hướng giảm thiểu
- Áp dụng đọc theo khối (xử lý streaming)
- Sử dụng phân trang khi truy cập cơ sở dữ liệu
- Tránh giữ dữ liệu trung gian lâu hơn mức cần thiết
Tuân thủ quy tắc “xử lý dữ liệu lớn theo khối” sẽ giúp ngăn ngừa việc cạn kiệt heap.
3-2. Tích lũy dữ liệu quá mức trong các Collection
Điều này cực kỳ phổ biến đối với các lập trình viên mới bắt đầu đến trung cấp.
■ Những sai lầm điển hình
- Liên tục thêm log hoặc dữ liệu tạm thời vào một
List→ nó liên tục tăng mà không được xóa - Dùng một
Maplàm cache (nhưng không bao giờ loại bỏ các mục) - Tạo đối tượng mới liên tục trong vòng lặp
- Tạo ra số lượng lớn các đối tượng tạm thời qua Streams hoặc lambda
Trong Java, miễn là một tham chiếu còn tồn tại, GC không thể thu hồi đối tượng.
Trong nhiều trường hợp, các lập trình viên vô tình giữ các tham chiếu còn sống.
■ Hướng giảm thiểu
- Định nghĩa vòng đời cho các cache
- Đặt giới hạn dung lượng cho các collection
- Đối với các cơ chế dữ liệu lớn, xóa định kỳ
Để tham khảo, ngay cả khi nó không “trông” như một rò rỉ bộ nhớ:
List<String> list = new ArrayList<>();
for (...) {
list.add(heavyData); // ← grows forever
}
Kiểu mã này rất nguy hiểm.
3-3. Rò rỉ bộ nhớ (Giữ lại đối tượng không mong muốn)
Vì Java có GC, nhiều người thường nghĩ “rò rỉ bộ nhớ không xảy ra trong Java.”
Trong thực tế, rò rỉ bộ nhớ chắc chắn xảy ra trong Java.
■ Các điểm nóng thường gặp của rò rỉ
- Giữ đối tượng trong biến tĩnh
- Quên hủy đăng ký listener hoặc callback
- Để lại các tham chiếu còn sống trong Streams/Lambda
- Các đối tượng tích lũy trong các batch job chạy lâu dài
- Lưu trữ dữ liệu lớn trong ThreadLocal và thread được tái sử dụng
Rò rỉ bộ nhớ không phải là thứ bạn có thể hoàn toàn tránh trong Java.
■ Hướng giảm thiểu
- Xem lại cách bạn sử dụng biến tĩnh
- Đảm bảo luôn gọi
removeListener()vàclose() - Đối với các tiến trình chạy lâu, thực hiện heap dump và điều tra
- Tránh ThreadLocal trừ khi thực sự cần thiết
Vì rò rỉ bộ nhớ sẽ tái diễn ngay cả khi bạn tăng kích thước heap,
việc điều tra nguyên nhân gốc rễ là điều thiết yếu.
3-4. Kích thước Heap JVM quá nhỏ (Mặc định là nhỏ)
Đôi khi ứng dụng hoạt động tốt, nhưng heap đơn giản là quá nhỏ.
Kích thước heap mặc định thay đổi tùy theo hệ điều hành và phiên bản Java.
Trong Java 8, nó thường được đặt khoảng 1/64 đến 1/4 bộ nhớ vật lý.
Một cấu hình nguy hiểm thường thấy trong môi trường production là:
No -Xmx specified, while the app processes large data
■ Các kịch bản phổ biến
- Chỉ môi trường production có khối lượng dữ liệu lớn hơn, và heap mặc định không đủ
- Chạy trên Docker mà không thiết lập
-Xmx - Spring Boot khởi động dưới dạng fat JAR với các giá trị mặc định
■ Hướng giảm thiểu
- Đặt
-Xmsvà-Xmxthành các giá trị phù hợp - Trong container, hiểu bộ nhớ vật lý vs giới hạn cgroup và cấu hình cho phù hợp
3-5. Các mẫu chạy lâu mà đối tượng liên tục tích lũy
Các ứng dụng như sau thường gây áp lực bộ nhớ tăng dần theo thời gian:
- Ứng dụng Spring Boot chạy lâu dài
- Các batch job tiêu tốn nhiều bộ nhớ
- Ứng dụng web với lưu lượng người dùng lớn
Đặc biệt, các batch job thường hiển thị mẫu này:
- Bộ nhớ bị tiêu thụ
- GC hầu như không thu hồi được gì
- Một phần bộ nhớ vẫn còn lại, và lần chạy tiếp theo lại gặp OOM
This leads to many lỗi heap space xuất hiện chậm.
3-6. Hiểu sai giới hạn trong Container (Docker / Kubernetes)
Có một bẫy phổ biến trong Docker/Kubernetes:
■ Bẫy
- Không đặt
-Xmx→ Java tham chiếu tới bộ nhớ vật lý của máy chủ thay vì giới hạn của container → nó sử dụng quá nhiều → tiến trình bị OOM Killer giết
Đây là một trong những sự cố sản xuất phổ biến nhất.
■ Giảm thiểu
- Đặt
-XX:MaxRAMPercentagemột cách phù hợp - Đồng bộ
-Xmxvới giới hạn bộ nhớ của container - Hiểu “UseContainerSupport” trong Java 11+
4. Cách Kiểm Tra Kích Thước Heap
Khi bạn thấy lỗi “java heap space”, việc đầu tiên bạn nên làm là xác nhận lượng heap hiện đang được cấp phát.
Trong nhiều trường hợp, heap chỉ đơn giản là nhỏ hơn mong đợi — vì vậy việc kiểm tra là bước đầu tiên quan trọng.
Trong phần này, chúng tôi sẽ trình bày các cách kiểm tra kích thước heap từ dòng lệnh, bên trong chương trình, IDE và máy chủ ứng dụng.
4-1. Kiểm Tra Kích Thước Heap Từ Dòng Lệnh
Java cung cấp một số tùy chọn để kiểm tra các giá trị cấu hình JVM khi khởi động.
■ Sử dụng -XX:+PrintFlagsFinal
Đây là cách đáng tin cậy nhất để xác nhận kích thước heap:
java -XX:+PrintFlagsFinal -version | grep HeapSize
Bạn sẽ thấy đầu ra như sau:
- InitialHeapSize … kích thước heap ban đầu được chỉ định bởi
-Xms - MaxHeapSize … kích thước heap tối đa được chỉ định bởi
-Xmx
Ví dụ:
uintx InitialHeapSize = 268435456
uintx MaxHeapSize = 4294967296
Điều này có nghĩa là:
- Heap ban đầu: 256MB
- Heap tối đa: 4GB
■ Ví dụ Cụ Thể
java -Xms512m -Xmx2g -XX:+PrintFlagsFinal -version | grep HeapSize
Điều này cũng hữu ích sau khi thay đổi cài đặt, làm cho nó trở thành một phương pháp xác nhận đáng tin cậy.
4-2. Kiểm Tra Kích Thước Heap Từ Bên Trong Chương Trình Đang Chạy
Đôi khi bạn muốn kiểm tra lượng heap từ bên trong ứng dụng đang chạy.
Java làm cho việc này dễ dàng bằng cách sử dụng lớp Runtime:
long max = Runtime.getRuntime().maxMemory();
long total = Runtime.getRuntime().totalMemory();
long free = Runtime.getRuntime().freeMemory();
System.out.println("Max Heap: " + (max / 1024 / 1024) + " MB");
System.out.println("Total Heap: " + (total / 1024 / 1024) + " MB");
System.out.println("Free Heap: " + (free / 1024 / 1024) + " MB");
- maxMemory() … kích thước heap tối đa (
-Xmx) - totalMemory() … heap hiện đang được JVM cấp phát
- freeMemory() … không gian hiện có sẵn trong heap đó
Đối với các ứng dụng web hoặc các tiến trình chạy lâu, việc ghi lại các giá trị này có thể giúp trong quá trình điều tra sự cố.
4-3. Kiểm Tra Bằng Các Công Cụ Như VisualVM Hoặc Mission Control
Bạn cũng có thể kiểm tra việc sử dụng heap một cách trực quan bằng các công cụ GUI.
■ VisualVM
- Hiển thị việc sử dụng heap theo thời gian thực
- Thời gian GC
- Ghi lại heap dump
Đây là một công cụ cổ điển, thường được sử dụng trong phát triển Java.
■ Java Mission Control (JMC)
- Cho phép profiling chi tiết hơn
- Đặc biệt hữu ích cho các hoạt động trên Java 11+
Các công cụ này giúp bạn hình dung các vấn đề như chỉ thế hệ Old tăng lên.
4-4. Kiểm Tra Trong IDE (Eclipse / IntelliJ)
Nếu bạn chạy ứng dụng từ một IDE, cài đặt của IDE có thể ảnh hưởng đến kích thước heap.
■ Eclipse
Window → Preferences → Java → Installed JREs
Hoặc đặt -Xms / -Xmx dưới:
Run Configuration → VM arguments
■ IntelliJ IDEA
Help → Change Memory Settings
Hoặc thêm -Xmx dưới VM options trong Run/Debug Configuration.
Cẩn thận — đôi khi IDE tự nó áp đặt một giới hạn heap.
4-5. Kiểm Tra Trong Máy Chủ Ứng Dụng (Tomcat / Jetty)
Đối với các ứng dụng web, kích thước heap thường được chỉ định trong các script khởi động server.
■ Ví dụ Tomcat (Linux)
CATALINA_OPTS="-Xms512m -Xmx2g"
■ Ví dụ Tomcat (Windows)
set JAVA_OPTS=-Xms512m -Xmx2g
.Trong môi trường sản xuất, để các thiết lập ở mặc định là điều thường gặp—và nó thường dẫn đến lỗi heap space sau khi dịch vụ đã chạy một thời gian.
4-6. Kiểm tra Heap trong Docker / Kubernetes (Quan trọng)
Trong container, bộ nhớ vật lý, cgroups và các thiết lập Java tương tác theo những cách phức tạp.
Trong Java 11+, “UseContainerSupport” có thể tự động điều chỉnh heap, nhưng hành vi vẫn có thể không như mong đợi tùy thuộc vào:
- Giới hạn bộ nhớ của container (ví dụ,
--memory=512m) - Việc
-Xmxcó được đặt một cách rõ ràng hay không
Ví dụ, nếu bạn chỉ đặt một giới hạn bộ nhớ cho container:
docker run --memory=512m ...
và không đặt -Xmx, bạn có thể gặp phải:
- Java tham chiếu tới bộ nhớ của máy chủ và cố gắng cấp phát quá nhiều
- cgroups thực thi giới hạn
- Quá trình bị OOM Killer giết
Đây là một vấn đề rất phổ biến trong môi trường sản xuất.
4-7. Tóm tắt: Kiểm tra Heap là Bước Bắt buộc Đầu tiên
Thiếu hụt heap đòi hỏi các giải pháp rất khác nhau tùy thuộc vào nguyên nhân.
Bắt đầu bằng cách hiểu, như một tập hợp:
- Kích thước heap hiện tại
- Mức sử dụng thực tế
- Trực quan hoá qua các công cụ
5. Giải pháp #1: Tăng Kích thước Heap
Cách phản hồi trực tiếp nhất với lỗi “java heap space” là tăng kích thước heap.
Nếu nguyên nhân là thiếu bộ nhớ đơn giản, việc tăng heap một cách phù hợp có thể khôi phục hành vi bình thường.
Tuy nhiên, khi bạn tăng heap, điều quan trọng là phải hiểu cả
các phương pháp cấu hình đúng và các biện pháp phòng ngừa chính.
Cài đặt không đúng có thể dẫn đến suy giảm hiệu năng hoặc các vấn đề OOM (Out Of Memory) khác.
5-1. Tăng Kích thước Heap từ Dòng Lệnh
Nếu bạn khởi chạy một ứng dụng Java dưới dạng JAR, phương pháp cơ bản nhất là chỉ định -Xms và -Xmx:
■ Ví dụ: Ban đầu 512MB, Tối đa 2GB
java -Xms512m -Xmx2g -jar app.jar
-Xms… kích thước heap ban đầu được dành ra khi JVM khởi động-Xmx… kích thước heap tối đa mà JVM có thể sử dụng
Trong nhiều trường hợp, việc đặt -Xms và -Xmx bằng nhau giúp giảm chi phí từ việc thay đổi kích thước heap.
Ví dụ:
java -Xms2g -Xmx2g -jar app.jar
5-2. Cấu hình cho Ứng dụng Server Định cư (Tomcat / Jetty, v.v.)
Đối với các ứng dụng web, đặt các tùy chọn này trong các script khởi động của server ứng dụng.
■ Tomcat (Linux)
Đặt trong setenv.sh:
export CATALINA_OPTS="$CATALINA_OPTS -Xms512m -Xmx2048m"
■ Tomcat (Windows)
Đặt trong setenv.bat:
set CATALINA_OPTS=-Xms512m -Xmx2048m
■ Jetty
Thêm các dòng sau vào start.ini hoặc jetty.conf:
--exec
-Xms512m
-Xmx2048m
Vì các ứng dụng web có thể tăng đột biến mức sử dụng bộ nhớ tùy theo lưu lượng, môi trường production thường nên có dự phòng bộ nhớ nhiều hơn so với môi trường test.
5-3. Cài đặt Heap cho Ứng dụng Spring Boot
Nếu bạn chạy Spring Boot dưới dạng fat JAR, các nguyên tắc cơ bản vẫn giống nhau:
java -Xms1g -Xmx2g -jar spring-app.jar
Spring Boot thường tiêu thụ nhiều bộ nhớ hơn một chương trình Java đơn giản vì nó tải nhiều lớp và cấu hình khi khởi động.
Nó thường tiêu thụ nhiều bộ nhớ hơn một ứng dụng Java điển hình.
5-4. Cài đặt Heap trong Docker / Kubernetes (Quan trọng)
Đối với Java trong container, bạn phải cẩn thận vì giới hạn container và việc tính toán heap của JVM tương tác với nhau.
■ Ví dụ Đề xuất (Docker)
docker run --memory=1g \
-e JAVA_OPTS="-Xms512m -Xmx800m" \
my-java-app
■ Tại sao Bạn Phải Đặt Rõ -Xmx
Nếu bạn không chỉ định -Xmx trong Docker:
- JVM quyết định kích thước heap dựa trên bộ nhớ vật lý của máy chủ, không phải của container
- Nó có thể cố gắng cấp phát nhiều bộ nhớ hơn giới hạn của container
- Nó chạm tới giới hạn bộ nhớ cgroup và quá trình bị OOM Killer giết
Vì đây là một vấn đề rất phổ biến trong production, bạn luôn nên đặt -Xmx trong môi trường container.
5-5. Ví dụ Cài đặt Heap cho Môi trường CI/CD và Cloud
Trong các môi trường Java dựa trên cloud, một quy tắc chung là đặt heap dựa trên bộ nhớ khả dụng:
| Total Memory | Recommended Heap (Approx.) |
|---|---|
| 1GB | 512–800MB |
| 2GB | 1.2–1.6GB |
| 4GB | 2–3GB |
| 8GB | 4–6GB |
※ Để lại bộ nhớ còn lại cho hệ điều hành, chi phí GC và ngăn xếp các luồng.
Trong môi trường đám mây, tổng bộ nhớ có thể bị giới hạn. Nếu bạn tăng heap mà không lên kế hoạch, toàn bộ ứng dụng có thể trở nên không ổn định.
5-6. Tăng Heap Có Luôn Giải Quyết Được? → Có Giới Hạn
Tăng kích thước heap có thể tạm thời loại bỏ lỗi, nhưng không giải quyết được các trường hợp như:
- Có rò rỉ bộ nhớ
- Một collection liên tục tăng vô hạn
- Dữ liệu khổng lồ được xử lý hàng loạt
- Ứng dụng có thiết kế không đúng
Vì vậy, hãy coi việc tăng heap như một biện pháp khẩn cấp, và chắc chắn phải tiếp tục với tối ưu hoá mã và xem lại thiết kế xử lý dữ liệu của bạn, mà chúng ta sẽ đề cập tiếp theo.
6. Giải Pháp #2: Tối Ưu Hoá Mã
Tăng kích thước heap có thể là một biện pháp giảm thiểu hiệu quả, nhưng nếu nguyên nhân gốc rễ nằm ở cấu trúc mã của bạn hoặc cách bạn xử lý dữ liệu, lỗi “java heap space” sẽ quay trở lại sớm hay muộn.
Trong phần này, chúng ta sẽ đề cập đến các mẫu mã thực tế thường gặp mà lãng phí bộ nhớ, và các cách tiếp cận cụ thể để cải thiện chúng.
6-1. Suy Nghĩ Lại Cách Bạn Sử Dụng Collections
Các collection của Java (List, Map, Set, v.v.) rất tiện lợi, nhưng việc sử dụng không cẩn thận có thể dễ dàng trở thành nguyên nhân chính gây tăng bộ nhớ.
■ Mẫu ①: List / Map Tăng Không Giới Hạn
Một ví dụ phổ biến:
List<String> logs = new ArrayList<>();
while (true) {
logs.add(fetchLog()); // ← grows forever
}
Các collection không có điều kiện dừng rõ ràng hoặc giới hạn trên sẽ chắc chắn làm đầy heap trong các môi trường chạy lâu.
● Cải Thiện
- Sử dụng collection có giới hạn (ví dụ: đặt kích thước tối đa và loại bỏ các mục cũ)
- Thường xuyên xóa các giá trị không còn cần thiết
- Nếu bạn dùng Map làm cache, hãy áp dụng cache có eviction → Guava Cache hoặc Caffeine là các lựa chọn tốt
■ Mẫu ②: Không Đặt Dung Lượng Ban Đầu
ArrayList và HashMap tự động mở rộng khi vượt quá dung lượng, nhưng quá trình mở rộng này bao gồm: cấp phát mảng mới → sao chép → loại bỏ mảng cũ.
Khi xử lý các bộ dữ liệu lớn, việc không đặt dung lượng ban đầu là không hiệu quả và có thể lãng phí bộ nhớ.
● Ví Dụ Cải Thiện
List<String> items = new ArrayList<>(10000);
Nếu bạn có thể ước tính kích thước, tốt hơn nên đặt trước.
6-2. Tránh Xử Lý Hàng Loạt Dữ Liệu Lớn (Xử Lý Theo Đoạn)
Nếu bạn xử lý dữ liệu khổng lồ một lần, dễ rơi vào kịch bản tệ nhất: mọi thứ đều nằm trên heap → OOM.
■ Ví Dụ Xấu (Đọc Một Tập Tin Lớn Cả Lần)
String json = Files.readString(Paths.get("large.json"));
Object data = new ObjectMapper().readValue(json, Data.class);
■ Cải Thiện
- Sử dụng xử lý streaming (ví dụ: Jackson Streaming API)
- Đọc theo các phần nhỏ hơn (phân trang batch)
- Xử lý các stream tuần tự và không giữ lại toàn bộ dataset
● Ví Dụ: Xử Lý JSON Lớn Với Jackson Streaming
JsonFactory factory = new JsonFactory();
try (JsonParser parser = factory.createParser(new File("large.json"))) {
while (!parser.isClosed()) {
JsonToken token = parser.nextToken();
// Perform only what you need, and do not retain it in memory
}
}
6-3. Tránh Tạo Đối Tượng Không Cần Thiết
Streams và lambdas rất tiện lợi, nhưng chúng có thể tạo ra số lượng lớn các đối tượng tạm thời bên trong.
■ Ví Dụ Xấu (Tạo Một Danh Sách Trung Gian Lớn Với Streams)
List<Result> results = items.stream()
.map(this::toResult)
.collect(Collectors.toList());
Nếu items rất lớn, một số lượng lớn các đối tượng tạm thời được tạo ra, và heap bùng lên.
● Cải Thiện
- Xử lý tuần tự bằng vòng for
- Ghi ra chỉ những gì bạn cần ngay lập tức (không giữ lại mọi thứ)
- Tránh
collect(), hoặc kiểm soát nó một cách thủ công
6-4. Cẩn Thận Khi Nối Chuỗi
Các String trong Java là bất biến, vì vậy mỗi lần nối tạo một đối tượng mới.
■ Cải Thiện
- Sử dụng
StringBuildercho việc nối chuỗi nặng - Tránh nối chuỗi không cần thiết khi tạo log
StringBuilder sb = new StringBuilder(); for (String s : items) { sb.append(s); }
6-5. Đừng Xây Dựng Quá Nhiều Cache
Đây là một tình huống phổ biến trong các ứng dụng web và xử lý batch:
- “Chúng tôi đã thêm một cache để tăng tốc.”
- → nhưng quên xóa nó
- → cache tiếp tục tăng
- → thiếu bộ nhớ heap → OOM
■ Cải Thiện
- Đặt TTL (hết hạn dựa trên thời gian) và kích thước tối đa
- Sử dụng
ConcurrentHashMaplàm thay thế cache là rủi ro - Sử dụng cache được quản lý tốt như Caffeine để kiểm soát bộ nhớ một cách hợp lý
6-6. Đừng Tạo Lại Đối Tượng Trong Vòng Lặp Lớn
■ Ví Dụ Xấu
for (...) {
StringBuilder sb = new StringBuilder(); // created every iteration
...
}
Điều này tạo ra nhiều đối tượng tạm thời hơn cần thiết.
● Cải Thiện
StringBuilder sb = new StringBuilder();
for (...) {
sb.setLength(0); // reuse
}
6-7. Chia Công Việc Nặng Bộ Nhớ Thành Các Tiến Trình Riêng
Khi bạn xử lý dữ liệu thực sự khổng lồ trong Java, bạn có thể cần xem lại kiến trúc.
- Tách ETL thành một job batch riêng
- Ủy thác cho xử lý phân tán (Spark hoặc Hadoop)
- Tách các dịch vụ để tránh tranh chấp heap
6-8. Tối Ưu Hóa Mã Là Bước Quan Trọng Để Ngăn Ngừa Sự Lặp Lại
Nếu bạn chỉ tăng heap, cuối cùng bạn sẽ gặp “giới hạn” tiếp theo, và lỗi tương tự sẽ lại xảy ra.
Để ngăn ngừa cơ bản các lỗi “java heap space”, bạn phải:
- Hiểu khối lượng dữ liệu của bạn
- Xem xét các mẫu tạo đối tượng
- Cải thiện thiết kế collection
7. Giải Pháp #3: Tinh Chỉnh GC (Garbage Collection)
Lỗi “java heap space” có thể xảy ra không chỉ khi heap quá nhỏ, mà còn khi GC không thể thu hồi bộ nhớ hiệu quả và heap dần dần bị bão hòa.
Nếu không hiểu GC, bạn có thể dễ dàng chẩn đoán sai các triệu chứng như:
“Bộ nhớ nên có sẵn, nhưng chúng tôi vẫn gặp lỗi,” hoặc “Hệ thống trở nên cực kỳ chậm.”
Phần này giải thích cơ chế GC cơ bản trong Java và các điểm tinh chỉnh thực tế giúp trong hoạt động thực tế.
7-1. GC (Garbage Collection) Là Gì?
GC là cơ chế của Java để tự động loại bỏ các đối tượng không còn cần thiết.
Heap của Java được chia rộng ra thành hai thế hệ, và GC hoạt động khác nhau ở mỗi thế hệ.
● Thế hệ Young (đối tượng ngắn hạn)
- Eden / Survivor (S0, S1)
- Dữ liệu tạm thời được tạo cục bộ, v.v.
- GC xảy ra thường xuyên, nhưng nhẹ
● Thế hệ Old (đối tượng lâu dài)
- Các đối tượng được nâng lên từ Young
- GC nặng hơn; nếu xảy ra thường xuyên, ứng dụng có thể “đóng băng”
Trong nhiều trường hợp, “java heap space” cuối cùng xảy ra khi thế hệ Old đầy.
7-2. Các Loại GC và Đặc Điểm (Cách Chọn)
Java cung cấp nhiều thuật toán GC.
Việc chọn đúng cho khối lượng công việc của bạn có thể cải thiện đáng kể hiệu suất.
● ① G1GC (Mặc định từ Java 9)
- Chia heap thành các vùng nhỏ và thu hồi chúng một cách tăng dần
- Có thể giữ thời gian dừng toàn bộ ngắn hơn
- Thích hợp cho các ứng dụng web và hệ thống doanh nghiệp
→ Nói chung, G1GC là lựa chọn mặc định an toàn
● ② Parallel GC (Tốt cho các job batch có nhu cầu thông lượng cao)
- Được thực hiện song song và nhanh
- Nhưng thời gian dừng có thể trở nên dài hơn
- Thường có lợi cho xử lý batch nặng CPU
● ③ ZGC (GC độ trễ thấp với thời gian dừng mức miligiây)
- Có sẵn trong Java 11+
- Dành cho các ứng dụng nhạy cảm với độ trễ (máy chủ game, HFT)
- Hiệu quả ngay cả với heap lớn (hàng chục GB)
● ④ Shenandoah (GC độ trễ thấp)
- Thường liên quan đến các bản phân phối của Red Hat
- Có thể giảm thời gian dừng một cách mạnh mẽ
- Cũng có sẵn trong một số bản dựng như AWS Corretto
7-3. Cách Chuyển Đổi GC Một Cách Rõ Ràng
G1GC là mặc định trong nhiều cấu hình, nhưng bạn có thể chỉ định thuật toán GC tùy theo mục tiêu:
# G1GC
java -XX:+UseG1GC -jar app.jar
# Parallel GC
java -XX:+UseParallelGC -jar app.jar
# ZGC
java -XX:+UseZGC -jar app.jar
Vì thuật toán GC có thể thay đổi đáng kể hành vi heap và thời gian tạm dừng, các hệ thống sản xuất thường thiết lập điều này một cách rõ ràng.
7-4. Xuất GC Logs và Kiểm tra Vấn đề Bằng Mắt Thường
Điều quan trọng là hiểu GC đang thu hồi bao nhiêu bộ nhớ và stop-the-world pauses xảy ra thường xuyên như thế nào.
● Cấu hình Ghi Log GC Cơ Bản
java \
-Xms1g -Xmx1g \
-XX:+PrintGCDetails \
-XX:+PrintGCDateStamps \
-Xloggc:gc.log \
-jar app.jar
Bằng cách kiểm tra gc.log, bạn có thể xác định các dấu hiệu rõ ràng của áp lực heap, chẳng hạn như:
- Quá nhiều Young GC
- Thế hệ Old không bao giờ giảm
- Full GC xảy ra thường xuyên
- Mỗi GC thu hồi một lượng nhỏ bất thường
7-5. Các Trường Hợp GC Latency Kích Hoạt “java heap space”
Nếu áp lực heap được gây ra bởi các mẫu như sau, hành vi GC sẽ trở thành manh mối quyết định.
● Triệu Chứng
- Ứng dụng đột ngột bị đóng băng
- GC chạy trong vài giây đến hàng chục giây
- Thế hệ Old tiếp tục tăng trưởng
- Full GC tăng lên, và cuối cùng OOM xảy ra
Điều này cho thấy trạng thái mà GC đang cố gắng hết sức, nhưng không thể thu hồi đủ bộ nhớ trước khi đạt giới hạn.
■ Nguyên Nhân Gốc Rễ Phổ Biến
- Rò rỉ bộ nhớ
- Các bộ sưu tập được giữ vĩnh viễn
- Các đối tượng sống quá lâu
- Lạm phát thế hệ Old
Trong những trường hợp này, phân tích GC logs có thể giúp bạn xác định tín hiệu rò rỉ hoặc các đỉnh tải tại thời điểm cụ thể.
7-6. Các Điểm Chính Khi Tối Ưu Hóa G1GC
G1GC mạnh mẽ theo mặc định, nhưng việc tối ưu hóa có thể làm cho nó ổn định hơn nữa.
● Các Tham Số Phổ Biến
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=8m
-XX:InitiatingHeapOccupancyPercent=45
- MaxGCPauseMillis → Thời gian tạm dừng mục tiêu (ví dụ: 200ms)
- G1HeapRegionSize → Kích thước vùng được sử dụng để phân vùng heap
- InitiatingHeapOccupancyPercent → Phần trăm chiếm dụng của thế hệ Old kích hoạt chu kỳ GC
Tuy nhiên, trong nhiều trường hợp các giá trị mặc định là đủ, vì vậy chỉ thay đổi những điều này khi bạn có nhu cầu rõ ràng.
7-7. Tóm Tắt Về Tối Ưu Hóa GC
Các cải thiện GC giúp bạn hình dung các yếu tố không rõ ràng chỉ từ việc tăng kích thước heap:
- Thời gian sống của đối tượng
- Mẫu sử dụng bộ sưu tập
- Có tồn tại rò rỉ bộ nhớ hay không
- Áp lực heap tập trung ở đâu
Đó là lý do tại sao tối ưu hóa GC là một quy trình rất quan trọng để giảm thiểu “java heap space”.
8. Giải Pháp #4: Phát Hiện Rò Rỉ Bộ Nhớ
Nếu lỗi vẫn lặp lại ngay cả sau khi tăng heap và tối ưu hóa mã, nghi phạm có khả năng nhất là rò rỉ bộ nhớ.
Mọi người thường cho rằng Java kháng cự với rò rỉ bộ nhớ vì có GC, nhưng trên thực tế, rò rỉ bộ nhớ là một trong những nguyên nhân phiền toái và dễ tái phát nhất trong môi trường thực tế.
Ở đây, chúng tôi tập trung vào các bước thực tế mà bạn có thể sử dụng ngay lập tức, từ việc hiểu về rò rỉ đến sử dụng các công cụ phân tích như VisualVM và Eclipse MAT.
8-1. Rò Rỉ Bộ Nhớ Là Gì? (Có, Nó Xảy Ra Trong Java)
Rò rỉ bộ nhớ Java là:
Trạng thái mà các tham chiếu đến các đối tượng không cần thiết vẫn tồn tại, ngăn GC thu hồi chúng.
Ngay cả với garbage collection, rò rỉ thường xảy ra khi:
- Các đối tượng được giữ trong các trường
static - Các listener được đăng ký động không bao giờ được hủy đăng ký
- Các bộ sưu tập tiếp tục phát triển và giữ tham chiếu
- Các giá trị ThreadLocal tồn tại bất ngờ
- Chu kỳ sống của framework không khớp với chu kỳ sống của đối tượng của bạn
Vì vậy, rò rỉ hoàn toàn là một khả năng bình thường.
8-2. Các Mẫu Rò Rỉ Bộ Nhớ Điển Hình
● ① Sự Phát Triển Của Bộ Sưu Tập (Phổ Biến Nhất)
Liên tục thêm vào List/Map/Set mà không loại bỏ các mục.
Trong các hệ thống Java kinh doanh, một phần lớn các sự cố OOM đến từ mẫu này.
● ② Giữ Đối Tượng Trong Biến static
private static List<User> cache = new ArrayList<>();
Điều này thường trở thành điểm khởi đầu của rò rỉ.
● ③ Quên Hủy Đăng Ký Listener / Callback
Các tham chiếu vẫn tồn tại ở hậu cảnh qua GUI, observers, event listeners, v.v.
● ④ Lạm Dụng ThreadLocal
Trong môi trường thread-pool, các giá trị ThreadLocal có thể tồn tại lâu hơn dự định.
.### ● ⑤ Tham chiếu được giữ bởi các thư viện bên ngoài
Một số “bộ nhớ ẩn” khó quản lý từ mã ứng dụng, do đó việc phân tích bằng công cụ là điều cần thiết.
8-3. Các điểm kiểm tra để phát hiện “Dấu hiệu” rò rỉ bộ nhớ
Nếu bạn thấy các hiện tượng sau, nên nghi ngờ mạnh mẽ có rò rỉ bộ nhớ:
- Chỉ thế hệ Old tăng đều
- GC toàn bộ (Full GC) diễn ra thường xuyên hơn
- Bộ nhớ hầu như không giảm ngay cả sau Full GC
- Mức sử dụng heap tăng theo thời gian hoạt động
- Chỉ có môi trường production gặp sự cố sau thời gian chạy dài
Những điều này sẽ dễ hiểu hơn rất nhiều khi được trực quan hoá bằng các công cụ.
8-4. Công cụ #1: Kiểm tra rò rỉ một cách trực quan với VisualVM
VisualVM thường được đóng gói cùng JDK trong một số môi trường và là công cụ đầu tiên rất thân thiện.
● Những gì bạn có thể làm với VisualVM
- Giám sát bộ nhớ theo thời gian thực
- Xác nhận sự tăng trưởng của thế hệ Old
- Tần suất GC
- Giám sát luồng (thread)
- Ghi lại heap dump
● Cách ghi lại một Heap Dump
Trong VisualVM, mở tab “Monitor” và nhấn nút “Heap Dump”.
Sau đó bạn có thể đưa heap dump vừa ghi trực tiếp vào Eclipse MAT để phân tích sâu hơn.
8-5. Công cụ #2: Phân tích sâu với Eclipse MAT (Memory Analyzer Tool)
Nếu có một công cụ tiêu chuẩn trong ngành để phân tích rò rỉ bộ nhớ Java, đó chính là Eclipse MAT.
● Những gì MAT có thể hiển thị cho bạn
- Đối tượng nào tiêu thụ nhiều bộ nhớ nhất
- Các đường dẫn tham chiếu giữ các đối tượng còn tồn tại
- Lý do tại sao các đối tượng không được giải phóng
- Sự bùng nổ của các collection
- Báo cáo tự động “Leak Suspects” (đối tượng nghi ngờ rò rỉ)
● Các bước phân tích cơ bản
- Mở heap dump (*.hprof)
- Chạy “Leak Suspects Report”
- Tìm các collection giữ lượng lớn bộ nhớ
- Kiểm tra Dominator Tree để xác định các đối tượng “cha mẹ”
- Theo dõi đường dẫn tham chiếu (“Path to GC Root”)
8-6. Nếu bạn hiểu Dominator Tree, việc phân tích sẽ nhanh chóng tăng lên đáng kể
Dominator Tree giúp bạn xác định các đối tượng điều khiển (chiếm ưu thế) một phần lớn bộ nhớ.
Các ví dụ bao gồm:
- Một
ArrayListkhổng lồ - Một
HashMapvới số lượng khóa vô cùng lớn - Một cache không bao giờ được giải phóng
- Một singleton được giữ bởi
static
Việc tìm ra những đối tượng này có thể giảm đáng kể thời gian xác định nguồn rò rỉ.
8-7. Cách ghi lại Heap Dump (Dòng lệnh)
Bạn cũng có thể ghi lại heap dump bằng lệnh jmap:
jmap -dump:format=b,file=heap.hprof <PID>
Bạn cũng có thể cấu hình JVM tự động dump heap khi xảy ra OOM:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof
Điều này rất quan trọng cho việc điều tra sự cố trong môi trường production.
8-8. Việc sửa rò rỉ thực sự đòi hỏi thay đổi mã nguồn
Nếu tồn tại rò rỉ, các biện pháp như:
- Tăng kích thước heap
- Tinh chỉnh GC
chỉ là biện pháp hỗ trợ tạm thời.
Cuối cùng, bạn cần thay đổi thiết kế, ví dụ như:
- Sửa phần mã giữ tham chiếu vô thời hạn
- Xem lại thiết kế collection
- Tránh lạm dụng
static - Thực hiện việc loại bỏ và dọn dẹp cache

8-9. Cách phân biệt “Thiếu heap” vs “Rò rỉ bộ nhớ”
● Trong trường hợp Thiếu heap
- OOM xảy ra nhanh khi khối lượng dữ liệu tăng lên
- Tỷ lệ tăng đồng bộ với tải công việc
- Tăng kích thước heap sẽ ổn định hệ thống
● Trong trường hợp Rò rỉ bộ nhớ
- OOM xuất hiện sau thời gian hoạt động dài
- Khi số lượng yêu cầu tăng, hiệu năng dần suy giảm
- Bộ nhớ hầu như không giảm ngay cả sau Full GC
- Tăng kích thước heap không giải quyết được vấn đề
8-10. Tóm tắt: Nếu việc tinh chỉnh heap không khắc phục OOM, hãy nghi ngờ rò rỉ
Trong các vấn đề “java heap space”, nguyên nhân tốn thời gian nhất để xác định thường là rò rỉ bộ nhớ.
Nhưng với VisualVM + Eclipse MAT, bạn thường có thể phát hiện trong vòng vài phút:
- Các đối tượng tiêu thụ nhiều bộ nhớ nhất
- Các tham chiếu gốc giữ chúng còn tồn tại
- Nguồn gốc bùng nổ của các collection
9. Các vấn đề “java heap space” trong Docker / Kubernetes và cách khắc phục chúng
Modern Java applications increasingly run not only on on‑prem environments but also on Docker and Kubernetes (K8s).
However, because container environments use a different memory model than the host, there are many easy‑to‑misunderstand points for Java developers, and “java heap space” errors or OOMKilled (forced container termination) can happen frequently.
This section summarizes container‑specific memory management and the settings you must know in real operations.
9-1. Tại sao lỗi Heap Space lại phổ biến trong Container
Lý do rất đơn giản:
Java có thể không luôn nhận diện đúng giới hạn bộ nhớ của container.
● Nhầm lẫn thường gặp
“Vì tôi đã đặt giới hạn bộ nhớ Docker --memory=512m, Java nên chạy trong phạm vi 512MB.”
→ Trong thực tế, giả định này có thể sai.
Khi quyết định kích thước heap, Java có thể tham chiếu bộ nhớ vật lý của máy chủ thay vì giới hạn của container.
Kết quả:
- Java quyết định “máy chủ có đủ bộ nhớ”
- Nó cố gắng cấp phát một heap lớn hơn
- Khi vượt quá giới hạn của container, OOM Killer sẽ chạy và tiến trình bị kết thúc ép buộc
9-2. Cải tiến trong Java 8u191+ và Java 11+
Từ một số bản cập nhật Java 8 và trong Java 11+, tính năng “UseContainerSupport” đã được giới thiệu.
● Hành vi trong Container
- Có thể nhận diện giới hạn dựa trên cgroup
- Tự động tính toán kích thước heap trong giới hạn đó
Tuy nhiên, hành vi vẫn khác nhau tùy phiên bản, vì vậy việc cấu hình rõ ràng được khuyến nghị trong môi trường production.
9-3. Đặt kích thước Heap một cách rõ ràng trong Container (Bắt buộc)
● Mẫu khởi động được đề xuất
docker run \
--memory=1g \
-e JAVA_OPTS="-Xms512m -Xmx800m" \
my-java-app
Các điểm chính:
- Bộ nhớ container: 1GB
- Heap Java: giữ trong vòng 800MB
- Phần còn lại được dùng cho stack của các thread và bộ nhớ native
● Ví dụ xấu (Rất phổ biến)
docker run --memory=1g my-java-app # no -Xmx
→ Java có thể cấp phát heap dựa trên bộ nhớ của máy chủ, và khi vượt quá 1GB, bạn sẽ gặp OOMKilled.
9-4. Những cạm bẫy trong cài đặt bộ nhớ trên Kubernetes (K8s)
Trong Kubernetes, resources.limits.memory là yếu tố quan trọng.
● Ví dụ Pod
resources:
limits:
memory: "1024Mi"
requests:
memory: "512Mi"
Trong trường hợp này, giữ -Xmx của Java ở khoảng 800MB đến 900MB thường an toàn hơn.
● Tại sao đặt thấp hơn giới hạn?
Bởi vì Java sử dụng nhiều hơn chỉ heap:
- Bộ nhớ native
- Stack của các thread (hàng trăm KB × số lượng thread)
- Metaspace
- Chi phí công việc của GC
- Mã được JIT biên dịch
- Tải thư viện
Tổng cộng, chúng có thể tiêu thụ dễ dàng 100–300MB.
Trong thực tế, một quy tắc phổ biến là:
Nếu giới hạn = X, đặt -Xmx khoảng X × 0.7 đến 0.8 để an toàn.
9-5. Tỷ lệ Heap tự động trong Java 11+ (MaxRAMPercentage)
Trong Java 11, kích thước heap có thể được tính tự động bằng các quy tắc như:
● Cài đặt mặc định
-XX:MaxRAMPercentage=25
-XX:MinRAMPercentage=50
Ý nghĩa:
- Heap bị giới hạn ở 25% của bộ nhớ khả dụng
- Trong môi trường nhỏ, nó có thể sử dụng ít nhất 50% làm heap
● Cài đặt đề xuất
Trong container, thường an toàn hơn khi đặt MaxRAMPercentage một cách rõ ràng:
JAVA_OPTS="-XX:MaxRAMPercentage=70"
9-6. Tại sao OOMKilled xảy ra thường xuyên trong Container (Mô hình thực tế)
Một mô hình sản xuất phổ biến:
- Giới hạn bộ nhớ K8s = 1GB
- Không cấu hình
-Xmx - Java tham chiếu bộ nhớ của máy chủ và cố gắng cấp phát heap hơn 1GB
- Container bị kết thúc ép buộc → OOMKilled
Lưu ý rằng đây không nhất thiết là sự kiện java heap space (OutOfMemoryError)—đây là việc kết thúc ở mức container OOM.
9-7. Các điểm kiểm tra đặc thù cho Container bằng Log GC và Metrics
Trong môi trường container, đặc biệt chú ý tới:
- Việc pod khởi động lại có tăng không
- Các sự kiện OOMKilled có được ghi lại không
- Thế hệ Old có tiếp tục tăng không
- Việc thu hồi bộ nhớ GC có giảm mạnh vào những thời điểm nào không
- Bộ nhớ native (không phải heap) có hết không
Prometheus + Grafana giúp việc trực quan hoá trở nên dễ dàng hơn rất nhiều.
9-8. Tóm tắt: “Cài đặt Rõ ràng” là Mặc định trong Container
--memorymột mình có thể không khiến Java tính toán heap một cách chính xác- Luôn luôn đặt
-Xmx - Để lại không gian trống cho bộ nhớ native và stack của các luồng
- Đặt giá trị thấp hơn giới hạn bộ nhớ của Kubernetes
- Trên Java 11+,
MaxRAMPercentagecó thể hữu ích
10. Các Anti‑Pattern Cần Tránh (Mã Xấu / Cài Đặt Sai)
Lỗi “java heap space” không chỉ xảy ra khi heap thực sự không đủ, mà còn xuất hiện khi một số mẫu mã nguy hiểm hoặc cấu hình không đúng được sử dụng.
Ở đây chúng tôi tóm tắt các anti‑pattern thường gặp trong công việc thực tế.
10-1. Để Các Collection Không Giới Hạn Tăng Vô Hạn
Một trong những vấn đề phổ biến nhất là sự bùng nổ collection.
● Ví dụ Xấu: Thêm phần tử vào List mà không có bất kỳ giới hạn nào
List<String> logs = new ArrayList<>();
while (true) {
logs.add(getMessage()); // ← grows forever
}
Với thời gian hoạt động lâu, điều này dễ dàng đẩy bạn vào tình trạng OOM.
● Tại sao Nó Nguy hiểm
- GC không thể thu hồi bộ nhớ, và thế hệ Old bị bùng nổ
- Full GC trở nên thường xuyên, làm cho ứng dụng có khả năng bị treo cao hơn
- Việc sao chép một lượng lớn đối tượng làm tăng tải CPU
● Cách Tránh
- Đặt giới hạn kích thước (ví dụ: cache LRU)
- Thường xuyên xóa sạch
- Không giữ dữ liệu không cần thiết
10-2. Tải Tập Tin hoặc Dữ Liệu Khổng Lồ Một Lần
Đây là lỗi thường gặp trong xử lý batch và phía server.
● Ví dụ Xấu: Đọc một tệp JSON Khổng Lồ trong một lần
String json = Files.readString(Paths.get("large.json"));
Data d = mapper.readValue(json, Data.class);
● Điều Gì Sai
- Bạn giữ lại cả chuỗi chưa phân tích và các đối tượng đã phân tích trong bộ nhớ
- Một tệp 500 MB có thể tiêu tốn gấp đôi (hoặc hơn) dung lượng bộ nhớ
- Các đối tượng trung gian bổ sung được tạo ra, và heap nhanh chóng cạn kiệt
● Cách Tránh
- Sử dụng streaming (xử lý tuần tự)
- Đọc theo từng khối thay vì tải toàn bộ
- Không giữ toàn bộ dataset trong bộ nhớ
10-3. Tiếp Tục Giữ Dữ Liệu trong Biến static
● Ví dụ Xấu
public class UserCache {
private static Map<String, User> cache = new HashMap<>();
}
● Tại sao Nó Nguy hiểm
statictồn tại suốt thời gian JVM chạy- Nếu được dùng làm cache, các mục có thể không bao giờ được giải phóng
- Các tham chiếu vẫn còn, tạo môi trường cho rò rỉ bộ nhớ
● Cách Tránh
- Giảm thiểu việc sử dụng
staticở mức tối đa - Dùng framework cache chuyên dụng (ví dụ: Caffeine)
- Đặt TTL và giới hạn kích thước tối đa
10-4. Lạm dụng Stream / Lambda và Tạo Ra Các List Trung Gian Khổng Lồ
API Stream rất tiện lợi, nhưng nó có thể tạo ra các đối tượng trung gian bên trong và gây áp lực lên bộ nhớ.
● Ví dụ Xấu (collect tạo một list trung gian khổng lồ)
List<Item> result = items.stream()
.map(this::convert)
.collect(Collectors.toList());
● Cách Tránh
- Xử lý tuần tự bằng vòng for
- Tránh tạo các list trung gian không cần thiết
- Nếu dataset lớn, hãy cân nhắc không dùng Stream ở phần đó
10-5. Nối Chuỗi Lớn Bằng Toán tử +
Vì String là bất biến, mỗi lần nối sẽ tạo ra một đối tượng String mới.
● Ví dụ Xấu
String result = "";
for (String s : list) {
result += s;
}
● Sai Lầm
- Một String mới được tạo ra ở mỗi vòng lặp
- Số lượng đối tượng tạo ra khổng lồ, gây áp lực lên bộ nhớ
● Cách Tránh
StringBuilder sb = new StringBuilder();
for (String s : list) {
sb.append(s);
}
10-6. Tạo Quá Nhiều Cache và Không Quản Lý Chúng
● Các Ví dụ Xấu
- Lưu phản hồi API trong một Map vô thời hạn
- Liên tục cache hình ảnh hoặc dữ liệu tệp
- Không có cơ chế kiểm soát như LRU
● Tại sao Rủi ro
- Cache sẽ ngày càng lớn theo thời gian
- Bộ nhớ không thể thu hồi tăng lên
- Hầu hết sẽ trở thành vấn đề trong môi trường production
● Cách Tránh
- Đặt giới hạn thời gian sống (TTL) và kích thước tối đa cho mỗi cache
- Sử dụng các thư viện cache có hỗ trợ eviction tự động (LRU, LFU, …)
- Đánh giá lại nhu cầu cache; chỉ cache những dữ liệu thực sự cần thiết
Theo dõi và ghi log việc sử dụng cache để phát hiện sớm các hiện tượng tăng trưởng bất thường.
Sử dụng Caffeine / Guava Cache
- Đặt kích thước tối đa
- Cấu hình TTL (hết hạn)
10-7. Giữ Log hoặc Thống kê trong Bộ nhớ liên tục
● Ví dụ Xấu
List<String> debugLogs = new ArrayList<>();
debugLogs.add(message);
Trong môi trường sản xuất, log nên được ghi vào tệp hoặc hệ thống log. Giữ chúng trong bộ nhớ là rủi ro.
10-8. Không chỉ định -Xmx trong Container Docker
Điều này chiếm một phần lớn các sự cố liên quan đến heap hiện đại.
● Ví dụ Xấu
docker run --memory=1g my-app
● Điều gì Sai
- Java có thể tự động điều chỉnh kích thước heap dựa trên bộ nhớ của host
- Khi vượt quá giới hạn container, bạn sẽ gặp OOMKilled
● Cách Tránh
docker run --memory=1g -e JAVA_OPTS="-Xmx700m"
10-9. Điều chỉnh quá mức cài đặt GC
Việc điều chỉnh không đúng có thể gây phản tác dụng.
● Ví dụ Xấu
-XX:MaxGCPauseMillis=10
-XX:G1HeapRegionSize=1m
Các tham số cực đoan có thể khiến GC quá hung hãn hoặc không kịp theo.
● Cách Tránh
- Trong hầu hết các trường hợp, cài đặt mặc định là đủ
- Chỉ điều chỉnh tối thiểu khi có vấn đề cụ thể, đã đo lường
10-10. Tóm tắt: Hầu hết các Anti-Pattern xuất phát từ “Lưu trữ quá nhiều”
Điều mà tất cả các anti-pattern này chung là:
“Tích lũy nhiều đối tượng hơn cần thiết.”
- Các collection không giới hạn
- Giữ lại không cần thiết
- Tải dữ liệu hàng loạt
- Thiết kế nặng static
- Cache chạy ra ngoài kiểm soát
- Các đối tượng trung gian bùng nổ
Tránh những điều này có thể giảm đáng kể các lỗi “java heap space”.
11. Ví dụ Thực tế: Đoạn mã này nguy hiểm (Các mẫu vấn đề bộ nhớ điển hình)
Phần này giới thiệu các ví dụ mã nguy hiểm thường gặp trong dự án thực tế thường dẫn đến lỗi “java heap space”, và giải thích cho mỗi:
“Tại sao nó nguy hiểm” và “Cách khắc phục”.
Trong thực tế, các mẫu này thường xuất hiện cùng nhau, vì vậy chương này rất hữu ích cho việc review code và điều tra sự cố.
11-1. Tải Dữ liệu Lớn Hàng loạt
● Ví dụ Xấu: Đọc toàn bộ dòng của một CSV lớn
List<String> lines = Files.readAllLines(Paths.get("big.csv"));
● Tại sao nó nguy hiểm
- Càng file lớn, áp lực bộ nhớ càng cao
- Ngay cả CSV 100MB cũng có thể tiêu thụ hơn gấp đôi bộ nhớ trước và sau khi phân tích
- Giữ lại các bản ghi khổng lồ có thể làm cạn kiệt bộ nhớ Old generation
● Cải thiện: Đọc qua Stream (Xử lý tuần tự)
try (Stream<String> stream = Files.lines(Paths.get("big.csv"))) {
stream.forEach(line -> process(line));
}
→ Chỉ một dòng được giữ trong bộ nhớ tại một thời điểm, làm cho cách này rất an toàn.
11-2. Mẫu Phình to Collection
● Ví dụ Xấu: Liên tục tích lũy các đối tượng nặng trong List
List<Order> orders = new ArrayList<>();
while (hasNext()) {
orders.add(fetchNextOrder());
}
● Tại sao nó nguy hiểm
- Mỗi bước tăng kích thước lại cấp phát lại mảng nội bộ
- Nếu bạn không cần giữ mọi thứ, đó là lãng phí thuần túy
- Thời gian chạy dài có thể tiêu thụ không gian Old-generation lớn
● Cải thiện: Xử lý tuần tự + Batch khi cần
while (hasNext()) {
Order order = fetchNextOrder();
process(order); // process without retaining
}
Hoặc batch nó:
List<Order> batch = new ArrayList<>(1000);
while (hasNext()) {
batch.add(fetchNextOrder());
if (batch.size() == 1000) {
processBatch(batch);
batch.clear();
}
}
11-3. Tạo quá nhiều đối tượng trung gian qua Stream API
● Ví dụ Xấu: Các danh sách trung gian lặp lại qua map → filter → collect
List<Data> result = list.stream()
.map(this::convert)
.filter(d -> d.isValid())
.collect(Collectors.toList());
● Tại sao nó nguy hiểm
- Tạo ra nhiều đối tượng tạm thời nội bộ
- Đặc biệt rủi ro với các danh sách lớn
- Pipeline càng sâu, rủi ro càng cao
● Cải thiện: Sử dụng vòng for hoặc xử lý tuần tự
List<Data> result = new ArrayList<>();
for (Item item : list) {
Data d = convert(item);
if (d.isValid()) {
result.add(d);
}
}
11-4. Phân tích JSON hoặc XML một lần
● Ví dụ kém
String json = Files.readString(Paths.get("large.json"));
Data data = mapper.readValue(json, Data.class);
● Tại sao nó nguy hiểm
- Cả chuỗi JSON thô và các đối tượng đã giải tuần tự đều còn lại trong bộ nhớ
- Với các tệp có kích thước khoảng 100 MB, heap có thể đầy ngay lập tức
- Các vấn đề tương tự có thể xảy ra ngay cả khi sử dụng Stream API, tùy thuộc vào cách sử dụng
● Cải tiến: Sử dụng API Streaming
JsonFactory factory = new JsonFactory();
try (JsonParser parser = factory.createParser(new File("large.json"))) {
while (!parser.isClosed()) {
JsonToken token = parser.nextToken();
// Process only when needed and do not retain data
}
}
11-5. Tải toàn bộ hình ảnh / dữ liệu nhị phân vào bộ nhớ
● Ví dụ kém
byte[] image = Files.readAllBytes(Paths.get("large.png"));
● Tại sao nó nguy hiểm
- Dữ liệu nhị phân có thể lớn và “nặng” theo bản chất
- Trong các ứng dụng xử lý ảnh, đây là nguyên nhân chính gây OOM
● Cải tiến
- Sử dụng bộ đệm
- Xử lý dưới dạng luồng mà không giữ toàn bộ tệp trong bộ nhớ
- Đọc hàng loạt các log hàng triệu dòng cũng tương tự nguy hiểm
11-6. Giữ lại vô hạn qua bộ nhớ đệm tĩnh
● Ví dụ kém
private static final List<Session> sessions = new ArrayList<>();
● Điều gì sai
sessionssẽ không được giải phóng cho đến khi JVM thoát- Nó tăng lên cùng với các kết nối và cuối cùng dẫn đến OOM
● Cải tiến
- Sử dụng bộ nhớ đệm quản lý kích thước (Caffeine, Guava Cache, v.v.)
- Quản lý vòng đời session một cách rõ ràng
11-7. Sử dụng sai ThreadLocal
● Ví dụ kém
private static final ThreadLocal<SimpleDateFormat> formatter =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
ThreadLocal hữu ích, nhưng khi dùng với thread pool nó có thể giữ giá trị tồn tại và gây rò rỉ.
● Cải tiến
- Giữ ThreadLocal ngắn hạn
- Tránh sử dụng trừ khi thực sự cần thiết
- Gọi
remove()để xóa nó
11-8. Tạo quá nhiều Exception
Điều này thường bị bỏ qua, nhưng Exception là các đối tượng rất nặng do việc tạo stack trace.
● Ví dụ kém
for (...) {
try {
doSomething();
} catch (Exception e) {
// log only
}
}
→ Việc tràn ngập các exception có thể gây áp lực lên bộ nhớ.
● Cải tiến
- Không sử dụng exception cho luồng điều khiển bình thường
- Từ chối đầu vào không hợp lệ bằng việc xác thực
- Tránh ném exception trừ khi cần thiết
11-9. Tóm tắt: Mã nguy hiểm “lặng lẽ” ăn hết heap của bạn
Chủ đề chung là:
“các cấu trúc dần dần ép heap, chồng lên nhau.”
- Tải hàng loạt
- Bộ sưu tập vô hạn
- Quên hủy đăng ký/đặt lại
- Tạo đối tượng trung gian
- Tràn ngập exception
- Giữ lại tĩnh
- Các giá trị còn lại của ThreadLocal
Trong mọi trường hợp, tác động sẽ trở nên rõ ràng trong thời gian chạy dài.
12. Các thực hành tốt nhất về quản lý bộ nhớ Java (Cần thiết để ngăn ngừa tái diễn)
Cho đến nay, chúng ta đã đề cập đến các nguyên nhân gây lỗi “java heap space” và các biện pháp đối phó như mở rộng heap, cải tiến mã, tinh chỉnh GC và điều tra rò rỉ.
Phần này tóm tắt các thực hành tốt nhất giúp ngăn ngừa tái diễn một cách đáng tin cậy trong môi trường thực tế. Hãy coi chúng như các quy tắc tối thiểu để duy trì sự ổn định của các ứng dụng Java.
12-1. Đặt kích thước Heap một cách rõ ràng (đặc biệt trong môi trường Production)
Chạy các tải công việc production trên cấu hình mặc định là rủi ro.
● Thực hành tốt nhất
- Cài đặt rõ ràng
-Xmsvà-Xmx - Không chạy production trên cấu hình mặc định
- Giữ kích thước heap nhất quán giữa dev và prod (tránh sự khác biệt không mong muốn)
Ví dụ:
-Xms1g -Xmx1g
Trong Docker / Kubernetes, bạn phải đặt heap nhỏ hơn để phù hợp với giới hạn của container.
12-2. Giám sát đúng cách (GC, sử dụng bộ nhớ, OOM)
Các vấn đề về heap thường có thể ngăn ngừa nếu bạn phát hiện sớm các dấu hiệu cảnh báo.
● Những gì cần giám sát
- Sử dụng thế hệ cũ
- Xu hướng tăng trưởng thế hệ trẻ
- Tần suất Full GC
- Thời gian tạm dừng GC
- Sự kiện OOMKilled của container
- Số lần khởi động lại Pod (K8s)
● Công cụ đề xuất
- VisualVM
- JDK Mission Control
- Prometheus + Grafana
- Các chỉ số của nhà cung cấp đám mây (ví dụ: CloudWatch)
Sự tăng dần sử dụng bộ nhớ trong thời gian chạy dài là dấu hiệu rò rỉ kinh điển.
12-3. Sử dụng “Cache Kiểm soát”
Cache chạy quá mức là một trong những nguyên nhân phổ biến nhất gây OOM trong môi trường sản xuất.
● Thực hành tốt nhất
- Sử dụng Caffeine / Guava Cache
- Luôn cấu hình TTL (hết hạn)
- Đặt kích thước tối đa (ví dụ: 1.000 mục)
- Tránh cache tĩnh càng nhiều càng tốt
Caffeine.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .build();
12-4. Cẩn thận khi lạm dụng Stream API và Lambda
Đối với các bộ dữ liệu lớn, việc xâu chuỗi các thao tác Stream làm tăng số lượng đối tượng trung gian.
● Thực hành tốt nhất
- Không xâu chuỗi map/filter/collect quá mức cần thiết
- Xử lý các bộ dữ liệu khổng lồ một cách tuần tự bằng vòng lặp for
- Khi sử dụng collect, hãy chú ý đến khối lượng dữ liệu
Streams tiện lợi, nhưng không phải lúc nào cũng thân thiện với bộ nhớ.
12-5. Chuyển các tệp / dữ liệu lớn sang Streaming
Xử lý hàng loạt là nguyên nhân gốc lớn gây ra các vấn đề về heap.
● Thực hành tốt nhất
- CSV →
Files.lines() - JSON → Jackson Streaming
- Cơ sở dữ liệu → phân trang
- API → lấy dữ liệu theo phần (cursor/phân trang)
Nếu bạn thực thi “không tải toàn bộ vào bộ nhớ,” nhiều vấn đề về heap sẽ biến mất.
12-6. Đối xử cẩn thận với ThreadLocal
ThreadLocal mạnh mẽ, nhưng việc lạm dụng có thể gây rò rỉ bộ nhớ nghiêm trọng.
● Thực hành tốt nhất
- Đặc biệt cẩn thận khi kết hợp với pool luồng
- Gọi
remove()sau khi sử dụng - Không lưu trữ dữ liệu tồn tại lâu
- Tránh ThreadLocal tĩnh càng nhiều càng tốt
12-7. Thường xuyên chụp Heap Dump để phát hiện rò rỉ sớm
Đối với các hệ thống chạy lâu (ứng dụng web, hệ thống batch, IoT), việc chụp heap dump thường xuyên và so sánh chúng giúp phát hiện dấu hiệu rò rỉ sớm.
● Các tùy chọn
- VisualVM
- jmap
-XX:+HeapDumpOnOutOfMemoryError-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/heapdump.hprof
Tự động dump khi OOM là cài đặt bắt buộc trong môi trường production.
12-8. Giữ việc tinh chỉnh GC ở mức tối thiểu
Ý tưởng “tinh chỉnh GC sẽ tự động tăng hiệu năng” có thể nguy hiểm.
● Thực hành tốt nhất
- Bắt đầu với cài đặt mặc định
- Thực hiện thay đổi tối thiểu chỉ khi có vấn đề đã đo lường
- Sử dụng G1GC làm lựa chọn mặc định
- Trong nhiều trường hợp, tăng heap hiệu quả hơn so với tinh chỉnh vi mô
12-9. Xem xét tách kiến trúc
Nếu khối lượng dữ liệu trở nên quá lớn hoặc ứng dụng trở nên quá monolithic và yêu cầu heap khổng lồ, bạn có thể cần cải tiến kiến trúc:
- Microservices
- Tách xử lý dữ liệu batch
- Tách rời bằng hàng đợi tin nhắn (Kafka, v.v.)
- Xử lý phân tán (Spark, v.v.)
Nếu “dù thêm bao nhiêu heap cũng không đủ,” hãy nghi ngờ vấn đề kiến trúc.
12-10. Tóm tắt: Quản lý bộ nhớ Java là về tối ưu hoá lớp
Các vấn đề về heap hiếm khi được giải quyết bằng một cài đặt duy nhất hoặc một sửa lỗi mã duy nhất.
● Những điểm chính
- Luôn đặt heap một cách rõ ràng
- Giám sát là quan trọng nhất
- Không bao giờ cho phép bộ sưu tập bùng nổ
- Sử dụng streaming cho dữ liệu lớn
- Quản lý cache đúng cách
- Sử dụng ThreadLocal cẩn thận
- Phân tích rò rỉ bằng công cụ khi cần
- Containers yêu cầu tư duy khác
Tuân thủ các điểm này sẽ ngăn ngừa hầu hết các lỗi “java heap space” với độ chắc chắn cao.
13. Tóm tắt: Các điểm chính để ngăn ngừa lỗi “java heap space”
Trong bài viết này, chúng tôi đã đề cập đến các lỗi “java heap space” từ nguyên nhân gốc đến biện pháp giảm thiểu và ngăn ngừa tái phát.
Ở đây chúng tôi tổng hợp các yếu tố cần thiết dưới dạng bản tóm tắt thực tế.
13-1. Vấn đề thực sự không phải “Heap quá nhỏ” mà là “Tại sao nó lại hết?”
“java heap space” không chỉ là thiếu bộ nhớ đơn giản.
● Nguyên nhân gốc thường là một trong các trường hợp sau
- Kích thước heap quá nhỏ (cấu hình không đủ)
- Xử lý hàng loạt dữ liệu khổng lồ (vấn đề thiết kế)
- Bộ sưu tập phình to (thiếu việc xóa/thiết kế)
- Rò rỉ bộ nhớ (các tham chiếu vẫn còn)
- Cấu hình sai trong container (Docker/K8s‑specific)
Bắt đầu với: “Tại sao heap lại hết?”
13-2. Các bước đầu tiên để điều tra
① Xác nhận kích thước heap
→ Đặt rõ ràng -Xms / -Xmx
② Hiểu các ràng buộc bộ nhớ thời gian chạy
→ Trong Docker/Kubernetes, đồng bộ giới hạn và kích thước heap
→ Cũng kiểm tra -XX:MaxRAMPercentage
③ Thu thập và kiểm tra log GC
→ Tăng trưởng của old‑gen và Full GC thường xuyên là dấu hiệu cảnh báo
④ Thu thập và phân tích heap dump
→ Dùng VisualVM / MAT để có bằng chứng về rò rỉ
13-3. Các mẫu rủi ro cao thường gặp trong môi trường production
Như đã chỉ ra trong toàn bộ bài viết, các mẫu sau thường dẫn đến sự cố:
- Xử lý hàng loạt các tệp lớn
- Thêm vào List/Map mà không có giới hạn
- Cache chạy ra ngoài kiểm soát
- Tích lũy dữ liệu trong static
- Tạo ra các đối tượng trung gian bùng nổ qua chuỗi Stream
- Sử dụng ThreadLocal sai cách
- Không đặt -Xmx trong Docker
Nếu bạn thấy những điều này trong code hoặc cấu hình, hãy điều tra ngay.
13-4. Các giải pháp cơ bản liên quan đến thiết kế hệ thống và xử lý dữ liệu
● Những gì cần xem xét ở mức hệ thống
- Chuyển xử lý dữ liệu lớn sang xử lý streaming
- Sử dụng cache với TTL, giới hạn kích thước và eviction
- Thực hiện giám sát bộ nhớ định kỳ cho các ứng dụng chạy lâu dài
- Phân tích các dấu hiệu rò rỉ sớm bằng công cụ
● Nếu vẫn còn khó khăn
- Tách batch và online processing
- Microservices
- Áp dụng các nền tảng xử lý phân tán (Spark, Flink, …)
Có thể cần cải tiến kiến trúc.
13-5. Ba thông điệp quan trọng nhất
Nếu bạn chỉ nhớ ba điều:
✔ Luôn đặt heap một cách rõ ràng
✔ Không bao giờ xử lý hàng loạt dữ liệu khổng lồ
✔ Bạn không thể xác nhận rò rỉ nếu không có heap dump
Chỉ với ba điều này đã có thể giảm đáng kể các sự cố production nghiêm trọng do “java heap space”.
13-6. Quản lý bộ nhớ Java là kỹ năng tạo lợi thế thực sự
Quản lý bộ nhớ Java có thể cảm thấy khó, nhưng nếu bạn hiểu:
- Việc điều tra sự cố trở nên nhanh hơn đáng kể
- Hệ thống tải cao có thể chạy ổn định
- Tối ưu hiệu năng trở nên chính xác hơn
- Bạn trở thành kỹ sư hiểu cả ứng dụng và hạ tầng
Không phải phóng đại khi nói chất lượng hệ thống tỷ lệ thuận với hiểu biết về bộ nhớ.
14. FAQ
Cuối cùng, đây là phần Hỏi‑Đáp thực tế, bao phủ các câu hỏi thường gặp khi mọi người tìm kiếm “java heap space”.
Điều này bổ sung cho bài viết và giúp nắm bắt rộng hơn các nhu cầu người dùng.
Q1. Sự khác nhau giữa java.lang.OutOfMemoryError: Java heap space và GC overhead limit exceeded là gì?
● java heap space
- Xảy ra khi heap đã bị cạn kiệt thực tế
- Thường do dữ liệu quá lớn, bộ sưu tập phình to, hoặc cấu hình không đủ
● GC overhead limit exceeded
- GC đang làm việc rất mạnh nhưng không thu hồi được gì đáng kể
- Dấu hiệu GC không thể phục hồi do quá nhiều đối tượng còn sống
- Thường cho thấy rò rỉ bộ nhớ hoặc các tham chiếu tồn tại lâu dài
Một mô hình tư duy hữu ích:
heap space = đã vượt qua giới hạn,
GC overhead = ngay trước giới hạn.
Q2. Nếu tôi chỉ tăng heap lên, vấn đề sẽ được giải quyết?
✔ Có thể giúp tạm thời
✘ Không khắc phục nguyên nhân gốc
- Nếu heap thực sự quá nhỏ cho khối lượng công việc → sẽ giúp
- Nếu nguyên nhân là các collection hoặc rò rỉ → sẽ tái diễn
Nếu nguyên nhân là rò rỉ, việc gấp đôi heap chỉ trì hoãn lần OOM tiếp theo.
Q3. Tôi có thể tăng heap của Java bao nhiêu?
● Thông thường: 50%–70% của bộ nhớ vật lý
Bởi vì bạn phải để lại bộ nhớ cho:
- Bộ nhớ native
- Stack của các thread
- Metaspace
- Các worker GC
- Các tiến trình của hệ điều hành
Đặc biệt trong Docker/K8s, thực hành phổ biến là đặt:
-Xmx = 70%–80% của giới hạn container.
Q4. Tại sao Java lại bị OOMKilled trong container (Docker/K8s)?
● Trong nhiều trường hợp, vì -Xmx không được đặt
Docker có thể không luôn truyền giới hạn container một cách sạch sẽ tới Java, vì vậy Java tính kích thước heap dựa trên bộ nhớ của máy chủ → vượt quá giới hạn → bị OOMKilled.
✔ Khắc phục
docker run --memory=1g -e JAVA_OPTS="-Xmx800m"
Q5. Có cách nào dễ dàng để xác định đây có phải là rò rỉ bộ nhớ không?
✔ Nếu các dấu hiệu sau đúng, rất có khả năng là rò rỉ
- Lượng heap sử dụng liên tục tăng theo thời gian hoạt động
- Bộ nhớ hầu như không giảm ngay cả sau Full GC
- Old‑gen tăng theo mô hình “bậc thang”
- OOM xảy ra sau nhiều giờ hoặc nhiều ngày
- Các lần chạy ngắn vẫn ổn
Tuy nhiên, xác nhận cuối cùng cần phân tích heap dump (Eclipse MAT).
Q6. Cài đặt heap trong Eclipse / IntelliJ không được áp dụng
● Nguyên nhân phổ biến
- Bạn chưa chỉnh sửa Run Configuration
- Cài đặt mặc định của IDE đang được ưu tiên
- Một script khởi động khác với
JAVA_OPTSghi đè cài đặt của bạn - Bạn quên khởi động lại tiến trình
Cài đặt IDE khác nhau, vì vậy luôn kiểm tra trường “VM options” trong Run/Debug Configuration.
Q7. Có đúng là Spring Boot tiêu tốn rất nhiều bộ nhớ không?
Có. Spring Boot thường tiêu thụ nhiều bộ nhớ hơn do:
- Cấu hình tự động
- Nhiều Bean
- Tải lớp trong các fat JAR
- Máy chủ web nhúng (Tomcat, v.v.)
So với một chương trình Java thuần, nó có thể dùng thêm khoảng ~200–300 MB trong một số trường hợp.
Q8. Nên dùng GC nào?
Trong hầu hết các trường hợp, G1GC là lựa chọn an toàn mặc định.
● Đề xuất theo loại tải
- Ứng dụng web → G1GC
- Công việc batch yêu cầu thông lượng cao → Parallel GC
- Yêu cầu độ trễ cực thấp → ZGC / Shenandoah
Nếu không có lý do mạnh mẽ, hãy chọn G1GC.
Q9. Làm sao xử lý heap trong môi trường serverless (Cloud Run / Lambda)?
Môi trường serverless có giới hạn bộ nhớ chặt chẽ, vì vậy bạn nên cấu hình heap một cách rõ ràng.
Ví dụ (Java 11):
-XX:MaxRAMPercentage=70
Cũng lưu ý rằng bộ nhớ có thể tăng đột biến trong quá trình khởi động lạnh, vì vậy hãy để dư địa trong cấu hình heap.
Q10. Làm sao ngăn ngừa các vấn đề về heap Java tái diễn?
Nếu bạn tuân thủ chặt chẽ ba quy tắc sau, tần suất tái diễn sẽ giảm đáng kể:
✔ Đặt heap một cách rõ ràng
✔ Xử lý dữ liệu lớn bằng streaming
✔ Thường xuyên xem xét log GC và heap dump
Summary: Sử dụng FAQ để Xóa Tan Thắc Mắc và Áp Dụng Các Biện Pháp Kiểm Soát Bộ Nhớ Thực Tiễn
FAQ này đã bao quát các câu hỏi thường gặp được tìm kiếm về “java heap space” kèm theo các câu trả lời thực tiễn.
Kết hợp với bài viết chính, nó sẽ giúp bạn tự tin hơn trong việc xử lý các vấn đề bộ nhớ Java và cải thiện đáng kể độ ổn định của hệ thống trong môi trường production.


