<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: BaoKhang</title>
    <description>The latest articles on DEV Community by BaoKhang (@baokhang83).</description>
    <link>https://dev.to/baokhang83</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4035271%2Fab34f96d-b419-4c94-b4f1-08b1ff036e9c.png</url>
      <title>DEV Community: BaoKhang</title>
      <link>https://dev.to/baokhang83</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/baokhang83"/>
    <language>en</language>
    <item>
      <title>Why Kubernetes In-Place Memory Resize Gets Stuck "In Progress" on JVM Pods</title>
      <dc:creator>BaoKhang</dc:creator>
      <pubDate>Sun, 08 Nov 2026 19:02:37 +0000</pubDate>
      <link>https://dev.to/baokhang83/why-kubernetes-in-place-memory-resize-gets-stuck-in-progress-on-jvm-pods-3j4l</link>
      <guid>https://dev.to/baokhang83/why-kubernetes-in-place-memory-resize-gets-stuck-in-progress-on-jvm-pods-3j4l</guid>
      <description>&lt;p&gt;You lowered a JVM pod's memory with an in-place resize, and the pod now shows &lt;code&gt;PodResizeInProgress&lt;/code&gt;, an &lt;code&gt;Error&lt;/code&gt; reason, and an old memory limit that never changes. The kubelet is not broken. It refuses to lower a memory limit below the container's current usage, and a JVM keeps its committed heap even after the objects in it are garbage. The resize completes as soon as the JVM gives the memory back, which I confirmed on a Kubernetes 1.35.8 cluster. Below are the real outputs, the fix, and how long it took.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Cause:&lt;/strong&gt; the kubelet will not apply a memory-limit decrease while the container's current usage is above the new limit. A JVM's usage stays high after garbage collection because it keeps the heap committed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; condition &lt;code&gt;PodResizeInProgress=True&lt;/code&gt; with reason &lt;code&gt;Error&lt;/code&gt;. &lt;code&gt;containerStatuses[].allocatedResources&lt;/code&gt; shows the new value, but &lt;code&gt;containerStatuses[].resources&lt;/code&gt; still shows the old one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It retries.&lt;/strong&gt; The kubelet retried about every 11 to 15 seconds. The pod was not OOM-killed and did not restart. The check protected the JVM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; make the JVM release memory, then let the resize finish. Releasing after the patch finished the resize on the next retry (about 20 s in my run). Releasing before the patch finished it 2 seconds after the patch, and the error never appeared.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PodResizePending&lt;/code&gt; is a different problem.&lt;/strong&gt; I saw it only when I asked for more memory than the node has (&lt;code&gt;Infeasible&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;

&lt;p&gt;I ran a JVM pod on a single-node kind cluster, Kubernetes v1.35.8. The pod has Guaranteed QoS with a 900Mi memory request and limit. The JVM (Temurin 21, ZGC, &lt;code&gt;-Xmx600m&lt;/code&gt;) allocates 300 MB, holds it for 10 seconds, then drops all of it and idles. Inside the container, &lt;code&gt;memory.current&lt;/code&gt; read 417 MB.&lt;/p&gt;

&lt;p&gt;I shrank it to 300Mi through the &lt;code&gt;resize&lt;/code&gt; subresource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch pod jvm &lt;span class="nt"&gt;--subresource&lt;/span&gt; resize &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"spec":{"containers":[{"name":"app","resources":{"requests":{"memory":"300Mi"},"limits":{"memory":"300Mi"}}}]}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few minutes later, &lt;code&gt;kubectl get pod jvm -o yaml&lt;/code&gt; (trimmed) showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;cannot&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;decrease&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;limits:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;[attempting&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;set&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pod&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(314572800)&lt;/span&gt;
      &lt;span class="s"&gt;below&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;current&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(435347456),&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;attempting&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;to&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;set&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;container&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;memory&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;limit&lt;/span&gt;
      &lt;span class="s"&gt;(314572800)&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;below&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;current&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;usage&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;(435347456)]'&lt;/span&gt;
    &lt;span class="na"&gt;observedGeneration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;
    &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Error&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;True"&lt;/span&gt;
    &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PodResizeInProgress&lt;/span&gt;
  &lt;span class="na"&gt;containerStatuses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;allocatedResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;500m&lt;/span&gt;
      &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;300Mi&lt;/span&gt;        &lt;span class="c1"&gt;# what the kubelet accepted&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;500m&lt;/span&gt;
        &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;900Mi&lt;/span&gt;      &lt;span class="c1"&gt;# what is actually applied&lt;/span&gt;
    &lt;span class="na"&gt;restartCount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At +10 s the condition was &lt;code&gt;PodResizeInProgress=True&lt;/code&gt; with no reason. By +31 s the reason was &lt;code&gt;Error&lt;/code&gt;. The cgroup never changed: &lt;code&gt;memory.max&lt;/code&gt; stayed at 900 MB, and &lt;code&gt;memory.current&lt;/code&gt; stayed at 417 MB for the whole minute I watched.&lt;/p&gt;

&lt;p&gt;The events show a repeating error, and the kubelet log says why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Normal   ResizeStarted   Pod resize started: {... "memory":"300Mi" ... "generation":2}
Warning  ResizeError     Pod resize error: {... "error":"cannot decrease memory limits: [attempting to set pod memory limit (314572800) below current usage (435347456) ...]"}
Warning  ResizeError     (repeats)

kubelet: "Allocated pod resize is not currently feasible" err="cannot decrease memory limits: ..."
kubelet: ... ResizePodInPlaceError ... (retries at 18:50:18, :33, :47, :58, 18:51:13, :25)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That matches what the Kubernetes docs say: before decreasing a container's memory limit, if memory usage exceeds the requested limit, the resize is skipped and the status remains in an "In Progress" state. They call this best-effort because usage can spike right after the check. What the docs don't show is the &lt;code&gt;Error&lt;/code&gt; reason and the repeating &lt;code&gt;ResizeError&lt;/code&gt; events, which look alarming but are the kubelet waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a JVM does this
&lt;/h2&gt;

&lt;p&gt;The kubelet compares the container's current memory usage with the new limit. A JVM's usage is dominated by committed heap, and the JVM does not return committed heap just because the objects in it are gone.&lt;/p&gt;

&lt;p&gt;In my test the JVM sat idle for more than a minute with &lt;code&gt;ZUncommitDelay=1&lt;/code&gt; and gave nothing back. The likely reason is that no garbage-collection cycle ran after the data was dropped, so there was nothing to trigger an uncommit. I did not confirm that, and the behavior depends on the collector. Don't count on a JVM to release by itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;PodResizeInProgress&lt;/code&gt; vs &lt;code&gt;PodResizePending&lt;/code&gt;
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;What it means&lt;/th&gt;
&lt;th&gt;What I saw&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PodResizeInProgress&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The kubelet accepted the new resources and is applying them.&lt;/td&gt;
&lt;td&gt;With &lt;code&gt;Error&lt;/code&gt; and "below current usage", the kubelet is retrying a memory decrease.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PodResizePending&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The resize can't be started yet.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reason=Infeasible&lt;/code&gt;, message &lt;code&gt;Node didn't have enough capacity: memory, requested: 68719476736, capacity: 8321712128&lt;/code&gt;, when I asked for 64Gi on an 8 GB node.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you see &lt;code&gt;Infeasible&lt;/code&gt; or &lt;code&gt;Deferred&lt;/code&gt;, the problem is node capacity. If you see &lt;code&gt;InProgress&lt;/code&gt; plus "below current usage", the problem is the container's own memory, and the fix is below.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: release memory from inside the JVM
&lt;/h2&gt;

&lt;p&gt;On ZGC, lower the soft ceiling and force a collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;jvm &lt;span class="nt"&gt;--&lt;/span&gt; jcmd 1 VM.set_flag SoftMaxHeapSize 104857600
kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;jvm &lt;span class="nt"&gt;--&lt;/span&gt; jcmd 1 GC.run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Releasing while the resize was pending.&lt;/strong&gt; I ran the two commands against the stuck pod. &lt;code&gt;memory.current&lt;/code&gt; fell from 417 MB to 96 MB. The kubelet applied the resize on its next retry, 20 seconds after I started the release. &lt;code&gt;memory.max&lt;/code&gt; became 300 MB, &lt;code&gt;restartCount&lt;/code&gt; stayed 0, the condition cleared, and a &lt;code&gt;ResizeCompleted&lt;/code&gt; event appeared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Releasing before the patch.&lt;/strong&gt; I recreated the pod, released first (424 MB → 101 MB), then patched. The resize applied 2 seconds after the patch. For this pod, the events were &lt;code&gt;ResizeStarted&lt;/code&gt; and &lt;code&gt;ResizeCompleted&lt;/code&gt; only. The &lt;code&gt;Error&lt;/code&gt; condition never appeared.&lt;/p&gt;

&lt;p&gt;Releasing first is cleaner: no error condition, no wait for a retry. But if you are already stuck, you don't need to re-patch. Free the memory and the kubelet finishes the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  When it stays stuck
&lt;/h2&gt;

&lt;p&gt;The resize stays "In Progress" if the JVM can't get below the new limit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live data is bigger than the new limit.&lt;/strong&gt; If the heap holds more live objects than the limit, I'd expect no collection to free enough, and the resize to stay pending. I didn't test this case. Pick a limit that fits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ceiling is fixed.&lt;/strong&gt; &lt;code&gt;-Xmx&lt;/code&gt; can't change on a running JVM. The new limit must be above &lt;code&gt;-Xmx&lt;/code&gt; plus metaspace, code cache, thread stacks and direct buffers. If it isn't, you need a restart, which is what &lt;code&gt;resizePolicy: RestartContainer&lt;/code&gt; is for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory outside the heap.&lt;/strong&gt; &lt;code&gt;SoftMaxHeapSize&lt;/code&gt; only moves the heap. Native memory and direct buffers stay.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A debugging checklist
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Which condition? InProgress or Pending, and the reason&lt;/span&gt;
kubectl get pod &amp;lt;pod&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{range .status.conditions[?(@.type=="PodResizeInProgress")]}{.reason}: {.message}{"\n"}{end}'&lt;/span&gt;
kubectl get pod &amp;lt;pod&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{range .status.conditions[?(@.type=="PodResizePending")]}{.reason}: {.message}{"\n"}{end}'&lt;/span&gt;

&lt;span class="c"&gt;# 2. What is accepted vs actually applied&lt;/span&gt;
kubectl get pod &amp;lt;pod&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'allocated={.status.containerStatuses[0].allocatedResources.memory} applied={.status.containerStatuses[0].resources.limits.memory}{"\n"}'&lt;/span&gt;

&lt;span class="c"&gt;# 3. What the container is really using&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &amp;lt;pod&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'cat /sys/fs/cgroup/memory.current /sys/fs/cgroup/memory.max'&lt;/span&gt;

&lt;span class="c"&gt;# 4. The events&lt;/span&gt;
kubectl get events &lt;span class="nt"&gt;--field-selector&lt;/span&gt; involvedObject.name&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;pod&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;memory.current&lt;/code&gt; is above the new limit, the JVM is the thing to fix.&lt;/p&gt;

&lt;p&gt;One gotcha from my own test: use the default strategic merge patch, as in the command above. With &lt;code&gt;--type merge&lt;/code&gt;, the whole &lt;code&gt;containers&lt;/code&gt; list is replaced, and the API server rejects it with "resource requests cannot be removed".&lt;/p&gt;

&lt;h2&gt;
  
  
  What I didn't test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Collectors other than ZGC in a cluster. I tested ZGC, Shenandoah and G1 in a container separately, in &lt;a href="https://baokhang.hashnode.dev/g1-release-memory-kubernetes-in-place-pod-resize" rel="noopener noreferrer"&gt;another article&lt;/a&gt;, but not through a kubelet.&lt;/li&gt;
&lt;li&gt;A multi-container pod, or any version other than v1.35.8.&lt;/li&gt;
&lt;li&gt;Whether the retry interval changes under load, or what happens if usage spikes right after the kubelet's check.&lt;/li&gt;
&lt;li&gt;How long a release takes on a large heap under real traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;You need Docker, &lt;code&gt;kind&lt;/code&gt; and &lt;code&gt;kubectl&lt;/code&gt;. &lt;code&gt;Hold.java&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Hold&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;];&lt;/span&gt; &lt;span class="nc"&gt;Arrays&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;,(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"allocated 300MB"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"released refs"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;600000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pod.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jvm&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;app&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;eclipse-temurin:21-jdk&lt;/span&gt;
    &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;IfNotPresent&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;java"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-XX:+UseZGC"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-XX:ZUncommitDelay=1"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-Xmx600m"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/w/Hold.java"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;w&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/w&lt;/span&gt;&lt;span class="pi"&gt;}]&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;requests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;500m&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;900Mi&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
      &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;500m&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;900Mi&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="na"&gt;resizePolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;resourceName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NotRequired&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;resourceName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;cpu&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NotRequired&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;w&lt;/span&gt;
    &lt;span class="na"&gt;configMap&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;hold&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kind create cluster &lt;span class="nt"&gt;--name&lt;/span&gt; resize-test &lt;span class="nt"&gt;--image&lt;/span&gt; kindest/node:v1.35.8
kubectl create configmap hold &lt;span class="nt"&gt;--from-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Hold.java
kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; pod.yaml
kubectl &lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nt"&gt;--for&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;condition&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Ready pod/jvm &lt;span class="nt"&gt;--timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;240s
&lt;span class="c"&gt;# wait ~15 s until the JVM logs "released refs", then:&lt;/span&gt;
kubectl patch pod jvm &lt;span class="nt"&gt;--subresource&lt;/span&gt; resize &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'{"spec":{"containers":[{"name":"app","resources":{"requests":{"memory":"300Mi"},"limits":{"memory":"300Mi"}}}]}}'&lt;/span&gt;
kubectl get pod jvm &lt;span class="nt"&gt;-o&lt;/span&gt; yaml        &lt;span class="c"&gt;# PodResizeInProgress, reason Error&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;jvm &lt;span class="nt"&gt;--&lt;/span&gt; jcmd 1 VM.set_flag SoftMaxHeapSize 104857600
kubectl &lt;span class="nb"&gt;exec &lt;/span&gt;jvm &lt;span class="nt"&gt;--&lt;/span&gt; jcmd 1 GC.run  &lt;span class="c"&gt;# the kubelet finishes on its next retry&lt;/span&gt;
kind delete cluster &lt;span class="nt"&gt;--name&lt;/span&gt; resize-test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first image pull took about 90 seconds on my machine. The megabyte numbers will differ a little on yours.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why is my Kubernetes pod resize stuck in progress?
&lt;/h3&gt;

&lt;p&gt;The kubelet will not lower a memory limit below the container's current usage, and it keeps retrying. Check &lt;code&gt;kubectl get pod &amp;lt;pod&amp;gt; -o yaml&lt;/code&gt; for &lt;code&gt;PodResizeInProgress&lt;/code&gt; with a message saying "below current usage". Free memory inside the container and the resize finishes.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the difference between PodResizePending and PodResizeInProgress?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;PodResizePending&lt;/code&gt; means the resize can't start yet. I saw it with &lt;code&gt;reason=Infeasible&lt;/code&gt; when the request exceeded the node's capacity. &lt;code&gt;PodResizeInProgress&lt;/code&gt; means the kubelet accepted the resize and is applying it. For a memory decrease it stays that way while usage is above the new limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why won't the kubelet complete an in-place memory decrease for my Java pod?
&lt;/h3&gt;

&lt;p&gt;A JVM keeps its committed heap after garbage collection, so the container's usage stays above the new limit. On ZGC or Shenandoah, lower &lt;code&gt;SoftMaxHeapSize&lt;/code&gt; and run &lt;code&gt;GC.run&lt;/code&gt;. On G1, use &lt;code&gt;GC.run&lt;/code&gt; or &lt;code&gt;G1PeriodicGCInterval&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will the pod be OOM-killed while the resize is stuck?
&lt;/h3&gt;

&lt;p&gt;Not in my test. The pod kept running at 900Mi, with 0 restarts, until I freed memory. The Kubernetes docs say the check is best-effort, with no guarantees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I need to patch again after freeing memory?
&lt;/h3&gt;

&lt;p&gt;No. The kubelet retries on its own. In my run it completed 20 seconds after I released memory, with no new patch.&lt;/p&gt;

&lt;h3&gt;
  
  
  How long does the kubelet take to apply the resize after the JVM releases memory?
&lt;/h3&gt;

&lt;p&gt;20 seconds in the run where I released after the patch (the kubelet was retrying about every 11 to 15 seconds). If you release before the patch, it applied 2 seconds after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Warden fits
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/baokhang83/mnemo-jvm-warden" rel="noopener noreferrer"&gt;mnemo-jvm-warden&lt;/a&gt;, an open-source Kubernetes controller and sidecar that does the release-then-resize order. The sidecar lowers the soft heap ceiling where the collector supports it, forces &lt;code&gt;GC.run&lt;/code&gt;, and checks that the process's resident memory dropped. Only then does the resize reach the kubelet. That order is the second case in this article, where the error never appears. I haven't tested that exact path in the cluster in this article.&lt;/p&gt;

&lt;p&gt;You don't need a tool for the fix: the two &lt;code&gt;jcmd&lt;/code&gt; commands above work on their own.&lt;/p&gt;

</description>
      <category>kubectl</category>
      <category>jvm</category>
      <category>java</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>Your G1 Pod Ignores SoftMaxHeapSize. Here's What Works Instead</title>
      <dc:creator>BaoKhang</dc:creator>
      <pubDate>Sun, 08 Nov 2026 18:39:14 +0000</pubDate>
      <link>https://dev.to/baokhang83/your-g1-pod-ignores-softmaxheapsize-heres-what-works-instead-54k4</link>
      <guid>https://dev.to/baokhang83/your-g1-pod-ignores-softmaxheapsize-heres-what-works-instead-54k4</guid>
      <description>&lt;p&gt;G1GC vs. ZGC vs. Shenandoah for Kubernetes 1.35 In-Place Pod Resize&lt;/p&gt;

&lt;p&gt;When a pod shrinks in place, the JVM has to hand memory back to the operating system before the new limit applies. I tested how G1, ZGC and Shenandoah do that in a Linux container. All three released memory quickly in my test, but they differ in what triggers the release and in what &lt;code&gt;SoftMaxHeapSize&lt;/code&gt; does. G1 accepts that flag and silently ignores it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ZGC and Shenandoah:&lt;/strong&gt; &lt;code&gt;SoftMaxHeapSize&lt;/code&gt; works at runtime. Lower it, run &lt;code&gt;GC.run&lt;/code&gt;, and memory drops.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;G1:&lt;/strong&gt; &lt;code&gt;SoftMaxHeapSize&lt;/code&gt; is accepted by &lt;code&gt;jcmd&lt;/code&gt; and has no effect. By default nothing returns memory. Two things did, in my test: an explicit &lt;code&gt;GC.run&lt;/code&gt;, and &lt;code&gt;G1PeriodicGCInterval&lt;/code&gt; set to a few seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release speed:&lt;/strong&gt; I measured no real difference. Each collector released a 300 MB working set within about 1 to 7 seconds. I did not measure large heaps or busy applications, which is where I'd expect differences, so treat "G1 is slower" as unproven.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The real G1 difference&lt;/strong&gt; is the missing ceiling. Nothing holds G1's heap below a target after you release, so you size &lt;code&gt;-Xmx&lt;/code&gt; for the peak and accept that it can grow back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lowering the limit first&lt;/strong&gt; killed all three (OOMKilled, exit 137). Release first, then resize.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What each collector gives you
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ZGC&lt;/th&gt;
&lt;th&gt;Shenandoah&lt;/th&gt;
&lt;th&gt;G1&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SoftMaxHeapSize&lt;/code&gt; changes behavior&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt; (flag accepted, no effect)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Returns memory by default&lt;/td&gt;
&lt;td&gt;After &lt;code&gt;ZUncommitDelay&lt;/code&gt; (300 s by default)&lt;/td&gt;
&lt;td&gt;After &lt;code&gt;ShenandoahUncommitDelay&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;No&lt;/strong&gt;, not without a trigger&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What triggers a release&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;SoftMaxHeapSize&lt;/code&gt; + &lt;code&gt;GC.run&lt;/code&gt;, or the uncommit delay&lt;/td&gt;
&lt;td&gt;Same&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GC.run&lt;/code&gt;, or &lt;code&gt;G1PeriodicGCInterval&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pause cost of the trigger&lt;/td&gt;
&lt;td&gt;Not measured&lt;/td&gt;
&lt;td&gt;Not measured&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GC.run&lt;/code&gt; is a full, stop-the-world GC. Not measured at scale.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;The setup is the same as in my &lt;a href="https://baokhang.hashnode.dev/jvm-resizepolicy-restartcontainer-not-required-zgc-shenandoah" rel="noopener noreferrer"&gt;earlier test of &lt;code&gt;RestartContainer&lt;/code&gt;&lt;/a&gt;: Eclipse Temurin 21.0.13 in a Linux container on Docker Desktop, cgroup v2, and a small program that allocates and touches 300 MB, holds it for 10 seconds, then drops all of it and idles. &lt;code&gt;docker update --memory=300m&lt;/code&gt; lowers the cgroup limit on the running container in place. That is the kernel mechanism a pod resize uses, but there is no kubelet.&lt;/p&gt;

&lt;p&gt;For each collector, I ran two cases. &lt;strong&gt;Naive:&lt;/strong&gt; lower the limit straight away. &lt;strong&gt;Ordered:&lt;/strong&gt; set &lt;code&gt;SoftMaxHeapSize=100MB&lt;/code&gt;, run &lt;code&gt;GC.run&lt;/code&gt;, wait until &lt;code&gt;memory.current&lt;/code&gt; falls, then lower the limit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Collector&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;memory.current&lt;/code&gt; before&lt;/th&gt;
&lt;th&gt;Naive: lower limit to 300 MB&lt;/th&gt;
&lt;th&gt;Ordered: release first, then lower&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ZGC (&lt;code&gt;-Xmx600m&lt;/code&gt;, limit 900 MB)&lt;/td&gt;
&lt;td&gt;403 MB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;OOMKilled&lt;/strong&gt;, exit 137&lt;/td&gt;
&lt;td&gt;404 → &lt;strong&gt;89 MB&lt;/strong&gt; in 3 s, then &lt;strong&gt;still running&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shenandoah (&lt;code&gt;-Xmx500m&lt;/code&gt;, limit 700 MB)&lt;/td&gt;
&lt;td&gt;372 MB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;OOMKilled&lt;/strong&gt;, exit 137&lt;/td&gt;
&lt;td&gt;361 → &lt;strong&gt;64 MB&lt;/strong&gt; in 1 s, then &lt;strong&gt;still running&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G1 (&lt;code&gt;-Xmx600m&lt;/code&gt;, 4 MB regions, limit 900 MB)&lt;/td&gt;
&lt;td&gt;456 MB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;OOMKilled&lt;/strong&gt;, exit 137&lt;/td&gt;
&lt;td&gt;463 → &lt;strong&gt;143 MB&lt;/strong&gt; in 2 s, then &lt;strong&gt;still running&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;G1 passes the ordered case, but I want to be exact about why. In that run I set &lt;code&gt;SoftMaxHeapSize&lt;/code&gt; too, and it did nothing. The explicit &lt;code&gt;GC.run&lt;/code&gt; did the work.&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually released memory on G1
&lt;/h3&gt;

&lt;p&gt;I tested each G1 lever on its own, starting from about 460 to 530 MB in use:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Lever on G1&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Nothing (default), wait 25 s&lt;/td&gt;
&lt;td&gt;459 → 459 MB. &lt;strong&gt;No release.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;SoftMaxHeapSize=100MB&lt;/code&gt; only, wait 20 s&lt;/td&gt;
&lt;td&gt;535 → 535 MB. &lt;strong&gt;No effect.&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;GC.run&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;472 → &lt;strong&gt;150 MB&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;G1PeriodicGCInterval=3000&lt;/code&gt; set live, no &lt;code&gt;GC.run&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;518 → &lt;strong&gt;156 MB&lt;/strong&gt; in 7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;SoftMaxHeapSize&lt;/code&gt; is listed as a manageable flag on G1 in JDK 21, so &lt;code&gt;jcmd VM.set_flag&lt;/code&gt; returns "Command executed successfully". That success message is misleading. Nothing in G1 reads the flag. If your automation sets the flag and then checks that the command succeeded, it will believe it lowered the ceiling on G1 when it did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to make G1 release memory back to the OS
&lt;/h2&gt;

&lt;p&gt;Pick one of these. I tested the first two.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run &lt;code&gt;GC.run&lt;/code&gt; from outside.&lt;/strong&gt; &lt;code&gt;jcmd &amp;lt;pid&amp;gt; GC.run&lt;/code&gt; triggers a full GC, after which G1 shrinks its heap. It is immediate, and it is a stop-the-world collection, so its pause grows with the live set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable periodic GC.&lt;/strong&gt; &lt;code&gt;G1PeriodicGCInterval&lt;/code&gt; is a manageable flag, default &lt;code&gt;0&lt;/code&gt; (off). Set it at startup, or live: &lt;code&gt;jcmd &amp;lt;pid&amp;gt; VM.set_flag G1PeriodicGCInterval 3000&lt;/code&gt; (milliseconds). In my test, memory fell within 7 seconds on an idle JVM. A related flag, &lt;code&gt;G1PeriodicGCSystemLoadThreshold&lt;/code&gt;, is also manageable and, per JEP 346, can hold the periodic GC back while the system is busy. I did not test it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tune &lt;code&gt;MinHeapFreeRatio&lt;/code&gt; and &lt;code&gt;MaxHeapFreeRatio&lt;/code&gt;.&lt;/strong&gt; These are manageable and control how much free heap G1 keeps after a collection. I did not test them here.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What this means for in-place resize
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Release before you resize, on every collector.&lt;/strong&gt; The failure was identical across all three: lowering the limit while the JVM held committed memory got it killed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust "command succeeded" on G1.&lt;/strong&gt; Verify that &lt;code&gt;memory.current&lt;/code&gt; actually dropped, then resize. In Kubernetes, the kubelet's check also compares current usage to the new limit and holds the resize while usage is higher, although the docs say that check is best-effort.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan the ceiling on G1.&lt;/strong&gt; With ZGC and Shenandoah, &lt;code&gt;SoftMaxHeapSize&lt;/code&gt; keeps the heap near your target after release. On G1 nothing does, so a G1 heap that was just released can grow back toward &lt;code&gt;-Xmx&lt;/code&gt; before the limit drops. I did not test how fast. The practical rule is the one from my earlier article: the smallest limit must exceed &lt;code&gt;-Xmx&lt;/code&gt; plus non-heap memory, and on G1 you cannot rely on a soft ceiling to enforce it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind the delay on ZGC.&lt;/strong&gt; &lt;code&gt;ZUncommitDelay&lt;/code&gt; defaults to 300 seconds. I shortened it to 1 second for the test. Without that, a ZGC release can take minutes unless you also lower the soft ceiling and wait for the uncommit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I didn't test
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Heap sizes above about 600 MB, live data that doesn't die, or JVMs under load. A full GC on G1 can cost far more at scale, and I haven't measured it.&lt;/li&gt;
&lt;li&gt;Whether G1 re-expands after release.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;G1PeriodicGCSystemLoadThreshold&lt;/code&gt;, &lt;code&gt;MinHeapFreeRatio&lt;/code&gt; and &lt;code&gt;MaxHeapFreeRatio&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A real kubelet. I used &lt;code&gt;docker update&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;G1 with the default region size. I set 4 MB regions because my test allocates 1 MB arrays, which are "humongous" objects in G1's default configuration, and the first G1 run ran out of heap.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;You need Docker. Save the files below in an empty folder.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Hold.java&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Hold&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;];&lt;/span&gt; &lt;span class="nc"&gt;Arrays&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;,(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"allocated 300MB"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"released refs"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;600000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run.sh&lt;/code&gt; runs the naive and ordered cases for any collector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# usage: ./run.sh &amp;lt;naive|ordered&amp;gt; &amp;lt;jvm flags...&amp;gt;      (run from the folder containing Hold.java)&lt;/span&gt;
&lt;span class="c"&gt;# Example: LIM=900 XMX=600 ./run.sh ordered -XX:+UseZGC -XX:ZUncommitDelay=1&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;warden-test
docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/w &lt;span class="nt"&gt;-w&lt;/span&gt; /w eclipse-temurin:21-jdk javac Hold.java
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;700&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;m &lt;span class="nt"&gt;--memory-swap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;700&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;m &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/w &lt;span class="nt"&gt;-w&lt;/span&gt; /w &lt;span class="se"&gt;\&lt;/span&gt;
  eclipse-temurin:21-jdk java &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-Xmx&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;500&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;m Hold &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 60&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;docker logs &lt;span class="nv"&gt;$n&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'released refs'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;2
mem&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/memory.current&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1048576&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"memory.current after the app dropped its data: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mode&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; ordered &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; jcmd 1 VM.set_flag SoftMaxHeapSize 104857600   &lt;span class="c"&gt;# 100 MB soft ceiling&lt;/span&gt;
  docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; jcmd 1 GC.run
  &lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 30&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 250 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"memory.current after SoftMaxHeapSize + GC.run: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;docker update &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300m &lt;span class="nt"&gt;--memory-swap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300m &lt;span class="nv"&gt;$n&lt;/span&gt;        &lt;span class="c"&gt;# in-place cgroup change, no restart&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;8
docker inspect &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'running={{.State.Running}} OOMKilled={{.State.OOMKilled}} exit={{.State.ExitCode}}'&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt;
docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x run.sh
&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;900 &lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600 ./run.sh naive   &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UseG1GC &lt;span class="nt"&gt;-XX&lt;/span&gt;:G1HeapRegionSize&lt;span class="o"&gt;=&lt;/span&gt;4m
&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;900 &lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600 ./run.sh ordered &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UseG1GC &lt;span class="nt"&gt;-XX&lt;/span&gt;:G1HeapRegionSize&lt;span class="o"&gt;=&lt;/span&gt;4m
&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;900 &lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600 ./run.sh naive   &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UseZGC &lt;span class="nt"&gt;-XX&lt;/span&gt;:ZUncommitDelay&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;900 &lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600 ./run.sh ordered &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UseZGC &lt;span class="nt"&gt;-XX&lt;/span&gt;:ZUncommitDelay&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;700 &lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;500 ./run.sh naive   &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UseShenandoahGC &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UnlockExperimentalVMOptions &lt;span class="nt"&gt;-XX&lt;/span&gt;:ShenandoahUncommitDelay&lt;span class="o"&gt;=&lt;/span&gt;1000
&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;700 &lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;500 ./run.sh ordered &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UseShenandoahGC &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UnlockExperimentalVMOptions &lt;span class="nt"&gt;-XX&lt;/span&gt;:ShenandoahUncommitDelay&lt;span class="o"&gt;=&lt;/span&gt;1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;g1-probe.sh&lt;/code&gt; tests each G1 lever on its own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Which G1 lever actually returns memory? Run from the folder containing Hold.java.&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/w &lt;span class="nt"&gt;-w&lt;/span&gt; /w eclipse-temurin:21-jdk javac Hold.java
start&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1
  docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;900m &lt;span class="nt"&gt;--memory-swap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;900m &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/w &lt;span class="nt"&gt;-w&lt;/span&gt; /w eclipse-temurin:21-jdk &lt;span class="se"&gt;\&lt;/span&gt;
    java &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UseG1GC &lt;span class="nt"&gt;-XX&lt;/span&gt;:G1HeapRegionSize&lt;span class="o"&gt;=&lt;/span&gt;4m &lt;span class="nt"&gt;-Xmx600m&lt;/span&gt; Hold &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
  &lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 60&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;docker logs &lt;span class="nv"&gt;$1&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'released refs'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
mem&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nv"&gt;$1&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/memory.current&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1048576&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"1) nothing (default), wait 25s"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           start g1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem g1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;25&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   &lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="s2"&gt; MB -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem g1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"2) SoftMaxHeapSize=100MB only, wait 20s"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  start g1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem g1&lt;span class="si"&gt;)&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;g1 jcmd 1 VM.set_flag SoftMaxHeapSize 104857600 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;20&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   &lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="s2"&gt; MB -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem g1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"3) GC.run only"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                           start g1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem g1&lt;span class="si"&gt;)&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;g1 jcmd 1 GC.run &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;4&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   &lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="s2"&gt; MB -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem g1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"4) G1PeriodicGCInterval=3000 set live"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    start g1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem g1&lt;span class="si"&gt;)&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;g1 jcmd 1 VM.set_flag G1PeriodicGCInterval 3000 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 40&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem g1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 250 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"   &lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="s2"&gt; MB -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem g1&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; MB (after &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s)"&lt;/span&gt;
docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; g1 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect &lt;code&gt;OOMKilled=true exit=137&lt;/code&gt; for naive runs and &lt;code&gt;running=true&lt;/code&gt; for ordered ones. The megabyte numbers will differ a little on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Which garbage collector is best for Kubernetes in-place pod resize?
&lt;/h3&gt;

