07-Java语言核心-JVM原理-JVM对象模型详解

JVM对象模型详解

一、知识概述

在Java虚拟机中,对象的创建、存储和访问是Java程序运行的基础。理解JVM对象模型有助于我们编写更高效的代码,排查内存相关问题,以及进行性能优化。

对象的内存布局

在HotSpot虚拟机中,对象在内存中的存储布局可以分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。

复制代码
┌─────────────────────────────────────────────────────────────┐
│                     Java 对象内存布局                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │                    对象头 (Header)                    │   │
│  ├─────────────────────────────────────────────────────┤   │
│  │  ┌─────────────────────────────────────────────┐    │   │
│  │  │            Mark Word (标记字)                 │    │   │
│  │  │            32位: 4字节 / 64位: 8字节          │    │   │
│  │  │  存储: hashCode、锁状态、GC年龄等             │    │   │
│  │  └─────────────────────────────────────────────┘    │   │
│  │  ┌─────────────────────────────────────────────┐    │   │
│  │  │         Class Pointer (类型指针)             │    │   │
│  │  │            指向方法区的类元数据               │    │   │
│  │  │         开启指针压缩: 4字节,否则: 8字节      │    │   │
│  │  └─────────────────────────────────────────────┘    │   │
│  │  ┌─────────────────────────────────────────────┐    │   │
│  │  │         Array Length (数组长度)              │    │   │
│  │  │            仅数组对象有此字段                 │    │   │
│  │  └─────────────────────────────────────────────┘    │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │              实例数据 (Instance Data)                │   │
│  │                                                      │   │
│  │  字段内容(继承的 + 自身的)                         │   │
│  │  - 基本类型: 按大小存储                              │   │
│  │  - 引用类型: 存储 reference                          │   │
│  │                                                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │             对齐填充 (Padding)                       │   │
│  │                                                      │   │
│  │  保证对象大小是8字节的整数倍                         │   │
│  │                                                      │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

对象大小计算

类型 大小 说明
对象头(普通对象) 12字节(64位开启压缩) Mark Word(8) + Class Pointer(4)
对象头(数组对象) 16字节(64位开启压缩) 额外4字节数组长度
引用 4字节(开启压缩)/ 8字节 对象引用
int 4字节 整型
long 8字节 长整型
对齐填充 0~7字节 凑整到8的倍数

二、知识点详细讲解

2.1 对象头详解

对象头包含两部分信息:Mark Word(标记字)和类型指针。如果是数组,还有一个记录数组长度的部分。

Mark Word结构

Mark Word在32位和64位虚拟机中的长度不同,但存储的信息相似。它在不同的锁状态下存储的内容不同:

复制代码
32位 JVM Mark Word 结构:

┌───────────────────────────────────────────────────────────────┐
│                        Mark Word (32 bits)                     │
├───────────────────────────────────────────────────────────────┤
│                                                                 │
│  无锁状态 (Normal):                                             │
│  ┌─────────────────────────────┬─────┬─────┬─────┐            │
│  │      hashcode:25            │ age:4│ 0:1 │ 01  │            │
│  └─────────────────────────────┴─────┴─────┴─────┘            │
│                                                                 │
│  偏向锁 (Biased):                                               │
│  ┌─────────────────────────────┬─────┬─────┬─────┐            │
│  │    thread:23 | epoch:2      │ age:4│ 1:1 │ 01  │            │
│  └─────────────────────────────┴─────┴─────┴─────┘            │
│                                                                 │
│  轻量级锁 (Lightweight Locked):                                 │
│  ┌─────────────────────────────────────────┬─────┐            │
│  │          ptr_to_lock_record:30           │ 00  │            │
│  └─────────────────────────────────────────┴─────┘            │
│                                                                 │
│  重量级锁 (Heavyweight Locked):                                 │
│  ┌─────────────────────────────────────────┬─────┐            │
│  │          ptr_to_heavyweight_monitor:30  │ 10  │            │
│  └─────────────────────────────────────────┴─────┘            │
│                                                                 │
│  GC标记 (Marked for GC):                                        │
│  ┌─────────────────────────────────────────┬─────┐            │
│  │              (空)                        │ 11  │            │
│  └─────────────────────────────────────────┴─────┘            │
│                                                                 │
└───────────────────────────────────────────────────────────────┘

字段说明:
  hashcode: 对象的哈希码
  age: 对象的GC年龄(4位,最大15)
  thread: 持有偏向锁的线程ID
  epoch: 偏向锁的时间戳
  ptr_to_lock_record: 栈中锁记录的指针
  ptr_to_heavyweight_monitor: 对象监视器的指针
  最后2位: 锁状态标记
使用JOL工具分析对象
java 复制代码
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

/**
 * 使用JOL(Java Object Layout)分析对象布局
 * 依赖: org.openjdk.jol:jol-core:0.16
 */
public class ObjectLayoutDemo {
    
    public static void main(String[] args) {
        // 打印JVM信息
        System.out.println("=== JVM信息 ===");
        System.out.println(VM.current().details());
        System.out.println();
        
        // === 1. 空对象 ===
        System.out.println("=== 空对象布局 ===");
        EmptyObject empty = new EmptyObject();
        System.out.println(ClassLayout.parseInstance(empty).toPrintable());
        
        // === 2. 基本类型字段 ===
        System.out.println("\n=== 基本类型字段 ===");
        PrimitiveFields primitives = new PrimitiveFields();
        System.out.println(ClassLayout.parseInstance(primitives).toPrintable());
        
        // === 3. 引用类型字段 ===
        System.out.println("\n=== 引用类型字段 ===");
        ReferenceFields references = new ReferenceFields();
        System.out.println(ClassLayout.parseInstance(references).toPrintable());
        
        // === 4. 数组对象 ===
        System.out.println("\n=== 数组对象布局 ===");
        int[] intArray = new int[5];
        System.out.println(ClassLayout.parseInstance(intArray).toPrintable());
        
        Object[] objArray = new Object[5];
        System.out.println(ClassLayout.parseInstance(objArray).toPrintable());
        
        // === 5. 继承关系 ===
        System.out.println("\n=== 继承关系 ===");
        ChildClass child = new ChildClass();
        System.out.println(ClassLayout.parseInstance(child).toPrintable());
        
        // === 6. 对象大小计算 ===
        System.out.println("\n=== 对象大小 ===");
        System.out.println("EmptyObject 大小: " + 
            VM.current().sizeOf(empty) + " 字节");
        System.out.println("PrimitiveFields 大小: " + 
            VM.current().sizeOf(primitives) + " 字节");
        System.out.println("int[5] 大小: " + 
            VM.current().sizeOf(intArray) + " 字节");
    }
    
