目录
正在加载目录…
专栏文章
专栏文章
Java 专栏
1. Java:跨平台语言与生态全景 2. Java 并发:JMM、锁与线程池原理 3. JVM:类加载、内存、GC 与性能诊断 4. Java 性能调优:CPU、内存、锁与 IO 排查 5. Java 新特性速查:从 Lambda 到虚拟线程 6. Java 业务开发:高频陷阱与 Code Review 清单 7. Java 核心知识:集合、并发与 JVM 面试要点 8. AQS 原理:同步队列与加锁解锁全流程 9. JVM 与 Linux 内存:分区、分配与 GC 边界 10. AtomicLong 与 LongAdder:并发计数器选型 11. Java 引用与内存泄漏:六类场景与修复 12. Java 测试实践:JUnit、Mockito 与 Testcontainers 13. Java 并发入门:线程、锁与线程池全景

JVM:类加载、内存、GC 与性能诊断

发布于 2026-06-17 14:40 · 最后编辑于 2026-07-31 15:51 · 字数 6,102 👁 211 次阅读

本文从 JVM 整体架构出发,深入剖析类加载机制、对象内存布局、垃圾回收算法(分代 GC / G1 / ZGC)、即时编译原理,以及 synchronized 锁升级的底层实现,并给出生产环境的诊断工具使用指南。

目录

章节说明
JVM 整体架构运行时数据区、执行引擎
类加载机制加载→链接→初始化、双亲委派
对象内存布局对象头、压缩指针、字段重排列
垃圾回收基础可达性分析、三种回收方式、安全点
分代垃圾回收新生代 Minor GC、老年代、卡表
垃圾回收器Serial/Parallel/CMS/G1/ZGC
即时编译(JIT)C1/C2、分层编译、OSR
synchronized 底层实现偏向锁→轻量级锁→重量级锁
JVM 内存模型Happens-Before、内存屏障
JVM 诊断工具jps/jstat/jmap/jstack/jcmd
JVM 调优实践GC 参数、堆大小、线程栈

JVM 整体架构

../../assets/03 Java 虚拟机/file 20260605123643384

┌─────────────────────────────────────────────────────────────┐
│                       Java 源代码                            │
│                          ↓ javac                             │
│                       字节码 (.class)                        │
└─────────────────────────────────────────────────────────────┘
                           ↓ 类加载
┌─────────────────────────────────────────────────────────────┐
│                      JVM 运行时                              │
│  ┌──────────────────────────────────────────────────────┐   │
│  │               运行时数据区                            │   │
│  │  ┌──────────┐ ┌──────────┐ ┌──────────────────────┐ │   │
│  │  │方法区     │ │  堆      │ │  线程私有区域         │ │   │
│  │  │(元空间)   │ │(GC 管理) │ │  虚拟机栈/本地方法栈  │ │   │
│  │  │类信息/常量│ │对象实例  │ │  程序计数器           │ │   │
│  │  └──────────┘ └──────────┘ └──────────────────────┘ │   │
│  └──────────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────────┐   │
│  │               执行引擎                                │   │
│  │  解释器 → C1 编译器 → C2 编译器(分层编译)           │   │
│  └──────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

运行时数据区

区域线程内容OOM 风险
堆(Heap)共享所有对象实例、数组是(最常见)
方法区(元空间)共享类信息、常量池、静态变量是(类加载过多)
虚拟机栈私有栈帧(局部变量表、操作数栈、动态链接)StackOverflow
本地方法栈私有Native 方法栈帧StackOverflow
程序计数器私有当前执行字节码行号

类加载机制

../../assets/03 Java 虚拟机/file 20260605123643832

三个阶段

flowchart LR
    A["加载<br/>查找字节流<br/>创建 Class 对象"] --> B["链接"]
    B --> B1["验证<br/>约束检查"]
    B --> B2["准备<br/>静态字段分配内存"]
    B --> B3["解析<br/>符号引用→实际引用"]
    B1 & B2 & B3 --> C["初始化<br/>执行 &lt;clinit&gt;"]

加载:通过类加载器查找字节流(.class 文件、网络、动态生成),创建 Class 对象。

链接

  • 验证:确保字节码满足 JVM 约束(格式、语义、字节码、符号引用验证)
  • 准备:为静态字段分配内存并赋零值(不是代码中的初始值)
  • 解析:将符号引用替换为直接引用(内存地址)