&lt;p&gt;In my test, ZGC and Shenandoah are the easiest, because &lt;code&gt;SoftMaxHeapSize&lt;/code&gt; changes their behavior and keeps the heap near a target. G1 also works if something triggers a release, but it has no soft ceiling. Pick the collector your latency requirements need first, and then use the matching release method.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make G1 release memory back to the OS?
&lt;/h3&gt;

&lt;p&gt;Run &lt;code&gt;jcmd &amp;lt;pid&amp;gt; GC.run&lt;/code&gt;, or set &lt;code&gt;G1PeriodicGCInterval&lt;/code&gt; to a value in milliseconds (default &lt;code&gt;0&lt;/code&gt;, off). In my test both returned memory within a few seconds on a small idle heap.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does &lt;code&gt;SoftMaxHeapSize&lt;/code&gt; work with G1?
&lt;/h3&gt;

&lt;p&gt;No. &lt;code&gt;jcmd VM.set_flag SoftMaxHeapSize&lt;/code&gt; succeeds on G1 in JDK 21, but I saw no change in memory over 20 seconds, so I conclude G1 ignores it. It works on ZGC and Shenandoah.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why was my pod OOMKilled after an in-place memory decrease?
&lt;/h3&gt;

&lt;p&gt;The limit dropped while the JVM still held more committed memory than the new limit allows. In my test this killed the JVM on G1, ZGC and Shenandoah every time. Release memory first, check the resident memory, then lower the limit.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is &lt;code&gt;GC.run&lt;/code&gt; safe to use in production?
&lt;/h3&gt;

