wangmingzhou1986 commented on issue #9795:
URL: https://github.com/apache/paimon/issues/9795#issuecomment-5658576025

   Follow-up with the heap-level evidence I offered in the report. **The 
retained classloaders survive forced full GCs unchanged, so this is strong 
reachability, not deferred collection.**
   
   ## How the data was obtained
   
   The official Flink image ships a JRE, so I streamed a JDK 17 into the 
running TaskManager container (`tar` pod→pod, no restart, so the 
already-retained classloaders were preserved) and ran `jcmd` against the live 
JVM.
   
   One gotcha worth recording for anyone repeating this: the HotSpot attach 
mechanism requires the **same uid** as the target JVM. `kubectl exec` lands as 
`root` while the TaskManager runs as `flink`, and attaching as root fails with 
`target process 1 doesn't respond within 10500ms`. You have to `su` to the 
JVM's owner first.
   
   ## `VM.classloader_stats` on a TaskManager holding 570.7 MB Metaspace
   
   | classloader type | live instances | classes | ChunkSz |
   |---|---|---|---|
   | `org.apache.flink.util.ChildFirstClassLoader` | **23** | 68,461 | 50.7 MB |
   | `FlinkUserCodeClassLoaders$SafetyNetWrapperClassLoader` | **23** | 0 | – |
   | **`org.apache.paimon.plugin.ComponentClassLoader`** | **19** | 7,521 | 9.3 
MB |
   | `org.apache.paimon.shade.org.codehaus.janino.ByteArrayClassLoader` | 55 | 
55 | – |
   | `jdk.internal.reflect.DelegatingClassLoader` | 564 | 564 | 0.3 MB |
   | `org.codehaus.commons.compiler.util.reflect.ByteArrayClassLoader` | 13 | 
13 | – |
   | *(total)* | 703 CLDs | 93,193 | 71.6 MB |
   
   `SafetyNetWrapperClassLoader` is created 1:1 with a job's user-code 
classloader, and there are exactly **23** of them alongside **23** 
`ChildFirstClassLoader` — i.e. 23 finished jobs whose classloaders are still 
alive.
   
   ## Forced full GC — the decisive test
   
   Two consecutive `jcmd <pid> GC.run` (both reported `Command executed 
successfully`), 20 s apart:
   
   | | before GC | after 1st | after 2nd |
   |---|---|---|---|
   | `ChildFirstClassLoader` | **23** | **23** | **23** |
   | `SafetyNetWrapperClassLoader` | **23** | **23** | **23** |
   | **`ComponentClassLoader` (Paimon)** | **19** | **19** | **19** |
   | shaded-janino `ByteArrayClassLoader` | 55 | 41 | 35 |
   | total CLDs | 703 | 412 | 406 |
   | **Metaspace (Flink metric)** | **570.7 MB** | – | **570.7 MB** |
   
   GC clearly ran and clearly collected things — total CLDs dropped 703 → 406 
and the janino loaders shrank 55 → 35. But the **user classloaders and the 
Paimon `ComponentClassLoader`s did not move at all**, and Metaspace did not 
drop by a single byte.
   
   This rules out "the JVM just hasn't collected yet" and confirms a 
strong-reference retention.
   
   ## Two concrete suspects
   
   **1. `ComponentClassLoader` is rooted at the platform loader, not at a job.**
   
   `jcmd VM.classloaders` shows the 19 instances hanging **directly off 
`PlatformClassLoader`**, siblings of Flink's own 
`PluginLoader$PluginClassLoader`:
   
   ```
   +-- "platform", jdk.internal.loader.ClassLoaders$PlatformClassLoader
         |
         +-- org.apache.paimon.plugin.ComponentClassLoader
         +-- org.apache.flink.core.plugin.PluginLoader$PluginClassLoader
         +-- org.apache.paimon.plugin.ComponentClassLoader
         +-- org.apache.paimon.plugin.ComponentClassLoader
         ...        (19 in total)
   ```
   
   They are not children of any user classloader, they accumulate roughly 1:1 
with jobs (19 vs 23), and they survive full GC — so something static is holding 
them. If a `ComponentClassLoader` (or anything it loaded) also references the 
job's `ChildFirstClassLoader`, that alone would pin the whole chain.
   
   **2. The shaded janino reflection cache holds `Class` objects from the user 
loader.**
   
   In `GC.class_histogram`, 
`org.apache.paimon.shade.org.codehaus.janino.ReflectionIClass$ReflectionIMethod`
 appears as **eight separate entries** (997 instances ×6, 938 ×2, …). A class 
name appearing multiple times in a histogram means it was **loaded once per 
classloader and all copies are still live**. Janino keeps static 
reflection/`IClass` caches, which is a textbook classloader pin when the cached 
`Class` objects come from a child loader.
   
   ## One number I cannot yet explain
   
   `VM.classloader_stats` accounts for **71.6 MB** of chunks in total, while 
the `Metaspace` MXBean reports **570.7 MB** used on the same JVM at the same 
moment. I'm reporting the discrepancy rather than explaining it away — possibly 
freed-but-unreturned chunks, or a different accounting boundary. It does not 
affect the conclusion above (counts, not bytes, are what stayed flat across GC).
   
   ## Environment recap
   
   Flink 1.20.5 session cluster, Temurin 17.0.19, G1, TM 
`-XX:MaxMetaspaceSize=768m`.
   Workload: repeated short batch jobs, each shipping `paimon-flink-1.20` 
(1.2.0/1.3.1) as a **user jar** via `sql-client -j`, doing `CREATE CATALOG ... 
'type'='paimon'` on `s3://` + `SELECT COUNT(*)` against a small primary-key 
table. ~18.2 MB Metaspace per finished job, never returned.
   
   I can still produce a full heap dump with dominator/GC-root analysis if that 
would be more useful than the above — just say the word. Happy to test a patch 
as well.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to