    // 空对象
    static class EmptyObject {
        // 只有对象头
    }
    
    // 基本类型字段
    static class PrimitiveFields {
        byte b;      // 1字节
        short s;     // 2字节
        int i;       // 4字节
        long l;      // 8字节
        float f;     // 4字节
        double d;    // 8字节
        char c;      // 2字节
        boolean z;   // 1字节
    }
    
    // 引用类型字段
    static class ReferenceFields {
        Object obj;      // 引用,4字节(开启压缩)
        String str;      // 引用,4字节
        int[] array;     // 引用,4字节
    }
    
    // 父类
    static class ParentClass {
        int parentField;
    }
    
    // 子类
    static class ChildClass extends ParentClass {
        int childField;
    }
}

/*
运行结果示例(64位JVM,开启指针压缩):

=== JVM信息 ===
# Running 64-bit HotSpot VM.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

=== 空对象布局 ===
java.lang.Object object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   4        (object header)           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
  4   4        (object header)           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
  8   4        (object header)           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
 12   4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

=== 基本类型字段 ===
PrimitiveFields object internals:
OFF  SZ     TYPE DESCRIPTION               VALUE
  0   8          (object header)           01 00 00 00 00 00 00 00 (1)
  8   4          (object header)           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
 12   1   byte   PrimitiveFields.b         0
 13   1   boolean PrimitiveFields.z        false
 14   2   char   PrimitiveFields.c         
 16   2   short  PrimitiveFields.s         0
 20   4   float  PrimitiveFields.f         0.0
 24   8   double PrimitiveFields.d         0.0
 32   8   long   PrimitiveFields.l         0
 40   4   int    PrimitiveFields.i         0
 44   4        (loss due to the next object alignment)
Instance size: 48 bytes
*/

2.2 指针压缩

在64位JVM中,为了减少内存占用,可以使用指针压缩(Compressed Oops)。默认开启。

指针压缩原理
java 复制代码
/**
 * 指针压缩演示
 */
public class CompressedOopsDemo {
    
    public static void main(String[] args) {
        // 检查指针压缩是否开启
        System.out.println("=== 指针压缩状态 ===");
        
        String compressOops = getPropertyValue("UseCompressedOops");
        String compressClassPointers = getPropertyValue("UseCompressedClassPointers");
        
        System.out.println("UseCompressedOops: " + compressOops);
        System.out.println("UseCompressedClassPointers: " + compressClassPointers);
        
        // 计算内存差异
        System.out.println("\n=== 内存占用对比 ===");
        
        // 创建大量对象测试
        int count = 1_000_000;
        
        // 每个对象的内存差异
        long referenceSizeWith = 4;   // 开启压缩,引用4字节
        long referenceSizeWithout = 8; // 关闭压缩,引用8字节
        
        // 每个对象头的差异
        long headerSizeWith = 12;      // 开启压缩,对象头12字节
        long headerSizeWithout = 16;   // 关闭压缩,对象头16字节
        
        System.out.println("创建 " + count + " 个对象:");
        System.out.println();
        
        // 空对象
        long emptyWith = 16;    // 12头 + 4填充
        long emptyWithout = 16; // 16头,无需填充
        System.out.println("空对象:");
        System.out.println("  开启压缩: " + (emptyWith * count / 1024 / 1024) + " MB");
        System.out.println("  关闭压缩: " + (emptyWithout * count / 1024 / 1024) + " MB");
        
        // 包含引用的对象
        ReferenceObject[] refs = new ReferenceObject[count];
        long refObjWith = 16;   // 12头 + 4引用
        long refObjWithout = 24; // 16头 + 8引用
        System.out.println("\n包含1个引用的对象:");
        System.out.println("  开启压缩: " + (refObjWith * count / 1024 / 1024) + " MB");
        System.out.println("  关闭压缩: " + (refObjWithout * count / 1024 / 1024) + " MB");
        System.out.println("  节省: " + ((refObjWithout - refObjWith) * count / 1024 / 1024) + " MB");
        
        // 数组
        Object[] array = new Object[count];
        // 数组头 + 引用数组
        long arrayWith = 16 + 4L * count;   // 16头(含长度) + 4*count引用
        long arrayWithout = 24 + 8L * count; // 24头 + 8*count引用
        System.out.println("\n长度" + count + "的对象数组:");
        System.out.println("  开启压缩: " + (arrayWith / 1024 / 1024) + " MB");
        System.out.println("  关闭压缩: " + (arrayWithout / 1024 / 1024) + " MB");
        System.out.println("  节省: " + ((arrayWithout - arrayWith) / 1024 / 1024) + " MB");
        
        // 指针压缩的地址范围
        System.out.println("\n=== 指针压缩限制 ===");
        System.out.println("压缩指针最大寻址: 4GB (2^32)");
        System.out.println("配合对象对齐8字节: 最大32GB堆内存");
        System.out.println("超过32GB需要关闭压缩或使用其他对齐");
        System.out.println();
        System.out.println("相关参数:");
        System.out.println("  -XX:+UseCompressedOops     开启普通对象指针压缩");
        System.out.println("  -XX:+UseCompressedClassPointers  开启类指针压缩");
        System.out.println("  -XX:ObjectAlignmentInBytes=8  对象对齐字节数");
    }
    
    private static String getPropertyValue(String property) {
        try {
            return sun.misc.VM.getSavedProperty(property);
        } catch (Exception e) {
            return "unknown";
        }
    }
    
    static class ReferenceObject {
        Object ref;
    }
}