初始化:执行 <clinit> 方法(静态代码块 + 静态字段赋值),JVM 保证线程安全,且只执行一次。

触发初始化的时机

  1. new 指令新建对象
  2. 调用静态方法
  3. 访问静态字段(非 final 常量)
  4. 子类初始化触发父类初始化
  5. 反射调用
  6. JVM 启动时的主类
// 利用类初始化的线程安全性实现懒加载单例
public class Singleton {
    private Singleton() {}

    private static class LazyHolder {
        // 只有调用 getInstance() 时才触发 LazyHolder 的初始化
        static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

双亲委派模型

每个类加载器收到加载请求时,先委派给父加载器,父加载器无法完成时才自己尝试。

graph TD
    A["Bootstrap ClassLoader<br/>C++ 实现,加载 java.base 等核心类"] --> B["Platform ClassLoader<br/>Java 9+,加载 Java SE 平台模块"]
    B --> C["Application ClassLoader<br/>加载 classpath 下的应用类"]
    C --> D["自定义 ClassLoader"]

意义

  • 防止核心类被覆盖(如自己写一个 java.lang.String 不会生效)
  • 同一类由不同 ClassLoader 加载会得到两个不同的 Class 对象(可用于实现热部署、插件隔离)

对象内存布局

对象头(Object Header)

每个 Java 对象的内存开头是对象头,包含:

字段大小内容
Mark Word64 bit哈希码、GC 年龄、锁状态标志
类型指针64 bit(未压缩)/ 32 bit(压缩)指向类元数据
数组长度32 bit(仅数组)数组大小

开启压缩指针(默认):类型指针从 64 bit 压缩为 32 bit,对象头从 16 字节降至 12 字节。

# 压缩指针相关参数
-XX:+UseCompressedOops        # 开启(默认)
-XX:ObjectAlignmentInBytes=8  # 内存对齐粒度(默认 8 字节)

堆超过 32GB 时,压缩指针自动关闭(32 bit × 8 字节对齐 = 32GB 寻址空间)。

字段重排列

JVM 会对字段进行重排列以满足内存对齐,减少填充浪费:

# 示例:启用压缩指针时,class B extends A 的内存布局
OFFSET  SIZE   TYPE
     0     4        (object header - mark word 低 32 bit)
     4     4        (object header - mark word 高 32 bit)
     8     4        (object header - 类型指针)
    12     4    int A.i        ← JVM 将 int 字段提前填充 4 字节空缺
    16     8   long A.l
    24     8   long B.l
    32     4    int B.i
    36     4        (padding)

内存浪费分析

// Integer 对象的内存占用
// 对象头:12 字节(压缩指针)
// int value:4 字节
// padding:0 字节(刚好 16 字节对齐)
// 总计:16 字节,而 int 本身只有 4 字节
// 额外开销高达 300%!
Integer i = 42;  // 16 字节 vs 基本类型 int 的 4 字节

垃圾回收基础

可达性分析

JVM 以一系列 GC Roots 为起点,遍历所有可达对象(标记为存活),未被遍历到的对象即为垃圾。

GC Roots 包括

  • 虚拟机栈中引用的对象(局部变量)
  • 已加载类的静态变量
  • JNI handles
  • 已启动且未停止的 Java 线程

可达性分析解决了引用计数法无法处理循环引用的问题。

三种回收方式

方式原理优点缺点
清除(Sweep)标记死亡对象,记入空闲列表简单内存碎片、分配效率低
压缩(Compact)存活对象移至内存起始位置无碎片,指针碰撞分配移动对象开销大
复制(Copy)内存分两半,存活对象复制到另一半无碎片,分配快空间利用率只有 50%

现代 GC 综合三种方式:新生代用复制(大部分对象死亡,复制代价低),老年代用标记-压缩

Stop-the-World 与安全点

GC 标记阶段需要暂停所有应用线程(Stop-the-World),防止标记过程中对象引用被修改。

JVM 通过**安全点(Safepoint)**机制实现 STW:应用线程在安全点检测到 GC 请求后主动挂起。安全点通常位于:

