基于gc.log与堆转储文件快速分析内存导致服务挂掉问题

第一步:5分钟快速定位问题类型

1.1 检查GC日志中的死亡信号

# 查看最后50行,找到死亡时间点
tail -50 gc.log

# 或者更精确地查找死亡痕迹
grep -n "OutOfMemoryError\|FULL GC\|Allocation Failure\|concurrent mode failure" gc.log | tail -10

1.2 运行这个快速诊断脚本

#!/bin/bash
echo "=== GC日志快速诊断 ==="
echo "1. 文件信息:"
wc -l gc.log
echo -e "\n2. 关键错误统计:"
echo "OOM错误: $(grep -c "OutOfMemoryError" gc.log)"
echo "Full GC次数: $(grep -c "Full GC" gc.log)"
echo "并发模式失败: $(grep -c "concurrent mode failure" gc.log)"

echo -e "\n3. 最后5次关键事件:"
tail -100 gc.log | grep -B2 -A2 "Full GC\|OOM\|failed\|Error" | tail -20

echo -e "\n4. 内存增长趋势:"
grep "Full GC" gc.log | tail -5 | awk '{
    for(i=1;i<=NF;i++) {
        if($i ~ /old=[0-9]+K->[0-9]+K/) {
            match($i, /old=([0-9]+)K->([0-9]+)K/, arr)
            printf("时间 %s: 老年代 %sK → %sK\n", $1, arr[1], arr[2])
        }
    }
}'

 第二步:分析堆转储文件(核心步骤)

2.1 先查看堆转储基本信息

# 检查文件有效性
jmap -histo heap_dump.hprof 2>&1 | head -5

# 如果报错,可能需要指定JRE路径
export JAVA_HOME=/path/to/java
$JAVA_HOME/bin/jmap -histo heap_dump.hprof > /dev/null && echo "堆转储文件正常" || echo "文件可能损坏"

2.2 生成对象直方图(最重要的一步)

# 按对象数量排序
jmap -histo heap_dump.hprof | head -50 > histo_by_count.txt

# 按内存占用排序(倒序)
jmap -histo heap_dump.hprof | awk '
BEGIN {print "Class_Name\t<实例数>\t<总大小>\t<平均大小>"}
NR>3 {
    gsub(/</, "", $2); gsub(/>/, "", $2)
    printf("%s\t%s\t%s\t%.1f\n", $2, $3, $4, $4/$3)
}' | sort -k3 -rn | head -50 > histo_by_size.txt

echo "已生成 histo_by_size.txt,查看前50个最大对象"

2.3 重点关注你们自己的代码

# 查找你们公司包名的对象
jmap -histo heap_dump.hprof | grep -E "com\.yourcompany|com\.yourproject" | sort -k4 -rn > our_objects.txt

# 如果没有明确包名,查找常见的可疑类
jmap -histo heap_dump.hprof | grep -E "HashMap|ArrayList|LinkedList|ConcurrentHashMap|byte\[\]|char\[\]" > collections.txt

2.4 分析具体问题模式

#!/bin/bash
echo "=== 堆转储详细分析 ==="

# 1. 查看byte[]数组占用(通常最大)
echo "1. byte[] 数组分析:"
BYTE_ARRAY_INFO=$(jmap -histo heap_dump.hprof | grep "^\s*[0-9]\+:\s\+[0-9]\+\s\+[0-9]\+\s\+byte\[\]" | head -1)
if [ ! -z "$BYTE_ARRAY_INFO" ]; then
    echo "  发现: $BYTE_ARRAY_INFO"
    NUM_BYTE_ARRAYS=$(echo $BYTE_ARRAY_INFO | awk '{print $2}')
    TOTAL_SIZE=$(echo $BYTE_ARRAY_INFO | awk '{print $3}')
    AVG_SIZE=$(echo "scale=2; $TOTAL_SIZE * 1024 / $NUM_BYTE_ARRAYS" | bc)
    echo "  平均每个byte[]大小: $AVG_SIZE 字节"
fi

# 2. 查看HashMap相关
echo -e "\n2. HashMap分析:"
jmap -histo heap_dump.hprof | grep -E "HashMap\$Node|HashMap\$Entry" | head -3

# 3. 查看线程数量
echo -e "\n3. 线程分析:"
THREAD_COUNT=$(jmap -histo heap_dump.hprof | grep " java.lang.Thread" | awk '{print $2}')
if [ ! -z "$THREAD_COUNT" ]; then
    echo "  线程数量: $THREAD_COUNT"
    if [ $THREAD_COUNT -gt 500 ]; then
        echo "  ⚠️  线程数过多!"
    fi
fi

# 4. 查看Class数量
echo -e "\n4. Class数量:"
jmap -histo heap_dump.hprof | grep " java.lang.Class" | awk '{print "  Class数量: "$2}'

 第三步:分析对象引用关系

