JProfiler‘Run GC’与JVM常规GC差异及Java应用内存释放异常咨询
Hey, this is a super common issue when working with CMS GC and profiling tools—let me break down what's going on here, based on my own troubleshooting experiences:
Core Differences Between JProfiler's Run GC and Regular CMS GC
1. What JProfiler's "Run GC" Actually Does
When you click that button, JProfiler doesn't just nudge the JVM—it triggers a full, stop-the-world (STW) garbage collection that forces the JVM to clean up every possible unreachable object, no exceptions. Under the hood, it typically calls System.gc() (with guarantees that the GC completes before returning), and if your CMS setup uses a fallback collector like Serial Old or Parallel Old, this full GC will also compact the old generation—moving all live objects together to eliminate memory fragmentation.
2. How Regular CMS GC Behaves
Your configured ConcMarkSweepGC (CMS) is optimized for low latency, not maximum memory reclamation:
- It runs most of its work concurrently with application threads (no full STW pause for most phases), which means it can't catch all garbage (called "floating garbage") created during the GC process—this garbage has to wait for the next cycle.
- CMS avoids compacting the old generation by default (compaction is STW and hurts latency), so over time, you can get significant memory fragmentation. Even if there's plenty of total free space, it might look like memory is "full" because there's no single contiguous block for large objects.
- By default, CMS only triggers a concurrent old-gen GC when the old generation is nearly full (controlled by
-XX:CMSInitiatingOccupancyFraction, default ~92%). Most regular GCs you're seeing are likely young-gen GCs (which only clean up the Eden/Survivor spaces, not the old gen), hence why only 1-1.2GB is freed.
Why You're Seeing Such a Big Difference in Memory Release
When you click JProfiler's Run GC:
- It forces a full STW GC that cleans up all floating garbage from previous CMS cycles.
- It compacts the old generation, turning fragmented free space into a single contiguous block—so even if the total free memory was the same before, it now looks like more is "available" because it's usable for large objects.
- It also cleans up metadata in the metaspace (if you've enabled
-XX:+CMSClassUnloadingEnabled) that regular CMS might not touch as aggressively.
Steps to Fix Your Memory Issue
- Check Your GC Logs: Enable GC logging with
-Xloggc:gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStampsto confirm whether regular GCs are only young-gen collections, or if CMS is running but leaving garbage/fragmentation. - Tweak CMS Parameters:
- Lower
-XX:CMSInitiatingOccupancyFraction(e.g., to 70-75) to trigger concurrent old-gen GCs earlier, reducing floating garbage. - Enable
-XX:+UseCMSCompactAtFullCollectionand set-XX:CMSFullGCsBeforeCompaction=1to force compaction after every full GC (note: this adds STW pauses, so test carefully). - Add
-XX:+CMSClassUnloadingEnabledto clean up unused classes in the metaspace.
- Lower
- Hunt for Memory Leaks: Use JProfiler to take a heap dump after a regular GC and after JProfiler's Run GC. Compare the two to see which objects are only getting cleaned up during full GC—this could point to leaks like static collections holding references, unclosed resources (DB connections, file handles), or thread pools retaining old tasks.
- Avoid Frequent Manual GCs: While JProfiler's Run GC is great for debugging, don't rely on
System.gc()in production—it can introduce unexpected STW pauses. Focus on fixing the root cause instead.
内容的提问来源于stack exchange,提问作者Tanveer Ali