&lt;p&gt;It triggers a full collection. On G1 that is stop-the-world, and its pause grows with the live set. I only tested a 600 MB heap on an idle JVM, so measure the pause on your own heap before relying on it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Do I still need &lt;code&gt;RestartContainer&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;Only if the heap ceiling has to move. See my &lt;a href="https://baokhang.hashnode.dev/jvm-resizepolicy-restartcontainer-not-required-zgc-shenandoah" rel="noopener noreferrer"&gt;earlier article on &lt;code&gt;RestartContainer&lt;/code&gt;&lt;/a&gt; for the case where it is and isn't required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Warden fits
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/baokhang83/mnemo-jvm-warden" rel="noopener noreferrer"&gt;mnemo-jvm-warden&lt;/a&gt;, an open-source Kubernetes controller and sidecar that does the release-then-resize order for you. From its code and docs, it adjusts to the collector like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Detection:&lt;/strong&gt; the agent reads the target's GC beans and records, per collector, whether a soft max and uncommit are supported. ZGC and Shenandoah support both. G1 supports uncommit only. Serial, Parallel and Epsilon are treated as unsupported, and the agent stays read-only for them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Release:&lt;/strong&gt; it sets &lt;code&gt;SoftMaxHeapSize&lt;/code&gt; only where the collector has it, so on G1 that step is skipped instead of being sent and ignored. It then forces &lt;code&gt;GC.run&lt;/code&gt; and polls the committed heap until it drops and holds steady. It checks the process's resident memory before it shrinks the pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Boost on G1:&lt;/strong&gt; a G1 workload that wants a Boost lease must already launch with &lt;code&gt;-Xmx&lt;/code&gt; covering the largest &lt;code&gt;heapTarget&lt;/code&gt; it will request, because there is no soft ceiling to raise later. The agent rejects a heap target above the launch-time &lt;code&gt;-Xmx&lt;/code&gt; for every collector, and the Boost docs note that G1 has no live soft-max knob, so a G1 Boost only ever confirms that the allocation change landed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The conclusions above do not depend on any tool. The &lt;code&gt;jcmd&lt;/code&gt; commands work without it.&lt;/p&gt;