/*
指针压缩工作原理:

地址编码:
  压缩前:64位真实地址
  压缩后:32位偏移量 × 8(对象对齐)

示例:
  真实地址: 0x0000000123456780
  压缩后:   0x02468ACF (偏移量/8)

限制:
  32位 × 8字节对齐 = 32GB最大堆内存
  超过此限制需要关闭压缩
*/

2.3 对象创建过程

java 复制代码
/**
 * 对象创建过程详解
 */
public class ObjectCreationDemo {
    
    public static void main(String[] args) {
        /*
        对象创建步骤:
        
        1. 类加载检查
           - 检查类是否已加载
           - 未加载则先加载类
        
        2. 分配内存
           - 指针碰撞(Bump the Pointer):堆规整时使用
           - 空闲列表(Free List):堆不规整时使用
           - 并发安全:CAS或TLAB
        
        3. 初始化零值
           - 将分配的内存初始化为零值
           - 保证对象字段有默认值
        
        4. 设置对象头
           - 设置类元数据指针
           - 设置对象哈希码、GC年龄等
        
        5. 执行<init>方法
           - 调用构造方法
           - 初始化成员变量
           - 执行构造代码块
        */
        
        // 示例:跟踪对象创建
        System.out.println("=== 对象创建过程 ===\n");
        
        // 步骤1:类加载检查
        System.out.println("1. 类加载检查");
        System.out.println("   检查Person类是否已加载");
        
        // 步骤2:分配内存
        System.out.println("\n2. 分配内存");
        System.out.println("   在堆中分配对象所需空间");
        
        // 步骤3:初始化零值
        System.out.println("\n3. 初始化零值");
        Person p = new Person();  // 此时所有字段为零值
        System.out.println("   name = null, age = 0");
        
        // 步骤4:设置对象头
        System.out.println("\n4. 设置对象头");
        System.out.println("   设置类指针、哈希码种子等");
        
        // 步骤5:执行构造方法
        System.out.println("\n5. 执行<init>方法");
        p = new Person("张三", 25);
        System.out.println("   调用构造方法初始化");
        System.out.println("   结果: " + p);
        
        // 对象创建的内存分配方式
        System.out.println("\n=== 内存分配方式 ===\n");
        memoryAllocationDemo();
        
        // TLAB演示
        System.out.println("\n=== TLAB (Thread Local Allocation Buffer) ===\n");
        tlabDemo();
    }
    
    /**
     * 内存分配方式演示
     */
    private static void memoryAllocationDemo() {
        /*
        指针碰撞(Bump the Pointer):
        ┌─────────────────────────────────────────┐
        │ 已分配区域 │        空闲区域          │
        │           │                           │
        │  obj1 obj2│←─ ptr ─→                 │
        └─────────────────────────────────────────┘
        
        - 适用:堆内存规整(使用标记-整理、复制算法)
        - 方式:指针向后移动对象大小距离
        - 优点:效率高
        - 使用:Serial、ParNew等收集器
        
        空闲列表(Free List):
        ┌─────────────────────────────────────────┐
        │ obj1 │空闲│ obj2 │  空闲  │ obj3 │空闲 │
        └─────────────────────────────────────────┘
                ↑           ↑              ↑
              记录在空闲列表中
        
        - 适用:堆内存不规整(使用标记-清除算法)
        - 方式:从空闲列表找合适大小的空闲块
        - 缺点:效率较低,可能产生碎片
        - 使用:CMS收集器
        */
        
        System.out.println("指针碰撞: 适用于规整堆(Serial, ParNew)");
        System.out.println("空闲列表: 适用于不规整堆(CMS)");
    }
    
    /**
     * TLAB演示
     */
    private static void tlabDemo() {
        /*
        TLAB (Thread Local Allocation Buffer):
        每个线程在堆中预先分配一小块私有区域
        
        ┌─────────────────────────────────────────────────────────┐
│                       Java Heap                          │
        │                                                          │
        │  ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐       │
│  │Thread 1 │ │Thread 2 │ │Thread 3 │ │Thread 4 │       │
        │  │  TLAB   │ │  TLAB   │ │  TLAB   │ │  TLAB   │       │
        │  │         │ │         │ │         │ │         │       │
        │  └─────────┘ └─────────┘ └─────────┘ └─────────┘       │
        │                                                          │
        │                   共享区域(需要同步)                    │
        │                                                          │
        └─────────────────────────────────────────────────────────┘
        
        优点:
        - 避免多线程竞争
        - 在TLAB中分配无需同步
        - 使用指针碰撞方式,高效
        
        参数:
        -XX:+UseTLAB           开启TLAB(默认开启)
        -XX:TLABSize=256k      设置TLAB大小
        -XX:TLABRefillWasteFraction=64  TLAB refill阈值
        */
        
        System.out.println("TLAB: 每个线程的私有分配缓冲区");
        System.out.println("优点: 避免并发竞争,提高分配效率");
        System.out.println("参数: -XX:+UseTLAB (默认开启)");
    }
    
    static class Person {
        private String name;
        private int age;
        
        // 构造代码块
        {
            System.out.println("   构造代码块执行");
        }
        
        public Person() {
            System.out.println("   无参构造执行");
        }
        
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
            System.out.println("   有参构造执行: name=" + name + ", age=" + age);
        }
        
        @Override
        public String toString() {
            return "Person{name='" + name + "', age=" + age + "}";
        }
    }
}

2.4 对象的访问定位

java 复制代码
/**
 * 对象访问定位方式
 */
public class ObjectAccessDemo {
    
    public static void main(String[] args) {
        /*
        创建对象后,Java程序需要通过栈上的reference数据来操作堆上的具体对象。
        
        访问方式取决于JVM实现,主要有两种:
        
        1. 句柄访问
        2. 直接指针访问
        */
        
        // === 句柄访问 ===
        System.out.println("=== 句柄访问 ===\n");
        handleAccessDemo();
        
        // === 直接指针访问 ===
        System.out.println("\n=== 直接指针访问 ===\n");
        directPointerDemo();
        
        // === HotSpot的实现 ===
        System.out.println("\n=== HotSpot实现 ===\n");
        System.out.println("HotSpot使用直接指针访问");
        System.out.println("优点: 访问速度快,减少一次间接访问");
        System.out.println("缺点: 对象移动时需要修改reference");
    }
    
