This project demonstrates the difference in behavior for a Java application experiencing an Out-of-Memory (OOM) condition in a container environment, specifically simulating the issues seen when migrating from cgroup v1 to cgroup v2 on platforms like EKS.
- The "Bad" Scenario (cgroup v1 simulation): An older or misconfigured JVM is not aware of the container's memory limits. It attempts to use more memory than allocated, causing the container runtime to abruptly kill the process (
OOMKilled). The application has no chance to handle the error gracefully. - The "Good" Scenario (cgroup v2): A modern, container-aware JVM correctly detects the memory limits. It respects these limits and throws a catchable
java.lang.OutOfMemoryErrorwhen the heap is exhausted, allowing the application to shut down gracefully.
This project is pre-configured to demonstrate the "bad" scenario.
src/main/java/com/example/oom/OomApp.java: A simple Java app that continuously allocates 1MB chunks of memory to the heap.- It prints the JVM's max heap size on startup.
- It has a
try-catchblock to gracefully handleOutOfMemoryError.
Dockerfile: Builds the Java app and configures the runtime.- It uses
ENV JAVA_OPTS="-XX:-UseContainerSupport ..."to explicitly disable the JVM's container awareness. This is the key to simulating the cgroup v1 problem. The JVM will now base its heap size on the node's memory, not the container's limit.
- It uses
k8s/deployment.yaml: Deploys the application to Kubernetes with a256Mimemory limit.
-
Build and Push the Docker Image
# Navigate to the project root directory cd oom-java-app # Build the image (replace with your registry if needed) docker build -t alliot/oom-jdk11:latest . # Push the image docker push alliot/oom-jdk11:latest
-
Deploy to Kubernetes
kubectl apply -f k8s/deployment.yaml
-
Observe the Failure
# Find your pod name POD_NAME=$(kubectl get pods -l app=oom-app -o jsonpath='{.items[0].metadata.name}') # Watch the logs kubectl logs -f $POD_NAME
You will see:
- The application starts.
- The
JVM Max Heap Size (Xmx)will be a large value (e.g., >1000 MB), ignoring the256Milimit from the deployment YAML. - The application will log memory allocations.
- The logs will suddenly stop after allocating around 256 MB. The graceful
OutOfMemoryErrormessage is never printed.
-
Confirm the
OOMKilledStatuskubectl describe pod $POD_NAMEIn the output, you will see that the pod's
StateisTerminatedwithReason: OOMKilled.
Now, let's reconfigure the JVM to be container-aware, as it should be in a cgroup v2 environment.
-
Modify the
Dockerfile- Open the
Dockerfile. - Comment out or change the
ENV JAVA_OPTSline to enable container support:(Note: For modern JVMs,# ENV JAVA_OPTS="-XX:-UseContainerSupport -XX:+UnlockDiagnosticVMOptions -Xlog:os+container=debug" ENV JAVA_OPTS="-XX:+UseContainerSupport"
+UseContainerSupportis the default, so you could also just remove theENVline entirely).
- Open the
-
Rebuild, Push, and Redeploy
# Rebuild and push the image with the same tag docker build -t alliot/oom-jdk11:latest . docker push alliot/oom-jdk11:latest # Delete the old deployment to ensure the new image is pulled kubectl delete deployment oom-app-deployment # Re-apply the deployment kubectl apply -f k8s/deployment.yaml
-
Observe the Success
# Find the new pod name POD_NAME=$(kubectl get pods -l app=oom-app -o jsonpath='{.items[0].metadata.name}') # Watch the logs kubectl logs -f $POD_NAME
You will now see:
- The
JVM Max Heap Size (Xmx)is a much smaller, more reasonable number (e.g., ~170 MB), as it respects the256Micontainer limit. - The application allocates memory until it hits the JVM's heap limit.
- The
SUCCESS: JVM gracefully caught OutOfMemoryError!message is printed to the console. - The pod exits with a
Completedstatus, notOOMKilled.
- The