</description>
      <category>java</category>
      <category>jvm</category>
      <category>kubernetes</category>
      <category>performance</category>
    </item>
    <item>
      <title>Why Kubernetes 1.35 resizePolicy: RestartContainer Isn't Required for ZGC/Shenandoah</title>
      <dc:creator>BaoKhang</dc:creator>
      <pubDate>Sun, 08 Nov 2026 17:51:08 +0000</pubDate>
      <link>https://dev.to/baokhang83/why-kubernetes-135-resizepolicy-restartcontainer-isnt-required-for-zgcshenandoah-1cgp</link>
      <guid>https://dev.to/baokhang83/why-kubernetes-135-resizepolicy-restartcontainer-isnt-required-for-zgcshenandoah-1cgp</guid>
      <description>&lt;h1&gt;
  
  
  Why Kubernetes 1.35 resizePolicy: RestartContainer Isn't Required for ZGC/Shenandoah
&lt;/h1&gt;

&lt;p&gt;A lot of current advice says to set &lt;code&gt;resizePolicy: RestartContainer&lt;/code&gt; for memory on every JVM container. For growing the heap ceiling, that advice is right. For shrinking and regrowing within a ceiling you chose at launch, I tested ZGC and Shenandoah in a Linux container and found a restart isn't needed, as long as the JVM releases memory before the limit drops.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Kubernetes 1.35 can change a running container's memory in place (the &lt;code&gt;pods/resize&lt;/code&gt; subresource is GA).&lt;/li&gt;
&lt;li&gt;A JVM fixes its maximum heap at startup. Raising the limit in place does not raise the heap. Two popular articles are right about this.&lt;/li&gt;
&lt;li&gt;That does &lt;strong&gt;not&lt;/strong&gt; mean every memory change needs a restart. Three different operations hide inside "resize memory", and only one of them requires it.&lt;/li&gt;
&lt;li&gt;On ZGC and Shenandoah you can lower and raise the heap's soft ceiling at runtime. In my test, releasing memory first let a container's limit drop from 900 MB (or 700 MB) to 300 MB with no restart. Lowering the limit first got the JVM OOM-killed every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the advice says, and where it's right
&lt;/h2&gt;