3.1 查找最大的对象并追踪引用链

# 先找到最大的类名
BIGGEST_CLASS=$(head -1 histo_by_size.txt | awk '{print $1}')

echo "最大的类是: $BIGGEST_CLASS"
echo "开始分析引用链..."

# 如果是你自己的类,直接分析
if [[ $BIGGEST_CLASS == com.* ]]; then
    echo "这是你们自己的类,需要找到谁创建了它"
    
    # 获取类的所有实例ID
    echo "获取实例ID..."
    jmap -histo heap_dump.hprof | grep "$BIGGEST_CLASS" | head -5
fi

3.2 使用jhat命令行模式(需要额外内存)

# 启动jhat分析服务(可能需要4-8G内存)
jhat -J-Xmx4g heap_dump.hprof &
JHAT_PID=$!
echo "jhat启动在端口7000,PID: $JHAT_PID"

# 等待启动
sleep 30

# 查询最大的对象
curl -s "http://localhost:7000/oql/?query=select%20s%20from%20java.lang.Object%20s%20where%20s.getClass().getName().equals(\"$BIGGEST_CLASS\")" | grep -A20 -B20 "instance"

# 完成后杀掉jhat
kill $JHAT_PID

3.3 更轻量的分析方式:使用jstack分析线程

如果有线程转储,可以结合分析:

# 查看哪些线程持有大量内存
grep -A5 -B5 "RUNNABLE" thread_dump.txt | grep -E "com\.yourcompany|正在执行" | head -20

第四步:结合业务代码定位问题

4.1 从堆转储反推代码位置

#!/bin/bash
echo "=== 问题定位指南 ==="

# 从histo_by_size.txt中读取可疑类
echo "检查以下可疑类:"
cat histo_by_size.txt | head -10 | while read line; do
    CLASS_NAME=$(echo $line | awk '{print $1}')
    INSTANCE_COUNT=$(echo $line | awk '{print $2}')
    TOTAL_SIZE_MB=$(echo $line | awk '{print $3/1024}')
    
    echo "类: $CLASS_NAME"
    echo "  实例数: $INSTANCE_COUNT"
    echo "  总内存: ${TOTAL_SIZE_MB}MB"
    
    # 给出可能的问题点
    if [[ $CLASS_NAME == *HashMap* ]]; then
        echo "  可能问题: 缓存未清理或无限增长的Map"
    elif [[ $CLASS_NAME == *ArrayList* ]]; then
        echo "  可能问题: 列表无限增长或查询未分页"
    elif [[ $CLASS_NAME == *byte[]* ]]; then
        echo "  可能问题: 大文件处理或网络传输未使用流"
    elif [[ $CLASS_NAME == *Thread* ]]; then
        echo "  可能问题: 线程池配置过大或线程泄露"
    fi
    echo "---"
done

4.2 在代码库中搜索可疑类

# 假设你在项目根目录
PROJECT_ROOT="/path/to/your/project"

# 搜索使用HashMap且可能有问题的地方
echo "搜索可能的问题代码..."
find $PROJECT_ROOT -name "*.java" -type f | xargs grep -l "static.*HashMap\|static.*ConcurrentHashMap" | head -10

# 搜索缓存相关代码
find $PROJECT_ROOT -name "*.java" -type f | xargs grep -l "Cache\|cache" | head -10

# 搜索大对象创建
find $PROJECT_ROOT -name "*.java" -type f | xargs grep -l "new byte\[.*\]\|ByteBuffer.allocate" | head -10

第五步:验证和复现

5.1 创建最小复现脚本

#!/bin/bash
echo "=== 内存问题复现脚本 ==="

# 1. 提取关键参数
echo "从GC日志提取关键参数:"
LAST_FULL_GC=$(grep "Full GC" gc.log | tail -1)
echo "最后一次Full GC: $LAST_FULL_GC"

# 2. 计算内存增长率
echo -e "\n计算内存增长率:"
grep "Full GC" gc.log | tail -10 | awk '{
    for(i=1;i<=NF;i++) {
        if($i ~ /old=[0-9]+K->[0-9]+K/) {
            match($i, /old=([0-9]+)K->([0-9]+)K/, arr)
            if(last_after > 0) {
                growth = (arr[2] - last_after) / last_after * 100
                printf("增长率: %.2f%% (从 %sK 到 %sK)\n", growth, last_after, arr[2])
            }
            last_after = arr[2]
        }
    }
}'

# 3. 给出复现建议
echo -e "\n复现建议:"
echo "1. 使用以下JVM参数启动测试:"
echo "   -Xmx512m -Xms512m -XX:+HeapDumpOnOutOfMemoryError"
echo "2. 模拟以下操作:"
echo "   - 重复调用最近修改的接口"
echo "   - 大数据量查询"
echo "   - 文件上传下载"

