Overview
When working on a project, you may encounter OOM or OOMKilled. Especially in a Kubernetes environment, it is common to face situations where "the Pod was restarted due to OOMKilled."
OOMKilled may seem like a simple server restart, but if left unaddressed without identifying the cause, it can repeatedly occur in high service load situations, leading to critical failures.
This document summarizes the investigation and review of what OOMKilled is, why it occurs, and how to analyze and resolve it based on encounters during the project's progress.
Difference between OOM and OOMKilled
Both OOM and OOMKilled are issues related to memory allocation. However, they differ in terms of the entity that triggers them and their operational mechanisms.
OOM (Out Of Memory Error) occurs within the JVM. When the heap memory is full and no more objects can be allocated, the JVM throws a java.lang.OutOfMemoryError and terminates the process. In other words, the JVM recognizes its own limit and throws an error.
OOMKilled occurs outside of the JVM, in container environments like Kubernetes. The moment the memory limit set for a container is exceeded, Kubernetes forcefully terminates that process. Since it is killed externally before the JVM can recognize the error, it is harder to identify the cause as no separate error logs are left, unlike OOM.
|
Classification |
OOM |
OOMKilled |
|---|---|---|
|
Entity that triggers the occurrence |
Inside JVM |
Kubernetes (cgroup) |
|
Cause |
Heap memory exhaustion |
Container limit exceeded |
|
Error log |
OutOfMemoryError log retained |
Forced termination without log |
|
Exit code |
1 |
137 |
|
Target memory |
Heap area |
Total of Heap + Non-Heap + Off-Heap |
When an OOMKilled occurs, the Pod is automatically restarted according to Kubernetes' restart policy. If it happens during times of low traffic, it may not be a big issue. However, if OOMKilled occurs under heavy load conditions, the situation can become serious. Depending on the Pod's status, the service may be interrupted during restart, and if traffic spikes immediately after the restart, memory can quickly fill up, resulting in repeated OOMKilled incidents. Ultimately, as CPU and memory loads increase, it can lead to critical failures for the entire service.
Distinction of memory
When referring to memory related to OOMKilled, it is important to understand not only Heap memory allocation but also Container, JVM, and other more specific memory details.
Container memory
One important factor to consider in a Kubernetes environment is the range of memory settings. The memory limit set for a container applies not just to the JVM but to the entire container.
In other words, it is essential to recognize the relationship where the container memory is greater than the JVM memory.
Container 메모리
│
├── JVM 프로세스 메모리
│ ├── Heap
│ ├── Non-Heap
│ └── Off-Heap (Direct Buffer)
│
├── OS 및 시스템 라이브러리
└── 사이드카 Container (Istio sidecar 등) 등 기타…
JVM memory
JVM memory is broadly categorized into three areas.
Heap area
This is the space where objects created by Java applications are stored. It is also the area targeted by GC (Garbage Collection).
|
Metrics |
Description |
|---|---|
|
Heap Used |
Memory currently in use |
|
Heap Committed |
Memory that the JVM has pre-allocated from the OS. It is not immediately released after GC. |
|
Heap Max |
The maximum heap size available for the JVM. |
The relationship between the three metrics is always Used ≤ Committed ≤ Max, and Committed is cleared during a Full GC.
Non-Heap Area
An area for JVM's own operation, not subject to GC.
|
Area |
Description |
|---|---|
|
Metaspace |
Stores class metadata and static variables. |
Metaspace can theoretically grow indefinitely if -XX:MaxMetaspaceSize is not set.
Off-Heap Area (Direct Buffer)
An area that is neither Heap nor Non-Heap, memory allocated directly by the JVM from the OS. It is primarily used for NIO-based file processing or network I/O.
Since it is not subject to GC, it can accumulate if not explicitly freed, and can grow indefinitely if a limit is not set with -XX:MaxDirectMemorySize.
Relationship between Container Memory and JVM Memory
It may seem fine if the JVM's Heap size is set lower than the Container Limit. However, since not only Heap but also Non-Heap and Off-Heap are managed together within the Container Limit, all three areas must be carefully calculated by summing them up. Especially, services that process many files depending on the system's nature may use Off-Heap a lot, while services that load many classes may use Non-Heap more, so it is essential to understand one's system characteristics thoroughly before designing memory, rather than just looking at Heap size.
In a system that uses 200MB for Non-Heap, 100MB for Off-Heap, and 50MB for other JVM-related memory, if the Container Limit is set to 1Gi and -XX:MaxRAMPercentage=80, the following situation occurs.
Heap Max (80%) : 819MB ← JVM이 사용 가능하다고 인식하는 Heap 한도
Non-Heap : 200MB
Off-Heap : 100MB
JVM 외 기타 : 50MB
─────────────────────────
Heap 외 합계 : 350MB
실질적으로 Heap에 허용되는 메모리 : 1,024MB - 350MB = 674MB
The JVM recognizes it can use up to 819MB of Heap and continues to allocate memory without a Full GC. However, since Kubernetes sums up Heap + Non-Heap + Off-Heap + others and compares it to the Container Limit, the moment Heap usage exceeds 674MB, it surpasses the Container Limit of 1Gi, and Kubernetes forcibly terminates the process before the JVM realizes it. This is what is known as OOMKilled.
JVM Settings
Below is a brief definition of the JVM options related to OOMKilled.
MaxRAMPercentage
-
Sets the maximum size of the heap as a percentage of the container limit. Setting it too low can waste memory, and setting it too high can lead to exceeding the container limit when combined with Non-Heap and Off-Heap consumption.
MaxMetaspaceSize
-
Limits the maximum size of Metaspace in the Non-Heap area. If not set, Metaspace can theoretically increase without limit. It is recommended to set a cap with some buffer based on current usage.
MaxDirectMemorySize
-
Limits the maximum size of Off-Heap (Direct Buffer). In services that heavily utilize NIO-based file processing or network I/O, if not set, the Direct Buffer can grow without limit and exceed the container limit. Setting this option also provides the additional benefit of logging OutOfMemoryError: Direct buffer memory when the limit is exceeded, making it easier to identify the cause.
OOMKilled server analysis case
Below is a summary of the memory status of the service where OOMKilled occurred, analyzed by AI based on the input. The OOMKilled issue was resolved after applying the final JVM option modifications.
Original notion link :
https://app.notion.com/p/vizend/Pod-OOMKilled-38b35bc54c13806699e8e3ae329bf38f
Memory status analysis
1. Container resource settings
|
Item |
Value |
|---|---|
|
Memory Limit |
1Gi (1,073 MB) |
|
Memory Request |
256Mi |
|
QoS Class |
Burstable |
2. JVM Memory Status (Current Time)
|
Area |
Value |
MB Conversion |
|---|---|---|
|
Heap Used |
376,595,248 bytes |
~359 MB |
|
Heap Committed |
512,229,376 bytes |
~488 MB |
|
Heap Max |
859,308,032 bytes |
~819 MB |
|
Non-Heap Used |
210,018,744 bytes |
~200 MB |
|
└ Metaspace |
127,576,696 bytes |
~122 MB |
3. Core Issue Analysis
Container Limit : 1,073 MB (1Gi)
────────────────────────────────────────
Heap Max (JVM 설정) : 819 MB
Non-Heap Used (현재) : 200 MB
────────────────────────────────────────
합산 최대 사용 가능 : 1,019 MB ← 한계에 매우 근접
나머지 여유 : ~54 MB ← 위험 수준
Heap Max + Non-Heap of JVM alone accounts for about 95% of the Container Limit.
When adding the internal overhead of the JVM (Thread Stack, GC metadata, etc.), it exceeds 1Gi, resulting in OOMKilled.
4. Check GC Method
The Actuator response shows PS Eden Space, PS Old Gen, PS Survivor Space.
We recommend using G1GC.
5. Cause Summary
|
Cause |
Description |
|---|---|
|
Excessive Heap Max |
Exceeding limit just before 1,019MB with 819MB Heap + 200MB Non-Heap |
|
Inappropriate GC Method |
Parallel GC → Insufficient recognition of container memory |
|
JVM Option Not Set |
Assuming lack of container recognition options like -XX:MaxRAMPercentage |
|
QoS Burstable |
Request ≠ Limit → No memory guarantee, OOM priority |
MaxRAMPercentage Simulation Result
Compared and analyzed MaxRAMPercentage from the current 80% to 60%.
Heap Usage
|
Item |
80% |
70% |
65% |
60% |
|---|---|---|---|---|
|
Container Limit |
1,073MB |
1,073MB |
1,073MB |
1,073MB |
|
Heap Max |
858MB |
~751MB |
~698MB |
~644MB |
|
Non-Heap Measurement |
~200MB |
~200MB |
~200MB |
~200MB |
|
Overhead including Thread Stack |
~50~80MB |
~50~80MB |
~50~80MB |
~50~80MB |
|
Total Estimate |
~1,108~1,131MB |
~1,001~1,031MB |
~948~978MB |
~894MB |
|
Free |
~42~72MB |
~95~125MB |
~179MB |
Comparison by option
|
Settings |
Heap Max |
Free |
Evaluation |
Comparison |
|---|---|---|---|---|
|
Current 80% |
~858MB |
~15MB |
Risk |
Issues when using Non-Heap, Off-Heap additional resources |
|
70% |
~751MB |
~42~72MB |
Unstable |
Increase Container Limit with G1GC (1.5Gi) |
|
60% |
~644MB |
~149~179MB |
Recommended |
Apply G1GC |
|
50% |
~536MB |
~257MB |
Safe but Heap may be insufficient |
Exclude from review |
-
The safest approach is to set it to 60% and monitor it, raising it if necessary.
-
Non-Heap or Off-Heap 200MB is not a fixed value and can cause OOMKilled under load situations such as additional file uploads.
Recommended JVM options (final)
# 현재
-XX:InitialRAMPercentage=25
-XX:MaxRAMPercentage=80
-XX:+UseParallelGC
# 권장
-XX:InitialRAMPercentage=25
-XX:MaxRAMPercentage=60 # Heap ~644MB로 제한
-XX:+UseG1GC # Container 친화적 GC
-XX:MaxMetaspaceSize=192m # Metaspace 상한 설정 (현재 122MB 사용 중)
-XX:MaxDirectMemorySize=140m # Direct Buffer 상한 설정 (현재 108MB 사용 중)
-
Heap Max: ~644MB
-
Non-Heap: ~200MB
-
Total: ~844MB → Provide a buffer within the Container Limit of 1Gi
sby