  • 方法返回前
  • 循环回边(非计数循环)
  • 异常抛出处

分代垃圾回收

../../assets/03 Java 虚拟机/file 20260605123643602

分代假设

大部分对象"朝生夕死",少部分对象长期存活。基于此,JVM 将堆分为新生代和老年代,针对不同代使用不同 GC 算法。

堆结构

堆
├── 新生代(Young Generation)
│   ├── Eden 区(新对象分配,约 80%)
│   ├── Survivor From(S0)
│   └── Survivor To(S1,始终保持空)
└── 老年代(Old Generation)

Minor GC(新生代 GC)

触发条件:Eden 区空间耗尽。

执行过程:

  1. 标记 Eden + S0(From)中的存活对象
  2. 将存活对象复制到 S1(To)
  3. 交换 S0 和 S1 的角色
  4. 复制次数超过阈值(默认 15)的对象晋升老年代
-XX:MaxTenuringThreshold=15   # 晋升老年代的年龄阈值
-XX:SurvivorRatio=8           # Eden:Survivor = 8:1:1
-XX:TargetSurvivorRatio=50    # Survivor 使用率超过 50% 时提前晋升

TLAB(Thread-Local Allocation Buffer):每个线程预先申请一块私有内存区域,对象分配通过指针碰撞完成,无需加锁,大幅提升分配效率。

卡表(Card Table)

Minor GC 只扫描新生代,但老年代对象可能引用新生代对象。为避免全堆扫描,HotSpot 引入卡表:

  • 将整个堆划分为 512 字节的"卡"
  • 维护卡表数组,标记可能含有跨代引用的"脏卡"
  • Minor GC 时只扫描脏卡,而非整个老年代

写屏障:每次引用赋值时,JIT 插入一条指令将对应卡标记为脏:

CARD_TABLE[address >> 9] = DIRTY;  // 相当于 address / 512

Full GC(老年代 GC)

触发条件:老年代空间不足、显式调用 System.gc()、晋升失败等。

Full GC 代价极高(全堆扫描 + 长时间 STW),应尽量避免。

垃圾回收器

垃圾回收器概览

回收器适用代算法特点
Serial新生代标记-复制单线程,Client 模式默认
ParNew新生代标记-复制多线程版 Serial,配合 CMS
Parallel Scavenge新生代标记-复制注重吞吐率
Serial Old老年代标记-压缩单线程
Parallel Old老年代标记-压缩多线程,注重吞吐率
CMS老年代标记-清除并发收集,低延迟(Java 9 废弃)
G1全堆标记-压缩可预测停顿,Java 9+ 默认
ZGC全堆标记-压缩停顿 < 10ms(Java 11+)

G1(Garbage First)

G1 打破了传统分代的固定边界,将堆划分为大量等大小的 Region(默认 1~32MB),每个 Region 可动态充当 Eden/Survivor/Old/Humongous(大对象)。

flowchart TD
    A["G1 GC 周期"] --> B["Young GC<br/>回收 Eden + Survivor Region"]
    A --> C["Mixed GC<br/>回收 Young + 部分 Old Region"]
    A --> D["Full GC<br/>退化兜底,尽量避免"]
    style D fill:#fcc,stroke:#c00

G1 的核心优势