&lt;p&gt;Rack2Cloud's &lt;a href="https://www.rack2cloud.com/vertical-pod-autoscaler-in-place-resize-production/" rel="noopener noreferrer"&gt;VPA in production article&lt;/a&gt; says: "&lt;code&gt;-Xmx&lt;/code&gt; is a startup parameter. The JVM reads it once at initialization and sets the maximum heap ceiling for the lifetime of the process." It concludes that &lt;code&gt;resizePolicy: RestartContainer&lt;/code&gt; for memory "is the correct setting for JVM services, not &lt;code&gt;NotRequired&lt;/code&gt;". Its &lt;a href="https://www.rack2cloud.com/kubernetes-1-35-in-place-pod-resize-production/" rel="noopener noreferrer"&gt;Kubernetes 1.35 article&lt;/a&gt; lists &lt;code&gt;RestartContainer&lt;/code&gt; as appropriate "for memory decreases on workloads where the allocator won't release memory without a restart".&lt;/p&gt;

&lt;p&gt;Shubham Chepe's &lt;a href="https://shubhamchepe.com/blog/kubernetes-in-place-pod-resize-java-jvm.html" rel="noopener noreferrer"&gt;post on the JVM and in-place resize&lt;/a&gt; gives the same prescription: "CPU &lt;code&gt;NotRequired&lt;/code&gt;, memory &lt;code&gt;RestartContainer&lt;/code&gt;". He explains why: "If you raise the memory limit in place, the heap ceiling doesn't grow." On shrinking he says it is "riskier still, since the JVM may already be using more than the new limit allows."&lt;/p&gt;

