丽鸿 發表於 2025-12-30 09:18:00

jvm~分析gc老年代内存过高的原因

<p>gc的老年代内存高居不下,导致最后full gc的发生,我们需要通过分析gc dump文件来解决biggest objects过多的问题</p>
<p><img src="https://images.cnblogs.com/cnblogs_com/lori/2399824/o_251230010703_kc-prod-gc.png" alt="" loading="lazy"></p>
<h1 id="生成dump文件">生成dump文件</h1>
<h3 id="在keycloak容器中安装轻量级工具">在keycloak容器中安装轻量级工具</h3>
<pre><code>microdnf install -y wget
microdnf install -y procps-ng #包含ps命令
ps -aux # 找到keycloak的pid,默认是712
wgethttps://github.com/apangin/jattach/releases/download/v2.0/jattach
chmod +x jattach
# 使用jattach执行内存命令
./jattach 712 dumpheap /tmp/heap.hprof
</code></pre>
<h3 id="下载生成的hprof文件使用jprofile分析它">下载生成的hprof文件,使用jprofile分析它</h3>
<p><img src="https://images.cnblogs.com/cnblogs_com/lori/2399824/o_251229071437_gc_heap20251229151406.png" alt="" loading="lazy"></p>
<blockquote>
<p>我们可以从biggest objects里面,查看哪些大对象没有被释放,或者频繁被创建</p>
</blockquote>
<h1 id="hprof文件和dump文件">HProf文件和Dump文件</h1>
<h3 id="1-dump-文件转储文件"><strong>1. Dump 文件(转储文件)</strong></h3>
<p><strong>广义概念</strong>:程序在某个时间点的状态快照。</p>
<h4 id="java-中的-dump-文件主要类型"><strong>Java 中的 Dump 文件主要类型</strong>:</h4>
<table>
<thead>
<tr>
<th>类型</th>
<th>描述</th>
<th>常用扩展名</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Heap Dump</strong></td>
<td>JVM 堆内存快照,包含所有对象实例</td>
<td><code>.hprof</code>, <code>.bin</code>, <code>.dmp</code></td>
</tr>
<tr>
<td><strong>Thread Dump</strong></td>
<td>线程状态快照,包含调用栈、锁信息</td>
<td><code>.tdump</code>, <code>.txt</code></td>
</tr>
<tr>
<td><strong>Core Dump</strong></td>
<td>进程崩溃时的完整内存镜像</td>
<td><code>.core</code>, <code>.dmp</code></td>
</tr>
</tbody>
</table>
<h3 id="2-hprof-文件"><strong>2. HProf 文件</strong></h3>
<p><strong>具体格式</strong>:Java 专用的堆转储文件格式。</p>
<h4 id="特点"><strong>特点</strong>:</h4>
<ul>
<li><strong>标准格式</strong>:Oracle/OpenJDK 的标准堆转储格式</li>
<li><strong>二进制格式</strong>:包含完整的堆内存数据结构</li>
<li><strong>包含内容</strong>:
<ul>
<li>所有对象实例及其数据</li>
<li>对象之间的引用关系</li>
<li>类元数据(Class metadata)</li>
<li>GC Root 引用链</li>
<li>静态变量</li>
</ul>
</li>
</ul>
<h3 id="3-两者关系"><strong>3. 两者关系</strong></h3>
<pre><code>Dump文件(概念)
├── Heap Dump(堆转储)
│   ├── HProf 格式(标准格式)
│   ├── JProfiler 格式(.snapshot)
│   └── YourKit 格式(.snapshot)
├── Thread Dump(线程转储)
└── Core Dump(核心转储)
</code></pre>
<h3 id="4-生成方式对比"><strong>4. 生成方式对比</strong></h3>
<h4 id="heap-dump-hprof-生成"><strong>Heap Dump (HProf) 生成</strong>:</h4>
<pre><code class="language-bash"># 1. 使用 jmap 命令(最常用)
jmap -dump:format=b,file=heap.hprof &lt;pid&gt;

# 2. 使用 jcmd 命令
jcmd &lt;pid&gt; GC.heap_dump /path/to/heap.hprof

# 3. JVM 启动参数(内存溢出时自动生成)
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/path/to/dumps

# 4. 使用 VisualVM、JConsole 等工具
</code></pre>
<h4 id="thread-dump-生成"><strong>Thread Dump 生成</strong>:</h4>
<pre><code class="language-bash"># 1. 使用 jstack 命令
jstack &lt;pid&gt; &gt; thread.dump

# 2. 使用 jcmd
jcmd &lt;pid&gt; Thread.print &gt; thread.dump

# 3. 发送信号
kill -3 &lt;pid&gt;# 输出到标准输出
</code></pre>
<h3 id="5-文件结构解析"><strong>5. 文件结构解析</strong></h3>
<h4 id="hprof-文件内容结构"><strong>HProf 文件内容结构</strong>:</h4>
<pre><code>文件头

记录类型1 (如:UTF8字符串表)

记录类型2 (如:类加载信息)

记录类型3 (如:实例数据)

记录类型4 (如:对象引用)

