JVM | 从内存模型到辣鸡回收

67 阅读8分钟

前言

当前文章只代表个人初学理解,若有问题,欢迎指出,蟹蟹~

一、内存模型

区域划分

  1. 堆: 存放实例对象;其中又被划分成:新生代、老年代,默认比例1:2,新生代中又区分Eden区、Surivivor区(survivor From、Survivor to),默认比例8:1:1。堆也是主要进行垃圾回收的区域。
  2. 方法区(元数据区): 存放虚拟机加载的:类型信息,域(Field)信息,方法(Method)信息,常量,静态变量,即时编译器编译后的代码缓存。
  3. 程序计数器: 记录线程执行的字节码行号。
  4. 虚拟机栈: 保存了每次方法调用的信息。每一次执行方法都会往虚拟机栈中压入一个栈帧(每个栈帧中具有:局部变量表、操作数栈、动态链接、方法出口)。
  5. 本地方法栈: 存放native方法。

总结

image.png 图来自

Java类加载机制是JVM在运行时动态加载类的核心机制,其核心流程分为加载、验证、准备、解析、初始化五个阶段。以下是各环节的详细功能说明:


二、类加载机制

1️⃣ 加载(Loading)

  • 功能:将类的.class文件二进制数据读入内存,并生成对应的java.lang.Class对象。
  • 核心步骤
    • 通过类的全限定名(如java.lang.String)获取字节流(如从JAR包、网络、动态代理生成等)。
    • 将字节流转换为方法区的运行时数据结构。
    • 在堆内存中创建Class对象,作为方法区数据的访问入口。
  • 类加载器
    • Bootstrap ClassLoader:加载JRE核心库(如rt.jar)。
    • Extension ClassLoader:加载jre/lib/ext目录下的扩展类。
    • Application ClassLoader:加载用户类路径(ClassPath)的类。
    • 自定义类加载器:实现特殊加载需求(如热部署、模块化)。

2️⃣ 验证(Verification)

  • 功能:确保字节流符合JVM规范,防止恶意代码破坏虚拟机安全。
  • 验证内容
    • 文件格式验证:检查魔数(0xCAFEBABE)、版本号等。
    • 元数据验证:检查语义(如是否有父类、是否继承final类)。
    • 字节码验证:确保方法逻辑合法(如类型转换、跳转指令)。
    • 符号引用验证:检查引用的类、方法、字段是否存在(发生在解析阶段)。

3️⃣ 准备(Preparation)

  • 功能:为类的静态变量分配内存并设置初始值。
  • 关键点
    • 仅分配类变量(static变量),实例变量在对象实例化时分配。
    • 初始值为零值(如int初始化为0booleanfalse)。
    • 若变量是static final常量(如public static final int x = 123),直接赋值为定义值。

4️⃣ 解析(Resolution)

  • 功能:将常量池中的符号引用替换为直接引用。
  • 符号引用:用一组符号描述引用的目标(如java.lang.Object)。
  • 直接引用:指向目标的指针、偏移量或句柄。
  • 解析目标
    • 类/接口解析(如com.example.MyClass)。
    • 字段解析(如MyClass.myField)。
    • 方法解析(如MyClass.myMethod())。

5️⃣ 初始化(Initialization)

  • 功能:执行类构造器<clinit>()方法,完成静态变量赋值和静态代码块逻辑。
  • 关键规则
    • JVM保证子类初始化前,父类已初始化。
    • <clinit>()由编译器自动生成,合并所有静态变量赋值和静态代码块。
    • 多线程环境下,JVM会加锁确保仅初始化一次。

类加载的触发时机

  • 主动引用(触发初始化):
    • new对象、访问静态变量/方法(非final常量)、反射调用类。
    • 初始化子类时,父类未初始化会触发父类初始化。
  • 被动引用(不触发初始化):
    • 通过子类引用父类的静态字段。
    • 定义类的数组(如MyClass[] arr = new MyClass[10])。
    • 访问static final常量(值在编译期确定)。

双亲委派模型(Parent Delegation)

  • 机制:类加载请求优先委派给父加载器处理,若父类无法完成,子加载器才尝试加载。
  • 目的
    • 避免重复加载,确保核心类(如java.lang.Object)唯一性。
    • 防止用户自定义类替换核心类(安全沙箱)。

类卸载(Unloading)

  • 条件:类的Class对象无引用,且对应的类加载器被回收。
  • 场景:由JVM的垃圾回收机制自动完成,常见于动态生成的类(如JSP、OSGi)。

示例流程

public class MyClass {
    public static int x = 10; // 准备阶段x=0,初始化阶段x=10
    static {
        System.out.println("Initialized!"); // 初始化阶段执行
    }
}
  1. 加载:找到MyClass.class并创建Class对象。
  2. 验证:检查字节码合法性。
  3. 准备:为x分配内存并赋初始值0
  4. 解析:处理System.out的符号引用。
  5. 初始化:执行<clinit>(),将x设为10,打印“Initialized!”。