  • 可设置最大停顿时间目标(-XX:MaxGCPauseMillis=200
  • 优先回收垃圾最多的 Region(Garbage First 的由来)
  • 并发标记阶段与应用线程同时运行

关键内部机制

  • RSet(记忆集):每个 Region 维护一个 Hash Table,记录"哪些其他 Region 引用了我"(points-into)。YGC 时只扫描年轻代的 RSet,避免全量扫描老年代。
  • SATB(快照标记):基于三色标记,通过 pre-write barrier 记录被替换的旧引用,防止并发标记时漏标白对象。代价是产生少量 float garbage。
  • 停顿预测模型:基于衰减标准偏差,用历史数据预测本次 GC 耗时,动态决定纳入 CSet 的 Region 数量以满足停顿目标。

Mixed GC 关键触发参数

参数说明默认值
InitiatingHeapOccupancyPercent触发并发标记的堆占用率45%
G1HeapWastePercent垃圾占比阈值,达到才触发 Mixed GC5%
G1MixedGCLiveThresholdPercent老年代 Region 存活率上限,超过则不纳入 CSet85%
G1MixedGCCountTarget一次并发标记后最多执行 Mixed GC 次数8
# G1 常用参数
-XX:+UseG1GC                    # 启用 G1(Java 9+ 默认)
-XX:MaxGCPauseMillis=200        # 目标最大停顿时间(毫秒)
-XX:G1HeapRegionSize=4m         # Region 大小(1~32MB,2 的幂次)
-XX:G1NewSizePercent=5          # 新生代最小占比
-XX:G1MaxNewSizePercent=60      # 新生代最大占比
-XX:InitiatingHeapOccupancyPercent=45  # 触发并发标记的堆占用率

ZGC(Java 11+)

ZGC 的目标是将 GC 停顿控制在 10ms 以内,且停顿时间不随堆大小增长(支持 8MB~4TB 堆)。

核心技术

  • 染色指针(Colored Pointers):将对象存活元数据存储在指针第 42~45 位,无需访问对象头即可获取 GC 状态
  • 读屏障:应用线程读取堆引用时自动触发,将旧地址更新为新地址,保障并发转移正确性

ZGC vs G1 对比

维度G1ZGC
转移阶段完全 STW并发执行
STW 次数多次仅 3 次(初始标记、再标记、初始转移)
停顿随堆增长
吞吐量略低(读屏障开销)
适用场景通用低延迟优先

美团实践经验(TP999 下降 18%~74%):

问题现象根本原因解决方案
流量突增时内存分配阻塞自适应触发 GC 不及时-XX:ZCollectionInterval=5(固定间隔触发)
压测时吞吐瓶颈并发回收线程不足增大 -XX:ConcGCThreads
单次 GC 停顿 30ms上万个 ClassLoader 导致 GC Roots 过大复用单一 ClassLoader

⚠️ ZGC 适用场景:低延迟服务(TP999 < 200ms 收益最大)。吞吐优先场景建议保留 G1,单代设计 + 读屏障会降低吞吐量。

-XX:+UseZGC                     # 启用 ZGC(Java 15+ 正式版)
-Xmx16g -Xms16g                 # 建议固定堆大小,避免扩缩容
-XX:ZCollectionInterval=5       # 固定 GC 间隔(秒),防止流量突增时分配阻塞
-XX:ConcGCThreads=4             # 并发 GC 线程数(默认 CPU 核数/4)

方法调用机制

五种字节码调用指令

指令用途分派时机
invokestatic静态方法编译期确定(非虚方法)
invokespecial构造器 <init>、私有方法、super()编译期确定(非虚方法)
invokevirtual所有虚方法(含 final,虽然 final 不可重写)运行时多态分派
invokeinterface接口方法运行时确定实现类
invokedynamic动态调用(Lambda、Groovy 等),分派逻辑由用户引导方法决定运行时动态解析

非虚方法(编译期确定):invokestatic + invokespecial 调用的方法,以及 final 方法(虽用 invokevirtual)。 虚方法:其余所有方法,运行时通过动态分派(vtable)确定实际调用版本。

解析 vs 分派

  • 解析:类加载阶段将符号引用转为直接引用,仅适用于非虚方法(调用目标编译期可知且运行期不变)
  • 静态分派:编译期根据静态类型选择方法版本(方法重载的实现基础)
  • 动态分派:运行时根据对象实际类型确定方法版本(方法重写/多态的实现基础),invokevirtual 每次都要在 vtable 中查找

即时编译(JIT)

分层编译

Java 8+ 默认开启分层编译(-XX:+TieredCompilation),结合 C1 的快速编译和 C2 的深度优化:

层次执行方式特点
0解释执行启动快,无编译开销
1C1,无 profiling快速编译,适合简单方法
2C1,部分 profiling收集调用次数、循环次数
3C1,全量 profiling收集分支预测、类型信息
4C2,深度优化内联、逃逸分析、循环展开等

典型编译路径

解释执行(0) → C1 全量 profiling(3) → C2 深度优化(4)

C2 代码比 C1 代码性能高 30% 以上。

触发条件

方法调用次数 + 循环回边次数超过阈值时触发编译。阈值随编译队列长度动态调整(队列越长,阈值越高,防止 C2 过载)。

OSR(On-Stack-Replacement)

OSR 允许在方法执行过程中(而非方法入口)切换到编译后的代码,专门解决"单次调用但包含热循环"的场景:

// 这种方法只会被调用一次,但循环是热点
// OSR 会在循环执行到一定次数后,直接替换栈帧,切换到编译代码
public static void main(String[] args) {
    long sum = 0;
    for (int i = 0; i < 1_000_000_000; i++) {  // 热循环
        sum += i;
    }
    System.out.println(sum);
}

主要优化技术

优化说明
方法内联将被调用方法的代码直接嵌入调用处,消除调用开销
逃逸分析判断对象是否逃逸出方法/线程,若不逃逸则栈上分配或标量替换
循环展开减少循环控制指令,提升指令级并行度
锁消除逃逸分析证明锁不会被多线程竞争时,消除加锁操作
锁粗化将多次连续的加锁合并为一次,减少加锁次数
-XX:+PrintCompilation           # 打印 JIT 编译情况
-XX:+PrintInlining              # 打印内联决策
-XX:CompileThreshold=10000      # 触发 C2 的调用次数阈值(关闭分层编译时有效)

synchronized 底层实现

字节码层面

// synchronized 代码块
public void foo(Object lock) {
    synchronized (lock) {
        lock.hashCode();
    }
}
// 编译为:monitorenter ... monitorexit(正常路径)
//                    ... monitorexit(异常路径)
// JVM 保证两条路径都能解锁

锁状态与 Mark Word

对象头的 Mark Word 最后几位标识锁状态:

状态标志位Mark Word 内容
无锁01哈希码、GC 年龄
偏向锁101线程 ID、epoch、GC 年龄
轻量级锁00指向栈帧中锁记录的指针
重量级锁10指向 Monitor 对象的指针
GC 标记11GC 算法使用

锁升级详解

偏向锁(针对单线程反复获取同一把锁):

  • 首次加锁:CAS 将线程 ID 写入 Mark Word,设置标志位为 101
  • 后续加锁:检查 Mark Word 中的线程 ID 是否匹配,匹配则直接返回(零 CAS)
  • 撤销代价高:需要等待安全点,遍历所有线程栈

轻量级锁(针对多线程在不同时间段获取同一把锁):

  • 加锁:在当前线程栈帧中分配锁记录,CAS 将 Mark Word 替换为锁记录地址
  • 解锁:CAS 将 Mark Word 还原
  • 竞争失败:膨胀为重量级锁

重量级锁(针对多线程同时竞争):

  • 依赖操作系统 mutex(pthread_mutex)
  • 阻塞/唤醒需要从用户态切换到内核态,开销极大
  • 引入自适应自旋:在阻塞前先自旋,根据历史成功率动态调整自旋次数
sequenceDiagram
    participant T1 as 线程 T1
    participant T2 as 线程 T2
    participant Lock as 锁对象

    T1->>Lock: 首次加锁(CAS 设置偏向锁)
    T1->>Lock: 再次加锁(检查线程 ID 匹配,直接返回)
    T2->>Lock: 请求加锁(线程 ID 不匹配)
    Note over Lock: 撤销偏向锁,升级为轻量级锁
    T2->>Lock: CAS 替换 Mark Word
    T1->>Lock: 也尝试 CAS(竞争)
    Note over Lock: CAS 失败,升级为重量级锁
    T1->>Lock: 阻塞等待

JVM 内存模型

Happens-Before 与内存屏障

JVM 通过内存屏障实现 Happens-Before 规则,禁止特定的指令重排序:

屏障类型禁止重排序对应场景
LoadLoad读操作不能被重排到另一读之前volatile 读后的普通读
StoreStore写操作不能被重排到另一写之前volatile 写前的普通写
LoadStore读操作不能被重排到写之前volatile 读后的普通写
StoreLoad写操作不能被重排到读之前volatile 写后的普通读(开销最大)

在 x86_64 架构上,只有 StoreLoad 屏障需要真正插入指令(lock add [rsp], 0),其他三种是空操作。

volatile 字段的底层实现

  • 写操作后插入 StoreLoad 屏障(强制刷新写缓存至主内存)
  • 读操作前插入 LoadLoad 屏障(从主内存读取最新值)
  • volatile 字段不能分配到寄存器(每次都直接读写内存)

数据竞争示例

int a = 0, b = 0;

// 线程 1
void method1() {
    int r2 = a;  // 读 a
    b = 1;       // 写 b
}

// 线程 2
void method2() {
    int r1 = b;  // 读 b
    a = 2;       // 写 a
}

// 没有同步时,(r1, r2) 可能出现 (1, 2)!
// 解决:将 b 声明为 volatile,利用 volatile 的 Happens-Before 规则
volatile int b = 0;
// 现在 b=1 HB r1=b,再加上程序顺序规则和传递性,r2=a HB a=2
// 因此 (1, 2) 不可能出现

JVM 诊断工具

jps:查看 Java 进程

jps -mlv
# 输出:PID 主类名 main参数 JVM参数
# 18331 com.example.Foo Hello World -Xmx512m

jstat:GC 统计

# 每隔 1 秒打印一次 GC 数据,共打印 10 次
jstat -gc <pid> 1s 10

# 关键列说明
# S0C/S1C:Survivor 0/1 容量
# S0U/S1U:Survivor 0/1 已用
# EC/EU:Eden 容量/已用
# OC/OU:老年代容量/已用
# YGC/YGCT:Minor GC 次数/总时间
# FGC/FGCT:Full GC 次数/总时间
# GCT:GC 总时间

# 判断是否内存泄漏:长时间观察 OU(老年代已用)是否持续上涨

jmap:堆分析

# 查看堆中对象统计(按内存从多到少排序)
jmap -histo <pid> | head -30

# 导出堆快照(用 MAT/VisualVM 分析)
jmap -dump:live,format=b,file=heap.hprof <pid>

# 也可以通过 JVM 参数在 OOM 时自动 dump
# -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.hprof

jstack:线程分析

# 打印所有线程的栈轨迹
jstack <pid>

# 常见线程状态
# RUNNABLE:正在执行(可能是 CPU 密集或死循环)
# BLOCKED:等待 synchronized 锁
# WAITING:等待 Object.wait() / LockSupport.park()
# TIMED_WAITING:有超时的等待
# TERMINATED:已结束

# jstack 会自动检测并打印死锁信息
# Found one Java-level deadlock:

jinfo:查看/修改 JVM 参数

# 查看当前 JVM 参数
jinfo <pid>

# 动态修改 manageable 参数(无需重启)
jinfo -flag +HeapDumpAfterFullGC <pid>
jinfo -flag HeapDumpPath=/tmp <pid>

jcmd:瑞士军刀

# 列出所有可用命令
jcmd <pid> help

# 常用子命令
jcmd <pid> GC.heap_info          # 堆信息
jcmd <pid> GC.class_histogram    # 对象统计
jcmd <pid> GC.heap_dump /tmp/heap.hprof  # 导出堆
jcmd <pid> Thread.print          # 线程栈(等同 jstack)
jcmd <pid> VM.flags              # JVM 参数
jcmd <pid> VM.uptime             # 运行时间

JVM 调优实践

堆大小设置

-Xms4g          # 初始堆大小
-Xmx4g          # 最大堆大小(建议与 Xms 相同,避免扩缩容开销)
-Xmn1g          # 新生代大小(G1 下不建议手动设置)
-XX:MetaspaceSize=256m      # 元空间初始大小
-XX:MaxMetaspaceSize=512m   # 元空间最大大小

GC 选择建议

场景推荐 GC关键参数
低延迟(< 200ms)G1-XX:MaxGCPauseMillis=100
极低延迟(< 10ms)ZGC(Java 15+)-XX:+UseZGC
高吞吐(批处理)Parallel GC-XX:+UseParallelGC
堆 < 4GB 的小应用G1 或 Serial默认即可

常见 GC 问题排查

现象可能原因解决方案
Full GC 频繁老年代空间不足、大对象直接晋升增大堆、调整晋升阈值
Minor GC 后存活对象多Survivor 区太小,对象提前晋升老年代增大新生代或 Survivor 比例
GC 停顿过长堆太大、GC 线程数不足换 G1/ZGC,调整 MaxGCPauseMillis
元空间 OOM动态生成大量类增大 MaxMetaspaceSize,检查类加载泄漏
堆 OOM内存泄漏或堆确实不够jmap -dump 后用 MAT 分析

常用 GC 日志参数

# Java 9+ 统一日志格式
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags:filecount=5,filesize=20m

# Java 8
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/var/log/gc.log
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20m

性能调优 Checklist

  • [ ] 确认 GC 停顿时间是否满足 SLA(jstat -gc 观察 GCT 占比)
  • [ ] 确认老年代增长趋势(是否内存泄漏)
  • [ ] 确认 Full GC 频率(超过 1 次/小时需关注)
  • [ ] 确认堆大小是否合理(OU 稳定后应 < OC 的 70%)
  • [ ] 确认元空间大小是否设置上限(防止无限增长)
  • [ ] 确认线程池配置(线程数、队列大小、拒绝策略)
  • [ ] 确认是否有大对象(> G1HeapRegionSize/2)直接进老年代

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论