    /**
     * 句柄访问演示
     */
    private static void handleAccessDemo() {
        /*
        句柄访问:
        
        ┌──────────────┐
        │ Java栈       │
        │ reference ───┼────┐
        └──────────────┘    │
                            ↓
        ┌─────────────────────────────────────────┐
        │              句柄池                      │
        │  ┌─────────────────────────────────┐    │
        │  │        句柄                      │    │
        │  │  ┌───────────┬───────────┐      │    │
        │  │  │对象实例数据│对象类型数据│      │    │
        │  │  │  指针      │   指针    │      │    │
        │  │  └─────┬─────┴─────┬─────┘      │    │
        │  └────────│───────────│────────────┘    │
        └───────────│───────────│─────────────────┘
                    ↓           ↓
        ┌───────────────┐ ┌─────────────────┐
        │   堆(实例)   │ │  方法区(类型)  │
        │   Object      │ │   Class数据     │
        └───────────────┘ └─────────────────┘
        
        优点:
        - reference存储稳定句柄地址
        - 对象移动时只需修改句柄,不动reference
        
        缺点:
        - 需要两次指针访问
        - 需要维护句柄池空间
        */
        
        System.out.println("句柄访问流程:");
        System.out.println("  reference -> 句柄 -> 对象实例");
        System.out.println("  reference -> 句柄 -> 对象类型");
        System.out.println();
        System.out.println("优点: 对象移动只需改句柄");
        System.out.println("缺点: 两次指针访问,效率稍低");
    }
    
    /**
     * 直接指针访问演示
     */
    private static void directPointerDemo() {
        /*
        直接指针访问(HotSpot使用):
        
        ┌──────────────┐
        │ Java栈       │
        │ reference ───┼──────────────────────┐
        └──────────────┘                      │
                                              ↓
        ┌─────────────────────────────────────────────┐
        │               堆(对象实例)                 │
        │  ┌─────────────────────────────────────┐    │
        │  │           对象头                     │    │
        │  │  其中包含指向方法区的类型指针         │────┼──→ 方法区(类型数据)
        │  ├─────────────────────────────────────┤    │
        │  │           实例数据                   │    │
        │  │          ...                        │    │
        │  └─────────────────────────────────────┘    │
        └─────────────────────────────────────────────┘
        
        优点:
        - 只需一次指针访问
        - 访问速度快
        
        缺点:
        - 对象移动需要修改reference
        - GC时需要更新所有引用
        */
        
        System.out.println("直接指针访问流程:");
        System.out.println("  reference -> 对象实例 -> 类型数据(通过对象头)");
        System.out.println();
        System.out.println("优点: 一次访问,效率高");
        System.out.println("缺点: 对象移动需要更新reference");
    }
    
    /**
     * 实际代码访问对象
     */
    public void accessObject() {
        // 创建对象
        User user = new User("张三", 25);  // user是栈上的reference
        
        // 访问对象字段
        String name = user.getName();  // 通过reference访问堆中的对象
        int age = user.getAge();
        
        // 调用对象方法
        user.sayHello();
        
        // 对象作为参数传递
        processUser(user);  // 传递的是reference的副本
    }
    
    private void processUser(User user) {
        // 仍然指向堆中同一个对象
        user.setAge(26);
    }
    
    static class User {
        private String name;
        private int age;
        
        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
        
        public String getName() { return name; }
        public int getAge() { return age; }
        public void setAge(int age) { this.age = age; }
        
        public void sayHello() {
            System.out.println("Hello, I'm " + name);
        }
    }
}

2.5 对象的分配策略

java 复制代码
/**
 * 对象分配策略详解
 */
public class ObjectAllocationDemo {
    
    public static void main(String[] args) {
        System.out.println("=== 对象分配策略 ===\n");
        
        // === 1. 对象优先在Eden分配 ===
        edenAllocation();
        
        // === 2. 大对象直接进入老年代 ===
        largeObjectAllocation();
        
        // === 3. 长期存活对象进入老年代 ===
        tenuringDemo();
        
        // === 4. 动态对象年龄判定 ===
        dynamicAgeDemo();
        
        // === 5. 空间分配担保 ===
        spaceGuaranteeDemo();
    }
    
    /**
     * Eden区分配
     */
    private static void edenAllocation() {
        System.out.println("【1. Eden区分配】");
        System.out.println("大多数对象首先在Eden区分配");
        System.out.println();
        
        /*
        新生代结构:
        ┌─────────────────────────────────────────────┐
        │                 新生代                       │
        │  ┌────────────┬─────────┬─────────┐        │
        │  │    Eden    │    S0   │    S1   │        │
        │  │   (80%)    │  (10%)  │  (10%)  │        │
        │  └────────────┴─────────┴─────────┘        │
        └─────────────────────────────────────────────┘
        
        Eden满了触发Minor GC
        */
        
        // 模拟Eden分配
        byte[] allocation1 = new byte[1024 * 1024];  // 1MB
        byte[] allocation2 = new byte[1024 * 1024];  // 1MB
        
        System.out.println("allocation1, allocation2 在Eden分配");
        System.out.println();
    }
    
    /**
     * 大对象直接进老年代
     */
    private static void largeObjectAllocation() {
        System.out.println("【2. 大对象直接进老年代】");
        System.out.println("超过阈值的大对象直接在老年代分配");
        System.out.println();
        
        /*
        参数:
        -XX:PretenureSizeThreshold=3145728  (3MB)
        
        注意:
        - 只对Serial和ParNew收集器有效
        - G1有Humongous区域处理大对象
        */
        
        // 假设阈值是3MB
        byte[] largeObject = new byte[4 * 1024 * 1024];  // 4MB
        // 大于阈值,直接在老年代分配
        
        System.out.println("大对象: 4MB > 阈值(3MB) -> 老年代分配");
        System.out.println("参数: -XX:PretenureSizeThreshold");
        System.out.println("注意: 避免大对象,增加Eden区GC压力");
        System.out.println();
    }
    