通过这一机制,JVM实现了类的动态加载、安全隔离和高效内存管理。

三、垃圾回收

以下是Java垃圾回收(Garbage Collection, GC)机制的详细解析,涵盖核心原理、算法及实践要点:


垃圾回收的本质

目标:自动回收不再使用的对象内存,避免内存泄漏,减少手动内存管理的复杂性。


关键概念

  1. 堆内存结构(以HotSpot VM为例):

    • 新生代(Young Generation)
      • Eden区:对象初次分配区域
      • Survivor区(From/To):存活对象过渡区
    • 老年代(Old Generation):长期存活对象
    • 元空间(Metaspace):类元信息(替代永久代)
  2. 对象存活判定

    • 引用计数法(Java未采用):循环引用问题
    • 可达性分析(Java核心算法):
      • 从GC Roots(栈局部变量、静态变量、JNI引用等)出发,不可达对象标记为垃圾

经典垃圾回收算法

算法过程优点缺点应用场景
标记-清除1. 标记存活对象
2. 清除未标记对象
简单内存碎片CMS老年代回收
复制算法将存活对象复制到新内存区无碎片,高效内存利用率50%新生代(Eden→Survivor)
标记-整理标记后压缩存活对象到内存一端无碎片移动对象开销大Serial Old、G1
分代收集新生代用复制,老年代用标记-清除/整理针对不同生命周期优化需配合不同算法实现所有现代JVM

垃圾收集器对比

收集器工作模式特点适用场景
Serial单线程简单高效,全程STW客户端小内存应用
Parallel多线程吞吐量优先(JDK8默认)后台计算型应用
CMS并发低停顿,分四阶段:初始标记→并发标记→重新标记→并发清除响应优先的Web应用
G1分区+并发将堆划分为Region,可预测停顿(JDK9+默认)大内存混合负载
ZGC并发+染色指针亚毫秒级停顿(<10ms),支持TB级堆超低延迟金融/电信系统
Shenandoah并发与ZGC竞争,低延迟且无需额外硬件支持通用低延迟场景

内存分配与回收策略

  1. 对象优先在Eden分配
    • 新生代内存不足时触发 Minor GC
  2. 大对象直接进老年代
    • 通过-XX:PretenureSizeThreshold设定阈值(默认0,由收集器决定)
  3. 长期存活对象晋升老年代
    • 年龄计数器(-XX:MaxTenuringThreshold,默认15)
  4. 动态年龄判定
    • Survivor区中相同年龄对象总大小超过Survivor一半时,≥该年龄的对象直接晋升

GC触发条件

GC类型触发条件
Minor GCEden区满时
Full GC1. 老年代空间不足
2. 方法区(元空间)不足
3. 调用System.gc()

调优关键指标

  • 吞吐量:用户代码运行时间 / (用户代码时间 + GC时间)
    (通常要求 > 95%)
  • 停顿时间:单次GC导致应用暂停的时长
    (CMS/G1目标:100-200ms;ZGC目标:<10ms)
  • 内存占用:堆空间大小与系统物理内存的平衡

常用JVM参数示例

// 基础配置
-Xms4g -Xmx4g                 // 堆初始/最大内存
-XX:NewRatio=2                // 新生代:老年代=1:2
-XX:SurvivorRatio=8           // Eden:Survivor=8:1:1

// 收集器选择
-XX:+UseG1GC                  // 启用G1
-XX:+UseConcMarkSweepGC       // 启用CMS

// GC日志
-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-Xloggc:/path/to/gc.log

GC优化实践

  1. 避免过早晋升老年代
    • 增加新生代大小(-Xmn
    • 增大Survivor区(调整-XX:SurvivorRatio
  2. 降低Full GC频率
    • 监控老年代使用率(jstat -gcutil
    • 避免大量短生命周期大对象
  3. 选择合适收集器
    • 小堆(<4G):Parallel GC
    • 中等堆(4-16G):G1 GC
    • 超大堆(>16G):ZGC/Shenandoah

监控工具推荐

  • 命令行jstatjmapjcmd
  • 图形工具:VisualVM、JConsole、GCViewer
  • APM工具:Prometheus+Grafana(配合JMX exporter)

示例分析

// 内存泄漏典型场景:静态集合持有对象引用
public class MemoryLeak {
    static List<byte[]> list = new ArrayList<>();
    public static void main(String[] args) {
        while(true) {
            list.add(new byte[1024 * 1024]); // 不断向老年代填充对象
        }
    }
}
  • 现象:频繁Full GC,最终OutOfMemoryError
  • 解决方案:检查长生命周期集合的清理逻辑

通过合理配置垃圾回收策略,可以在吞吐量、延迟、内存占用之间取得最佳平衡,这是Java高性能应用调优的核心技能之一。