...
</code></pre>
<h4 id="关键数据段"><strong>关键数据段</strong>:</h4>
<ol>
<li><strong>字符串表</strong> - 所有字符串常量的映射</li>
<li><strong>类信息</strong> - 加载的类、父类、接口</li>
<li><strong>实例数据</strong> - 每个对象实例的字段值</li>
<li><strong>对象数组</strong> - 数组类型和元素</li>
<li><strong>根引用</strong> - GC Roots 集合</li>
<li><strong>原始数据</strong> - 堆栈帧、本地变量等</li>
</ol>
<h3 id="6-分析方法工具"><strong>6. 分析方法工具</strong></h3>
<h4 id="分析-hprof-文件的工具"><strong>分析 HProf 文件的工具</strong>:</h4>
<table>
<thead>
<tr>
<th>工具</th>
<th>特点</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Eclipse MAT</strong></td>
<td>功能强大,内存分析专业</td>
<td>深入内存泄漏分析</td>
</tr>
<tr>
<td><strong>VisualVM</strong></td>
<td>JDK 自带,轻量级</td>
<td>快速查看概览</td>
</tr>
<tr>
<td><strong>JProfiler</strong></td>
<td>商业工具,可视化好</td>
<td>性能分析和监控</td>
</tr>
<tr>
<td><strong>YourKit</strong></td>
<td>商业工具,CPU/Memory分析</td>
<td>生产环境分析</td>
</tr>
<tr>
<td><strong>Java Mission Control</strong></td>
<td>Oracle 官方工具</td>
<td>Flight Recorder 数据分析</td>
</tr>
</tbody>
</table>
<h4 id="示例使用-mat-分析步骤"><strong>示例:使用 MAT 分析步骤</strong>:</h4>
<pre><code class="language-bash"># 1. 生成转储文件
jmap -dump:live,format=b,file=keycloak.hprof &lt;pid&gt;

# 2. 用 MAT 打开,常见视图:
- Histogram(直方图): 按类统计对象数量
- Dominator Tree(支配树): 找出内存占用最大的对象
- Leak Suspects(泄漏嫌疑): 自动分析疑似泄漏
- Path to GC Roots(到GC根的路径): 查看对象引用链
</code></pre>
<h3 id="7-实际案例分析"><strong>7. 实际案例分析</strong></h3>
<h4 id="你遇到的-queryplancache-问题分析"><strong>你遇到的 QueryPlanCache 问题分析</strong>:</h4>
<pre><code class="language-java">// HProf 分析中可能看到:
org.hibernate.engine.query.spi.QueryPlanCache
├── queryPlanCache (ConcurrentHashMap)
│   ├── key: "from User where realmId = ? order by createdTimestamp"
│   ├── value: QueryPlan对象 (包含解析后的SQL、参数映射等)
│   └── size: 可能数千个条目
└── parameterMetadataCache
    └── 存储参数元数据
</code></pre>
<h4 id="文件大小参考"><strong>文件大小参考</strong>:</h4>
<ul>
<li>小型应用:几百 MB</li>
<li>中型应用(如 Keycloak):1-4 GB</li>
<li>大型应用:10 GB+</li>
</ul>
<h3 id="8-使用技巧"><strong>8. 使用技巧</strong></h3>
<h4 id="减小-hprof-文件大小"><strong>减小 HProf 文件大小</strong>:</h4>
<pre><code class="language-bash"># 1. 只转储存活对象(推荐)
jmap -dump:live,format=b,file=heap.hprof &lt;pid&gt;

# 2. 过滤不需要的内容
# 使用MAT的OQL或过滤器功能

# 3. 压缩存储
jmap -dump:live,format=b,file=heap.hprof.gz &lt;pid&gt;
</code></pre>
<h4 id="自动化分析脚本"><strong>自动化分析脚本</strong>:</h4>
<pre><code class="language-bash">#!/bin/bash
PID=$(jps | grep keycloak | awk '{print $1}')
TIMESTAMP=$(date +%Y%m%d_%H%M%S)

# 生成堆转储
jmap -dump:live,format=b,file=/dump/keycloak_${TIMESTAMP}.hprof $PID

# 生成线程转储
jstack $PID &gt; /dump/thread_${TIMESTAMP}.dump

# 基本分析
echo "=== 堆转储生成完成 ==="
echo "文件: /dump/keycloak_${TIMESTAMP}.hprof"
echo "大小: $(du -h /dump/keycloak_${TIMESTAMP}.hprof | cut -f1)"
</code></pre>
<h3 id="9-注意事项"><strong>9. 注意事项</strong></h3>
<ol>
<li><strong>性能影响</strong>:生成堆转储会暂停 JVM(Stop-The-World),生产环境谨慎使用</li>
<li><strong>磁盘空间</strong>:确保有足够的磁盘空间(通常是堆内存的1-1.5倍)</li>
<li><strong>安全考虑</strong>:转储文件可能包含敏感数据(如密码、密钥)</li>
<li><strong>版本兼容性</strong>:HProf 文件与 JVM 版本相关,高版本工具可分析低版本文件</li>
</ol>
<h3 id="10-最佳实践"><strong>10. 最佳实践</strong></h3>
<ol>
<li><strong>定期收集基线</strong>:在应用稳定时收集作为对比基准</li>
<li><strong>配置自动转储</strong>:配置 OOM 时自动生成转储</li>
<li><strong>保留多个时间点</strong>:对于内存泄漏,保留多个时间点的转储对比</li>
<li><strong>配合日志分析</strong>:转储文件结合 GC 日志、应用日志分析</li>
</ol>
<p><strong>总结</strong>:HProf 是 Java 堆转储的标准格式文件,属于 Dump 文件的一种。通过分析这种文件,可以定位内存泄漏、优化内存使用,就像你发现的 <code>QueryPlanCache</code> 问题一样。</p>


</div>
<div id="MySignature" role="contentinfo">
    <p></p>
<div class="navgood">
<p>作者:仓储大叔,张占岭,<br>
荣誉:微软MVP<br>QQ:853066980</p>

<p><strong>支付宝扫一扫,为大叔打赏!</strong>
<br><img src="https://images.cnblogs.com/cnblogs_com/lori/237884/o_IMG_7144.JPG"></p>
</div><br><br>
来源:https://www.cnblogs.com/lori/p/19419257
頁: [1]
查看完整版本: jvm~分析gc老年代内存过高的原因