&lt;p&gt;Both are correct that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the heap ceiling comes from &lt;code&gt;-Xmx&lt;/code&gt; or &lt;code&gt;-XX:MaxRAMPercentage&lt;/code&gt; at launch, and&lt;/li&gt;
&lt;li&gt;a bigger cgroup limit does not make a running JVM use it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you need the heap ceiling itself to move, you need a restart. This article doesn't dispute that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three operations, not one
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Needs a restart?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Grow &lt;strong&gt;beyond&lt;/strong&gt; the &lt;code&gt;-Xmx&lt;/code&gt; the JVM launched with&lt;/td&gt;
&lt;td&gt;Yes. The ceiling is fixed.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Shrink&lt;/strong&gt; the container's memory&lt;/td&gt;
&lt;td&gt;Not on ZGC or Shenandoah, if the JVM releases memory first.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grow &lt;strong&gt;back&lt;/strong&gt; up to the launch-time ceiling&lt;/td&gt;
&lt;td&gt;No. Raise the container limit, then raise the soft ceiling.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The advice treats all three as the first one. That is the right call if you size &lt;code&gt;-Xmx&lt;/code&gt; to today's limit. It is the wrong one if you set &lt;code&gt;-Xmx&lt;/code&gt; for the peak and treat memory &lt;em&gt;below&lt;/em&gt; that as adjustable.&lt;/p&gt;