第六步:创建修复方案

6.1 根据分析结果生成修复建议

#!/bin/bash
echo "=== 修复方案生成 ==="

# 读取最大的类
TOP_CLASS=$(head -1 histo_by_size.txt | awk '{print $1}')

echo "问题根因: 发现大量 $TOP_CLASS 对象"

case $TOP_CLASS in
    *HashMap*)
        echo "修复方案:"
        echo "1. 检查静态HashMap是否无限增长"
        echo "2. 添加LRU淘汰策略"
        echo "3. 使用WeakHashMap或ConcurrentHashMap"
        ;;
    *ArrayList*)
        echo "修复方案:"
        echo "1. 查询添加分页"
        echo "2. 设置列表初始大小"
        echo "3. 及时清理不再使用的列表"
        ;;
    *byte*)
        echo "修复方案:"
        echo "1. 大文件使用流处理"
        echo "2. 复用ByteBuffer"
        echo "3. 限制上传文件大小"
        ;;
    *)
        echo "修复方案:"
        echo "1. 检查 $TOP_CLASS 对象的创建位置"
        echo "2. 查看是否有循环创建或静态引用"
        echo "3. 添加对象池或复用机制"
        ;;
esac

第七步:创建完整报告

#!/bin/bash
REPORT="memory_issue_report_$(date +%Y%m%d_%H%M%S).txt"

{
echo "内存问题分析报告"
echo "========================"
echo "生成时间: $(date)"
echo

echo "一、问题现象"
echo "------------"
echo "服务挂掉时间: $(tail -1 gc.log | awk '{print $1}')"
echo "错误信息:"
grep "OutOfMemoryError" gc.log | tail -1
echo

echo "二、GC日志分析"
echo "------------"
echo "1. Full GC次数: $(grep -c "Full GC" gc.log)"
echo "2. 最后5次Full GC:"
grep "Full GC" gc.log | tail -5 | awk '{print "   " $0}'
echo

echo "三、堆转储分析"
echo "------------"
echo "1. 堆转储文件大小: $(du -h heap_dump.hprof | awk '{print $1}')"
echo "2. 内存占用TOP 5:"
head -5 histo_by_size.txt | awk '{print "   " $0}'
echo

echo "四、问题根因"
echo "------------"
echo "主要问题类: $(head -1 histo_by_size.txt | awk '{print $1}')"
echo "实例数量: $(head -1 histo_by_size.txt | awk '{print $2}')"
echo "占用内存: $(head -1 histo_by_size.txt | awk '{print $3/1024}') MB"
echo

echo "五、修复建议"
echo "------------"
# 根据TOP类给出建议
TOP_CLASS=$(head -1 histo_by_size.txt | awk '{print $1}')
if [[ $TOP_CLASS == *HashMap* ]]; then
    cat << EOF
1. 代码层面:
   - 检查所有静态HashMap,添加大小限制
   - 使用Guava Cache或Caffeine替代
   - 添加缓存淘汰策略

2. 配置层面:
   - 调整JVM参数: -XX:NewRatio=3
   - 设置合理堆大小
   - 启用G1垃圾回收器

3. 监控层面:
   - 添加缓存命中率监控
   - 设置内存使用告警阈值
EOF
fi

echo
echo "六、验证方法"
echo "------------"
echo "1. 本地复现: 使用相同JVM参数启动,模拟用户操作"
echo "2. 性能测试: 使用jmeter压测,观察内存增长"
echo "3. 监控验证: 修复后持续监控老年代使用率"

} > $REPORT

echo "报告已生成: $REPORT"

快速排查流程图

开始
  ↓
1. tail -50 gc.log                         # 看死亡时刻
  ↓
2. jmap -histo heap_dump.hprof > histo.txt # 生成对象清单
  ↓
3. sort -k4 -rn histo.txt | head -20       # 找最大的对象
  ↓
4. 判断对象类型:
   - 如果是byte[] → 检查文件/网络操作
   - 如果是HashMap → 检查缓存代码
   - 如果是自定义类 → grep搜索该类
  ↓
5. 结合gc.log看增长趋势                   # 确认是否泄漏
  ↓
6. 修复并验证

最重要的三条命令

# 1. 查看死亡瞬间
tail -100 gc.log | grep -B5 -A5 "OutOfMemoryError\|Full GC"

# 2. 找到罪魁祸首
jmap -histo heap_dump.hprof | sort -k4 -rn | head -20

# 3. 定位到代码
grep -r "可疑类名" /path/to/project --include="*.java"

记住这个排查口诀

  • 看日志(怎么死的)

  • 找对象(谁占的内存)

  • 搜代码(谁创建的)

  • 验增长(是否持续增长)

按照这个流程,即使只有命令行,30分钟内也一定能找到问题根源!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值