    /**
     * 长期存活对象进入老年代
     */
    private static void tenuringDemo() {
        System.out.println("【3. 长期存活对象进入老年代】");
        System.out.println("对象经过多次GC仍存活,年龄达到阈值后晋升");
        System.out.println();
        
        /*
        年龄计数:
        - 对象在Eden出生,经历一次Minor GC存活,年龄+1
        - 年龄达到阈值,晋升老年代
        
        参数:
        -XX:MaxTenuringThreshold=15  (默认15)
        -XX:InitialTenuringThreshold=7  (初始阈值,并行GC)
        
        对象头中年龄占4位,最大值15
        */
        
        // 模拟年龄增长
        Object longLived = new Object();
        
        System.out.println("对象经历GC次数 -> 年龄");
        System.out.println("年龄达到 MaxTenuringThreshold(默认15) -> 晋升老年代");
        System.out.println();
        System.out.println("年龄记录在对象头Mark Word中:");
        System.out.println("┌─────────────────────────────────┐");
        System.out.println("│ ... │ age:4位 │ ... │");
        System.out.println("└─────────────────────────────────┘");
        System.out.println("  4位最大值15,所以年龄阈值最大15");
        System.out.println();
    }
    
    /**
     * 动态对象年龄判定
     */
    private static void dynamicAgeDemo() {
        System.out.println("【4. 动态对象年龄判定】");
        System.out.println("Survivor区相同年龄对象大小超过一半,直接晋升");
        System.out.println();
        
        /*
        动态晋升规则:
        如果Survivor空间中相同年龄所有对象大小的总和大于
        Survivor空间的一半,年龄大于或等于该年龄的对象
        直接进入老年代。
        
        示例:
        Survivor大小: 10MB
        
        年龄1: 2MB
        年龄2: 3MB
        年龄3: 4MB  (累计 2+3+4=9MB > 10MB/2)
        
        -> 年龄3及以上的对象晋升老年代
        */
        
        System.out.println("示例:");
        System.out.println("Survivor大小: 10MB");
        System.out.println("年龄1对象: 2MB");
        System.out.println("年龄2对象: 3MB");
        System.out.println("年龄3对象: 4MB (累计9MB > 5MB)");
        System.out.println();
        System.out.println("结果: 年龄≥3的对象晋升老年代");
        System.out.println();
    }
    
    /**
     * 空间分配担保
     */
    private static void spaceGuaranteeDemo() {
        System.out.println("【5. 空间分配担保】");
        System.out.println("Minor GC前检查老年代是否有足够空间");
        System.out.println();
        
        /*
        分配担保流程:
        
        1. Minor GC前检查
           老年代最大可用的连续空间 > 新生代所有对象总大小?
           
           YES -> Minor GC安全
           NO  -> 检查 HandlePromotionFailure 设置
           
        2. 允许担保失败 (HandlePromotionFailure=true)
           老年代最大可用连续空间 > 历次晋升到老年代对象的平均大小?
           
           YES -> 尝试Minor GC(有风险)
           NO  -> Full GC
           
        3. 不允许担保失败
           -> Full GC
        
        参数:
        -XX:-HandlePromotionFailure  (JDK 6之后默认允许)
        */
        
        System.out.println("检查流程:");
        System.out.println("  1. 老年代可用空间 > 新生代总大小?");
        System.out.println("     YES -> Minor GC");
        System.out.println("     NO  -> 继续");
        System.out.println();
        System.out.println("  2. 老年代可用空间 > 历史晋升平均值?");
        System.out.println("     YES -> 尝试Minor GC");
        System.out.println("     NO  -> Full GC");
        System.out.println();
    }
}

三、可运行Java代码示例

完整示例:对象大小计算工具

java 复制代码
import java.lang.reflect.*;
import java.util.*;

/**
 * 对象大小计算工具
 * 不使用JOL的情况下估算对象大小
 */
public class ObjectSizeCalculator {
    
    // 对象头大小(64位JVM,开启指针压缩)
    private static final int OBJECT_HEADER_SIZE = 12;
    
    // 数组头额外大小
    private static final int ARRAY_HEADER_EXTRA = 4;
    
    // 引用大小(开启压缩)
    private static final int REFERENCE_SIZE = 4;
    
    // 对齐
    private static final int ALIGNMENT = 8;
    
    // 基本类型大小
    private static final Map<Class<?>, Integer> PRIMITIVE_SIZES = new HashMap<>();
    
    static {
        PRIMITIVE_SIZES.put(byte.class, 1);
        PRIMITIVE_SIZES.put(boolean.class, 1);
        PRIMITIVE_SIZES.put(char.class, 2);
        PRIMITIVE_SIZES.put(short.class, 2);
        PRIMITIVE_SIZES.put(int.class, 4);
        PRIMITIVE_SIZES.put(float.class, 4);
        PRIMITIVE_SIZES.put(long.class, 8);
        PRIMITIVE_SIZES.put(double.class, 8);
    }
    
    /**
     * 计算对象大小
     */
    public static long sizeOf(Object obj) {
        if (obj == null) {
            return 0;
        }
        
        Class<?> clazz = obj.getClass();
        
        // 数组
        if (clazz.isArray()) {
            return sizeOfArray(obj);
        }
        
        // 普通对象
        return sizeOfObject(clazz);
    }
    
    /**
     * 计算普通对象大小
     */
    private static long sizeOfObject(Class<?> clazz) {
        long size = OBJECT_HEADER_SIZE;
        
        // 收集所有字段(包括父类)
        List<Field> fields = getAllFields(clazz);
        
        for (Field field : fields) {
            Class<?> fieldType = field.getType();
            
            if (fieldType.isPrimitive()) {
                size += PRIMITIVE_SIZES.get(fieldType);
            } else {
                size += REFERENCE_SIZE;
            }
        }
        
        // 对齐
        return align(size);
    }
    
    /**
     * 计算数组大小
     */
    private static long sizeOfArray(Object array) {
        Class<?> componentType = array.getClass().getComponentType();
        int length = java.lang.reflect.Array.getLength(array);
        
        long size = OBJECT_HEADER_SIZE + ARRAY_HEADER_EXTRA;
        
        if (componentType.isPrimitive()) {
            size += (long) PRIMITIVE_SIZES.get(componentType) * length;
        } else {
            size += (long) REFERENCE_SIZE * length;
        }
        
        return align(size);
    }
    