&lt;p&gt;ZGC and Shenandoah make this possible because of &lt;code&gt;SoftMaxHeapSize&lt;/code&gt;, a manageable flag. You can change it on a running JVM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jcmd &amp;lt;pid&amp;gt; VM.set_flag SoftMaxHeapSize &amp;lt;bytes&amp;gt;
jcmd &amp;lt;pid&amp;gt; GC.run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The collector tries to stay under the soft limit and uncommits what it frees. &lt;code&gt;-Xmx&lt;/code&gt; remains the hard ceiling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;I wanted to see what actually happens, so I ran it. The setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Eclipse Temurin 21.0.13 in a Linux container on Docker Desktop, on cgroup v2 (&lt;code&gt;/sys/fs/cgroup/memory.current&lt;/code&gt; was present).&lt;/li&gt;
&lt;li&gt;A small program that allocates and touches 300 MB, holds it for 10 seconds, then drops all references and idles.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker update --memory=300m --memory-swap=300m&lt;/code&gt; to lower the cgroup limit on the running container. This changes &lt;code&gt;memory.max&lt;/code&gt; in place, which is the same kernel mechanism a pod resize uses, but it is not the kubelet.&lt;/li&gt;
&lt;li&gt;Two runs per collector. &lt;strong&gt;Naive:&lt;/strong&gt; lower the limit straight away. &lt;strong&gt;Ordered:&lt;/strong&gt; set &lt;code&gt;SoftMaxHeapSize=100MB&lt;/code&gt;, run &lt;code&gt;GC.run&lt;/code&gt;, wait for &lt;code&gt;memory.current&lt;/code&gt; to fall, and only then lower the limit.&lt;/li&gt;
&lt;li&gt;Uncommit delays were shortened (&lt;code&gt;ZUncommitDelay=1&lt;/code&gt;, &lt;code&gt;ShenandoahUncommitDelay=1000&lt;/code&gt;) so the test finishes quickly. Defaults are longer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Collector&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;memory.current&lt;/code&gt; before&lt;/th&gt;
&lt;th&gt;Naive: lower limit to 300 MB&lt;/th&gt;
&lt;th&gt;Ordered: release first, then lower&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ZGC (&lt;code&gt;-Xmx600m&lt;/code&gt;, limit 900 MB)&lt;/td&gt;
&lt;td&gt;403 MB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;OOMKilled&lt;/strong&gt;, exit 137&lt;/td&gt;
&lt;td&gt;404 → &lt;strong&gt;89 MB&lt;/strong&gt; in 3 s, then limit lowered: &lt;strong&gt;still running&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shenandoah (&lt;code&gt;-Xmx500m&lt;/code&gt;, limit 700 MB)&lt;/td&gt;
&lt;td&gt;372 MB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;OOMKilled&lt;/strong&gt;, exit 137&lt;/td&gt;
&lt;td&gt;361 → &lt;strong&gt;64 MB&lt;/strong&gt; in 1 s, then limit lowered: &lt;strong&gt;still running&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the ordered sequence, &lt;code&gt;memory.max&lt;/code&gt; read back as 314572800 (300 MB) and the container never restarted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the kubelet adds
&lt;/h2&gt;

&lt;p&gt;My test used Docker, so it had no kubelet. In a real cluster, the kubelet tries to avoid an OOM kill when a memory limit goes down: according to the Kubernetes docs, if current usage is above the requested limit, the resize is skipped and stays "In Progress". The docs call this best-effort, with no guarantee, and note a race where usage can spike right after the check.&lt;/p&gt;

&lt;p&gt;For a JVM, that has a practical consequence. Committed heap counts as memory in use even when most of it is free, so the resize usually sits pending until the JVM gives memory back. Releasing first is what gets usage under the new limit, so the kubelet applies the resize. The ordered sequence works with the kubelet's check, not against it.&lt;/p&gt;

&lt;h2&gt;
  
  
  When RestartContainer is still the right answer
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You need the heap ceiling to change. &lt;code&gt;-Xmx&lt;/code&gt; is fixed per process.&lt;/li&gt;
&lt;li&gt;You run G1 and haven't enabled periodic GC. G1 has no soft ceiling and returns heap mainly at a full GC or concurrent cycle, so release is slower and less predictable. I didn't test it here.&lt;/li&gt;
&lt;li&gt;You can accept the restart. A restart discards compiled code, loaded classes, pools and caches, and a service with few replicas loses capacity while it warms up. If that is fine for you, &lt;code&gt;RestartContainer&lt;/code&gt; is simpler and needs no extra tooling.&lt;/li&gt;
&lt;li&gt;Your limit is already close to &lt;code&gt;-Xmx&lt;/code&gt; plus non-heap memory. Shrinking safely needs room: the smallest limit must exceed &lt;code&gt;-Xmx&lt;/code&gt; plus metaspace, code cache, thread stacks and direct buffers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What this test does not show
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It is a synthetic program, not a production service, and I tested ZGC and Shenandoah on one JDK build.&lt;/li&gt;
&lt;li&gt;ZGC and Shenandoah used slightly different heap and limit sizes (600/900 MB and 500/700 MB). My first ZGC run at 500 MB ran out of heap while filling 300 MB, so I gave it more room.&lt;/li&gt;
&lt;li&gt;It was not run through a kubelet. The kubelet's check will change what you see on a real cluster.&lt;/li&gt;
&lt;li&gt;I haven't measured how long a release takes on a large heap under real load.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;You need Docker. Save the two files below in an empty folder.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Hold.java&lt;/code&gt; allocates and touches 300 MB, holds it for 10 seconds, then drops it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;java.util.*&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Hold&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kd"&gt;throws&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;List&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ArrayList&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="o"&gt;];&lt;/span&gt; &lt;span class="nc"&gt;Arrays&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;,(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;add&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"allocated 300MB"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="o"&gt;;&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"released refs"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt; &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;flush&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
    &lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sleep&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;600000&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;run.sh&lt;/code&gt; starts a container, optionally releases memory first, then lowers the cgroup limit in place:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# usage: ./run.sh &amp;lt;naive|ordered&amp;gt; &amp;lt;jvm flags...&amp;gt;      (run from the folder containing Hold.java)&lt;/span&gt;
&lt;span class="c"&gt;# Example: LIM=900 XMX=600 ./run.sh ordered -XX:+UseZGC -XX:ZUncommitDelay=1&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;
&lt;span class="nv"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nv"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;warden-test
docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/w &lt;span class="nt"&gt;-w&lt;/span&gt; /w eclipse-temurin:21-jdk javac Hold.java
docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;700&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;m &lt;span class="nt"&gt;--memory-swap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;700&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;m &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PWD&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/w &lt;span class="nt"&gt;-w&lt;/span&gt; /w &lt;span class="se"&gt;\&lt;/span&gt;
  eclipse-temurin:21-jdk java &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-Xmx&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;500&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;m Hold &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 60&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;docker logs &lt;span class="nv"&gt;$n&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'released refs'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;2
mem&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="nb"&gt;cat&lt;/span&gt; /sys/fs/cgroup/memory.current&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="m"&gt;1048576&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"memory.current after the app dropped its data: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$mode&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; ordered &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; jcmd 1 VM.set_flag SoftMaxHeapSize 104857600   &lt;span class="c"&gt;# 100 MB soft ceiling&lt;/span&gt;
  docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; jcmd 1 GC.run
  &lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 30&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 250 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"memory.current after SoftMaxHeapSize + GC.run: &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;mem&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt; MB"&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;docker update &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300m &lt;span class="nt"&gt;--memory-swap&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;300m &lt;span class="nv"&gt;$n&lt;/span&gt;        &lt;span class="c"&gt;# in-place cgroup change, no restart&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;8