    /**
     * 获取所有字段
     */
    private static List<Field> getAllFields(Class<?> clazz) {
        List<Field> fields = new ArrayList<>();
        
        while (clazz != null && clazz != Object.class) {
            for (Field field : clazz.getDeclaredFields()) {
                if (!Modifier.isStatic(field.getModifiers())) {
                    fields.add(field);
                }
            }
            clazz = clazz.getSuperclass();
        }
        
        return fields;
    }
    
    /**
     * 对齐到8字节边界
     */
    private static long align(long size) {
        return (size + ALIGNMENT - 1) / ALIGNMENT * ALIGNMENT;
    }
    
    /**
     * 深度计算对象大小(包括引用的对象)
     */
    public static long deepSizeOf(Object obj) {
        return deepSizeOf(obj, new IdentityHashMap<>());
    }
    
    private static long deepSizeOf(Object obj, Map<Object, Object> visited) {
        if (obj == null || visited.containsKey(obj)) {
            return 0;
        }
        
        visited.put(obj, null);
        
        Class<?> clazz = obj.getClass();
        long size = sizeOf(obj);
        
        // 数组
        if (clazz.isArray()) {
            if (!clazz.getComponentType().isPrimitive()) {
                int length = java.lang.reflect.Array.getLength(obj);
                for (int i = 0; i < length; i++) {
                    Object element = java.lang.reflect.Array.get(obj, i);
                    size += deepSizeOf(element, visited);
                }
            }
            return size;
        }
        
        // 对象字段
        for (Field field : getAllFields(clazz)) {
            if (!field.getType().isPrimitive()) {
                try {
                    field.setAccessible(true);
                    Object fieldValue = field.get(obj);
                    size += deepSizeOf(fieldValue, visited);
                } catch (IllegalAccessException e) {
                    // 忽略
                }
            }
        }
        
        return size;
    }
    
    // === 测试 ===
    public static void main(String[] args) {
        System.out.println("=== 对象大小计算工具 ===\n");
        
        // 1. 空对象
        Object empty = new Object();
        System.out.println("空对象: " + sizeOf(empty) + " 字节");
        
        // 2. 包含基本类型的对象
        PrimitiveObject primitive = new PrimitiveObject();
        System.out.println("基本类型对象: " + sizeOf(primitive) + " 字节");
        
        // 3. 包含引用的对象
        ReferenceObject reference = new ReferenceObject();
        System.out.println("引用对象(浅): " + sizeOf(reference) + " 字节");
        System.out.println("引用对象(深): " + deepSizeOf(reference) + " 字节");
        
        // 4. 数组
        int[] intArray = new int[100];
        System.out.println("int[100]: " + sizeOf(intArray) + " 字节");
        
        Object[] objArray = new Object[100];
        System.out.println("Object[100](浅): " + sizeOf(objArray) + " 字节");
        
        // 5. 复杂对象
        Person person = new Person("张三", 25, new Address("北京", "朝阳"));
        System.out.println("复杂对象(浅): " + sizeOf(person) + " 字节");
        System.out.println("复杂对象(深): " + deepSizeOf(person) + " 字节");
        
        // 6. 集合
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            list.add("String" + i);
        }
        System.out.println("ArrayList(100个字符串)(深): " + deepSizeOf(list) + " 字节");
    }
    
    // 测试类
    static class PrimitiveObject {
        byte b;
        short s;
        int i;
        long l;
        float f;
        double d;
        char c;
        boolean z;
    }
    
    static class ReferenceObject {
        String str = "Hello, World!";
        Object obj = new Object();
    }
    
    static class Person {
        String name;
        int age;
        Address address;
        
        Person(String name, int age, Address address) {
            this.name = name;
            this.age = age;
            this.address = address;
        }
    }
    
    static class Address {
        String city;
        String district;
        
        Address(String city, String district) {
            this.city = city;
            this.district = district;
        }
    }
}

完整示例:逃逸分析与标量替换

java 复制代码
/**
 * 逃逸分析与标量替换示例
 */
public class EscapeAnalysisDemo {
    
    /**
     * 逃逸分析:判断对象是否可能逃逸方法
     */
    public static void main(String[] args) {
        System.out.println("=== 逃逸分析示例 ===\n");
        
        // 开启逃逸分析(JDK 7+默认开启)
        // -XX:+DoEscapeAnalysis
        
        // 1. 不逃逸
        noEscape();
        
        // 2. 方法逃逸
        methodEscape();
        
        // 3. 线程逃逸
        threadEscape();
        
        // 4. 标量替换
        scalarReplacement();
        
        // 5. 锁消除
        lockElimination();
    }
    
    /**
     * 不逃逸:对象仅在方法内使用
     * 可以进行标量替换,在栈上分配
     */
    private static void noEscape() {
        // 对象不逃逸,可以被优化
        Point p = new Point(1, 2);
        int result = p.getX() + p.getY();
        System.out.println("不逃逸: " + result);
        
        /*
        JIT编译后可能优化为:
        int x = 1;
        int y = 2;
        int result = x + y;
        // 对象根本不会在堆上创建
        */
    }
    
    /**
     * 方法逃逸:对象被返回或传递给其他方法
     */
    private static Point methodEscape() {
        Point p = new Point(3, 4);
        return p;  // 逃逸:返回给调用者
    }
    
    /**
     * 线程逃逸:对象被其他线程访问
     */
    private static void threadEscape() {
        Point p = new Point(5, 6);
        
        // 逃逸:被其他线程访问
        new Thread(() -> {
            System.out.println(p.getX());
        }).start();
    }
    
    /**
     * 标量替换:将对象拆解为标量(基本类型)
     */
    private static void scalarReplacement() {
        System.out.println("\n=== 标量替换 ===");
        
        long start = System.currentTimeMillis();
        
        for (int i = 0; i < 10000000; i++) {
            // 这个对象不会逃逸
            Point p = new Point(i, i + 1);
            int x = p.getX();
            int y = p.getY();
        }
        
        long end = System.currentTimeMillis();
        System.out.println("耗时: " + (end - start) + "ms");
        System.out.println("开启逃逸分析和标量替换后,对象不会在堆上创建");
        System.out.println();
        System.out.println("相关参数:");
        System.out.println("  -XX:+DoEscapeAnalysis     开启逃逸分析(默认)");
        System.out.println("  -XX:+EliminateAllocations  开启标量替换(默认)");
        System.out.println("  -XX:+EliminateLocks        开启锁消除(默认)");
        
        /*
        验证:
        java -XX:-DoEscapeAnalysis EscapeAnalysisDemo  // 关闭,较慢
        java -XX:+DoEscapeAnalysis EscapeAnalysisDemo  // 开启,较快
        */
    }
    
    /**
     * 锁消除:对象不逃逸时,锁操作可以消除
     */
    private static void lockElimination() {
        System.out.println("\n=== 锁消除 ===");
        
        long start = System.currentTimeMillis();
        
        for (int i = 0; i < 10000000; i++) {
            StringBuffer sb = new StringBuffer();
            sb.append("a").append(i);  // StringBuffer的方法都有synchronized
            // 但sb不逃逸,锁可以消除
        }
        
        long end = System.currentTimeMillis();
        System.out.println("StringBuffer耗时: " + (end - start) + "ms");
        System.out.println("StringBuffer的synchronized锁被消除");
        System.out.println("参数: -XX:+EliminateLocks (默认开启)");
    }
    
    // 测试类
    static class Point {
        private int x;
        private int y;
        
        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
        
        public int getX() { return x; }
        public int getY() { return y; }
    }
}

/*
逃逸分析优化效果:

1. 栈上分配
   - 对象不逃逸时,可以在栈上分配
   - 减少GC压力
   
2. 标量替换
   - 对象拆解为标量(基本类型和引用)
   - 完全消除对象创建
   
3. 锁消除
   - 对象不逃逸时,synchronized锁可以消除
   - 提高并发性能

相关JVM参数:
  -XX:+DoEscapeAnalysis        开启逃逸分析(JDK7+默认)
  -XX:+EliminateAllocations    开启标量替换(默认)
  -XX:+EliminateLocks          开启锁消除(默认)
*/

四、实战应用场景

场景1:内存优化

java 复制代码
import java.util.*;

/**
 * 基于对象模型的内存优化
 */
public class MemoryOptimizationDemo {
    
    public static void main(String[] args) {
        System.out.println("=== 内存优化技巧 ===\n");
        
        // === 1. 避免不必要的对象头开销 ===
        System.out.println("【1. 减少小对象数量】");
        
        // 不好的设计:每个属性都是对象
        class BadDesign {
            List<Integer> values = new ArrayList<>();
        }
        
        // 好的设计:使用数组
        class GoodDesign {
            int[] values;
        }
        
        System.out.println("List<Integer> 每个Integer都有对象头(16字节)");
        System.out.println("int[] 只有数组头+数据,节省大量内存");
        System.out.println();
        
        // === 2. 使用基本类型替代包装类 ===
        System.out.println("【2. 基本类型vs包装类】");
        
        // 包装类
        Integer[] integers = new Integer[1000];
        // 每个: 16字节头 + 4字节int = 20字节 -> 对齐24字节
        // 总计: 24 * 1000 = 24000字节 (还不算引用数组)
        
        // 基本类型
        int[] ints = new int[1000];
        // 只有: 16字节头 + 4 * 1000 = 4016字节
        // 节省约85%
        
        System.out.println("Integer[1000]: 约24KB");
        System.out.println("int[1000]: 约4KB");
        System.out.println("节省约85%内存");
        System.out.println();
        
        // === 3. 字段顺序优化 ===
        System.out.println("【3. 字段顺序优化】");
        
        // 不好的顺序
        class BadOrder {
            byte b1;    // 1字节
            long l1;    // 8字节 (需要8字节对齐,前面填充7字节)
            byte b2;    // 1字节
            long l2;    // 8字节 (前面填充7字节)
            byte b3;    // 1字节
            // 总计: 1 + 7(填充) + 8 + 1 + 7(填充) + 8 + 1 = 33字节 -> 40字节
        }
        
        // 好的顺序
        class GoodOrder {
            long l1;    // 8字节
            long l2;    // 8字节
            byte b1;    // 1字节
            byte b2;    // 1字节
            byte b3;    // 1字节
            // 总计: 8 + 8 + 1 + 1 + 1 = 19字节 -> 24字节
        }
        
        System.out.println("BadOrder: 40字节(大量填充)");
        System.out.println("GoodOrder: 24字节(字段按大小排序)");
        System.out.println("建议: 长字段在前,短字段在后");
        System.out.println();
        
        // === 4. 使用更紧凑的数据结构 ===
        System.out.println("【4. 紧凑数据结构】");
        
        // 存储布尔值
        boolean[] boolArray = new boolean[8];  // 8个字节
        byte bitSet = 0;  // 1个字节,用位存储8个布尔值
        
        System.out.println("存储8个布尔值:");
        System.out.println("boolean[8]: 16字节头 + 8字节 = 24字节");
        System.out.println("位运算byte: 1字节");
        System.out.println("节省约96%");
        System.out.println();
        
        // === 5. 对象池 ===
        System.out.println("【5. 对象池复用】");
        
        // 频繁创建销毁的对象使用对象池
        ObjectPool<Point> pointPool = new ObjectPool<>(() -> new Point());
        
        Point p = pointPool.borrow();
        p.set(1, 2);
        // 使用后归还
        pointPool.returnObject(p);
        
        System.out.println("对象池避免频繁创建销毁对象");
        System.out.println("适用: 创建开销大、频繁使用的对象");
        System.out.println();
        
        // === 6. 使用原始类型集合 ===
        System.out.println("【6. 原始类型集合】");
        
        // 标准集合
        // List<Integer> - 每个元素是Integer对象
        
        // 第三方原始类型集合(如Eclipse Collections, Trove)
        // IntArrayList - 内部使用int[]
        
        System.out.println("使用Eclipse Collections等库的原始类型集合");
        System.out.println("避免包装类开销");
    }
    
    static class Point {
        int x, y;
        void set(int x, int y) { this.x = x; this.y = y; }
    }
    
    static class ObjectPool<T> {
        private final Queue<T> pool = new LinkedList<>();
        private final java.util.function.Supplier<T> factory;
        