docker inspect &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'running={{.State.Running}} OOMKilled={{.State.OOMKilled}} exit={{.State.ExitCode}}'&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt;
docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nv"&gt;$n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run the four cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x run.sh
&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;900 &lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600 ./run.sh naive   &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UseZGC &lt;span class="nt"&gt;-XX&lt;/span&gt;:ZUncommitDelay&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;900 &lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;600 ./run.sh ordered &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UseZGC &lt;span class="nt"&gt;-XX&lt;/span&gt;:ZUncommitDelay&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;700 &lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;500 ./run.sh naive   &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UseShenandoahGC &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UnlockExperimentalVMOptions &lt;span class="nt"&gt;-XX&lt;/span&gt;:ShenandoahUncommitDelay&lt;span class="o"&gt;=&lt;/span&gt;1000
&lt;span class="nv"&gt;LIM&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;700 &lt;span class="nv"&gt;XMX&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;500 ./run.sh ordered &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UseShenandoahGC &lt;span class="nt"&gt;-XX&lt;/span&gt;:+UnlockExperimentalVMOptions &lt;span class="nt"&gt;-XX&lt;/span&gt;:ShenandoahUncommitDelay&lt;span class="o"&gt;=&lt;/span&gt;1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expect &lt;code&gt;OOMKilled=true exit=137&lt;/code&gt; for the naive runs and &lt;code&gt;running=true OOMKilled=false&lt;/code&gt; for the ordered ones. The exact megabyte numbers will differ a little on your machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Do I need &lt;code&gt;resizePolicy: RestartContainer&lt;/code&gt; for JVM memory?
&lt;/h3&gt;

&lt;p&gt;Only if the heap ceiling has to move. A JVM fixes its maximum heap at startup, so growing beyond the &lt;code&gt;-Xmx&lt;/code&gt; it launched with needs a restart. Shrinking, and growing back up to the launch ceiling, did not need one on ZGC or Shenandoah in my test, as long as the JVM released memory before the limit dropped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why doesn't raising the container's memory limit give the JVM more heap?
&lt;/h3&gt;

&lt;p&gt;The JVM computes its maximum heap once, from &lt;code&gt;-Xmx&lt;/code&gt; or from &lt;code&gt;-XX:MaxRAMPercentage&lt;/code&gt; applied to the limit it saw at startup. A later in-place change to the cgroup limit does not change that number.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I change &lt;code&gt;-Xmx&lt;/code&gt; on a running JVM?
&lt;/h3&gt;

&lt;p&gt;No. What you can change on ZGC and Shenandoah is the soft ceiling, &lt;code&gt;SoftMaxHeapSize&lt;/code&gt;, which sits at or below &lt;code&gt;-Xmx&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What happens if I lower the memory limit without releasing memory first?
&lt;/h3&gt;

&lt;p&gt;In my test the JVM was OOM-killed (exit code 137) every time, on both ZGC and Shenandoah. On a real cluster the kubelet makes a best-effort attempt to avoid this and may keep the resize pending while usage is above the new limit. The Kubernetes docs say this check gives no guarantees, because usage can spike right after it.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do I make a JVM give memory back without restarting it?
&lt;/h3&gt;

&lt;p&gt;On ZGC and Shenandoah, lower the soft ceiling and force a collection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jcmd &amp;lt;pid&amp;gt; VM.set_flag SoftMaxHeapSize &amp;lt;bytes&amp;gt;
jcmd &amp;lt;pid&amp;gt; GC.run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check that the process's resident memory dropped before you lower the limit. The collector's uncommit delay applies: I shortened it for the test, and the defaults are longer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does this work with G1?
&lt;/h3&gt;

&lt;p&gt;I didn't test it. G1 has no soft heap ceiling, so you cannot use the same flag. Plan for a slower, less predictable release, or use &lt;code&gt;RestartContainer&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is the default &lt;code&gt;resizePolicy&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;NotRequired&lt;/code&gt; for both CPU and memory, which means the change is applied without restarting the container. Setting &lt;code&gt;RestartContainer&lt;/code&gt; for memory restarts the container, not the whole pod.&lt;/p&gt;

&lt;h3&gt;
  
  
  How small can the memory limit go?
&lt;/h3&gt;

&lt;p&gt;The smallest limit must stay above &lt;code&gt;-Xmx&lt;/code&gt; plus non-heap memory (metaspace, code cache, thread stacks, direct buffers) plus working-set headroom. For a typical Spring Boot service the non-heap part is commonly 100 to 150 MiB.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Warden fits
&lt;/h2&gt;

&lt;p&gt;I maintain &lt;a href="https://github.com/baokhang83/mnemo-jvm-warden" rel="noopener noreferrer"&gt;mnemo-jvm-warden&lt;/a&gt;, an open-source Kubernetes controller and sidecar that runs this order for you: lower the soft ceiling, force a GC, verify the process's resident memory dropped, and only then shrink the pod in place. To grow, it reverses the order. It does not change &lt;code&gt;-Xmx&lt;/code&gt;, so the same sizing rule applies. There is a &lt;a href="https://github.com/baokhang83/mnemo-jvm-warden/blob/main/docs/shrink-jvm-memory-kubernetes.md" rel="noopener noreferrer"&gt;guide&lt;/a&gt; with the full reasoning, and a &lt;a href="https://baokhang.hashnode.dev/shrink-jvm-memory-kubernetes-in-place-resize" rel="noopener noreferrer"&gt;walkthrough on a kind cluster&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The point of this article doesn't depend on any tool. The &lt;code&gt;jcmd&lt;/code&gt; commands above work without it.&lt;/p&gt;

</description>
      <category>java</category>
      <category>jvm</category>
      <category>kubernetes</category>
      <category>devops</category>
    </item>
    <item>
      <title>I built a workflow that forces AI agents to teach you the code they write</title>
      <dc:creator>BaoKhang</dc:creator>
      <pubDate>Sat, 18 Jul 2026 12:23:01 +0000</pubDate>
      <link>https://dev.to/baokhang83/i-built-a-workflow-that-forces-ai-agents-to-teach-you-the-code-they-write-3784</link>
      <guid>https://dev.to/baokhang83/i-built-a-workflow-that-forces-ai-agents-to-teach-you-the-code-they-write-3784</guid>
      <description>&lt;p&gt;As a developer, I got a bit scared and fed-up of blindly merging massive pull requests and losing context on my own codebase, so I built &lt;strong&gt;FluencyLoop&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/baokhang83/fluencyloop" rel="noopener noreferrer"&gt;https://github.com/baokhang83/fluencyloop&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is an open-source, local-first workflow plugin for &lt;strong&gt;Claude Code and Codex&lt;/strong&gt; that ensures your code and your codebase fluency are produced together.&lt;/p&gt;

&lt;p&gt;Instead of letting an agent dump raw code, it tracks your technical familiarity locally and pauses to explain complex architectural choices or rejected alternatives only on topics you do not know yet. It also produces documentation and tracks the decisions it makes along the way.&lt;/p&gt;

&lt;p&gt;Try it out ! It feels really good to get the AI caring for my understanding 🤣&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>learning</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