        public ObjectPool(java.util.function.Supplier<T> factory) {
            this.factory = factory;
        }
        
        public T borrow() {
            T obj = pool.poll();
            return obj != null ? obj : factory.get();
        }
        
        public void returnObject(T obj) {
            pool.offer(obj);
        }
    }
}

场景2:对象布局调优

java 复制代码
import org.openjdk.jol.info.ClassLayout;
import java.util.*;

/**
 * 对象布局调优示例
 */
public class ObjectLayoutOptimization {
    
    public static void main(String[] args) {
        System.out.println("=== 对象布局调优 ===\n");
        
        // === 1. 分析当前布局 ===
        System.out.println("【1. 分析对象布局】");
        
        // 使用JOL分析
        // System.out.println(ClassLayout.parseClass(MyClass.class).toPrintable());
        
        // === 2. 字段重排序 ===
        System.out.println("\n【2. 字段重排序】");
        
        // JVM会自动进行字段重排序优化
        // 但最好还是按规范写
        
        System.out.println("优化前(可能有填充):");
        System.out.println("  byte, long, byte, long, byte");
        
        System.out.println("\n优化后(减少填充):");
        System.out.println("  long, long, byte, byte, byte");
        
        // === 3. 使用@Contended避免伪共享 ===
        System.out.println("\n【3. 避免伪共享】");
        
        // 多线程场景下,不同线程访问同一缓存行的不同变量
        // 会导致缓存行失效(伪共享)
        
        // 使用@Contended注解让JVM添加填充
        // @Contended
        // volatile long value;
        
        System.out.println("伪共享: 不同线程访问同一缓存行的不同变量");
        System.out.println("解决: @Contended注解或手动填充");
        System.out.println("参数: -XX:-RestrictContended");
        
        // === 4. 对齐控制 ===
        System.out.println("\n【4. 对齐控制】");
        
        System.out.println("默认对齐: 8字节");
        System.out.println("参数: -XX:ObjectAlignmentInBytes");
        System.out.println();
        System.out.println("增大对齐可以支持更大堆(指针压缩时):");
        System.out.println("  8字节对齐 -> 最大32GB堆");
        System.out.println("  16字节对齐 -> 最大64GB堆");
    }
    
    /**
     * 手动填充避免伪共享
     */
    static class PaddedLong {
        volatile long value;
        
        // 手动填充(确保value独占一个缓存行)
        long p1, p2, p3, p4, p5, p6, p7;
    }
    
    /**
     * 使用@Contended(需要JVM支持)
     */
    // @jdk.internal.vm.annotation.Contended
    static class ContendedLong {
        volatile long value;
    }
}

五、总结与最佳实践

核心要点回顾

概念 内容
对象头 Mark Word + 类型指针 + 数组长度(可选)
实例数据 字段内容,按大小排序减少填充
对齐填充 保证对象大小是8字节的倍数
指针压缩 64位JVM优化,减少内存占用
逃逸分析 判断对象是否逃逸方法
标量替换 对象拆解为标量,消除对象创建

最佳实践

  1. 内存优化

    • 使用基本类型替代包装类
    • 字段按大小排序(长字段在前)
    • 减少小对象数量
    • 使用对象池复用对象
  2. 避免伪共享

    java 复制代码
    // 多线程共享变量添加填充
    @Contended
    volatile long counter;
  3. 利用逃逸分析

    • 让对象不逃逸方法
    • JIT会自动优化栈上分配
  4. 合理设置指针压缩

    bash 复制代码
    # 堆内存 <= 32GB,开启指针压缩
    -XX:+UseCompressedOops
    
    # 堆内存 > 32GB,关闭或增大对齐
    -XX:ObjectAlignmentInBytes=16

对象大小速查表

对象类型 大小(开启压缩) 大小(关闭压缩)
空对象 16字节 16字节
包含1个int 16字节 16字节
包含1个long 16字节 24字节
包含1个引用 16字节 24字节
Object[0] 16字节 24字节
int[0] 16字节 16字节
Integer 16字节 16字节
Long 24字节 24字节

相关JVM参数

bash 复制代码
# 指针压缩
-XX:+UseCompressedOops           # 开启普通指针压缩(默认)
-XX:+UseCompressedClassPointers  # 开启类指针压缩(默认)

# 逃逸分析
-XX:+DoEscapeAnalysis            # 开启逃逸分析(默认)
-XX:+EliminateAllocations        # 开启标量替换(默认)
-XX:+EliminateLocks              # 开启锁消除(默认)

# 对齐
-XX:ObjectAlignmentInBytes=8     # 对象对齐字节数(默认8)

# TLAB
-XX:+UseTLAB                     # 开启TLAB(默认)
-XX:TLABSize=256k                # TLAB大小

# 对象年龄
-XX:MaxTenuringThreshold=15      # 晋升年龄阈值

扩展阅读

  • 《深入理解Java虚拟机》:周志明著,对象内存布局章节
  • JOL工具:OpenJDK项目,分析对象布局
  • 逃逸分析论文:Choi et al., "Escape Analysis for Java"
  • HotSpot源码:oops模块,对象模型实现
相关推荐
东离与糖宝2 小时前
零基础Java学生面试通关手册:项目+算法+框架一次搞定
java·人工智能·面试
gaozhiyong08132 小时前
超越跑分:Gemini 3.1 Pro 2026年多维度能力评估体系深度拆解
java·开发语言
皙然2 小时前
深入解析Java volatile关键字:作用、底层原理与实战避坑
java·开发语言
再玩一会儿看代码2 小时前
Java中 next() 和 nextLine() 有什么区别?一篇文章彻底搞懂
java·开发语言·经验分享·笔记·学习
心勤则明2 小时前
使用SpringAIAlibaba给上下文“瘦身”
java·人工智能·spring
张人玉2 小时前
上位机项目笔记
笔记·c#·上位机
YMWM_2 小时前
python3中的装饰器介绍及其应用场景
java·后端·spring
sheji34162 小时前
【开题答辩全过程】以 基于Java的饮品店管理系统的实现为例,包含答辩的问题和答案
java·开发语言
大阿明2 小时前
Spring.factories
java·数据库·spring