JVM对象创建与内存分配机制深度剖析:从创建到回收的全流程解析

一、对象的创建流程详解

1.1 对象创建的完整流程

复制代码
类加载检查 → 分配内存 → 初始化 → 设置对象头 → 执行<init>方法

1.2 五步流程深度解析

1. 类加载检查

当JVM遇到new指令时,首先检查:

  • 能否在常量池中定位到类的符号引用

  • 该类是否已被加载、解析和初始化

触发场景new关键字、对象克隆、对象序列化等。

2. 分配内存

类加载检查通过后,JVM为新生对象分配内存。这里有两个核心问题:

内存划分方法:
复制代码
// 根据堆内存是否规整,采用不同策略
if (堆内存规整) {
    // 使用"指针碰撞"(Bump the Pointer)
    // 所有用过的内存放一边,空闲内存放另一边
    // 分配时只需移动分界指针
} else {
    // 使用"空闲列表"(Free List)
    // 维护一个可用内存块列表
    // 分配时从列表中找到足够大的空间
}
并发问题解决方案:
复制代码
// 方案1:CAS + 失败重试
// 保证内存分配操作的原子性

// 方案2:TLAB(线程本地分配缓冲)
// 每个线程在堆中预先分配一小块私有内存
// 参数:-XX:+UseTLAB(默认开启),-XX:TLABSize指定大小
3. 初始化

内存分配完成后,JVM将分配到的内存空间初始化为零值(不包括对象头)。这保证了实例字段在不赋初值的情况下也能直接使用。

java

复制代码
// 示例:所有基本类型字段被设为0或false,引用类型设为null
public class User {
    private int id;        // 初始化为0
    private String name;   // 初始化为null
    private boolean flag;  // 初始化为false
}
4. 设置对象头

初始化零值后,JVM设置对象头信息,包括:

  • 对象哈希码

  • GC分代年龄

  • 锁状态标志

  • 类型指针(指向类元数据)

对象头结构(64位系统)

复制代码
|-------------------------------------------------|
|                 Mark Word (8字节)               |
|-------------------------------------------------|
|   类型指针(4字节,开启压缩) / 类指针(8字节)   |
|-------------------------------------------------|
|   数组长度(4字节,仅数组对象)                 |
|-------------------------------------------------|

锁状态在对象头中的存储

复制代码
| 锁状态   | 存储内容                             | 标志位 |
|----------|--------------------------------------|--------|
| 无锁     | 哈希码、分代年龄、偏向锁标识等         | 01     |
| 偏向锁   | 持有锁的线程ID、偏向时间戳等           | 01     |
| 轻量级锁 | 指向栈中锁记录的指针                   | 00     |
| 重量级锁 | 指向重量级锁(互斥量)的指针           | 10     |
| GC标记   | 空                                   | 11     |
5. 执行<init>方法

最后执行对象的构造方法,按照程序员的意愿进行初始化。这是对象创建的最后一步,也是程序员唯一能控制的部分。


二、对象内存分配策略

2.1 对象分配流程图

2.2 栈上分配与逃逸分析

逃逸分析:分析对象动态作用域,判断对象是否会被外部方法引用。

复制代码
// 示例1:对象未逃逸(可栈上分配)
public void test1() {
    User user = new User();  // 对象未逃逸,可在栈上分配
    user.setId(1);
    user.setName("张三");
    // 方法结束,对象随栈帧销毁
}

// 示例2:对象逃逸(需堆上分配)
public User test2() {
    User user = new User();  // 对象逃逸,返回给外部
    user.setId(1);
    user.setName("李四");
    return user;  // 对象逃逸出方法作用域
}

JVM参数

  • -XX:+DoEscapeAnalysis:开启逃逸分析(JDK7+默认开启)

  • -XX:-DoEscapeAnalysis:关闭逃逸分析

2.3 标量与聚合量替换

标量替换:当对象不会被外部访问且可分解时,JVM不会创建该对象,而是将其成员变量分解为标量在栈上分配。

复制代码
// 优化前:创建User对象
public void test() {
    User user = new User();
    user.id = 1;
    user.name = "test";
    System.out.println(user.id);
}

// 优化后:标量替换
public void test() {
    int id = 1;      // 标量分配在栈上
    String name = "test";
    System.out.println(id);
}

JVM参数-XX:+EliminateAllocations(开启标量替换,JDK7+默认开启)

2.4 实战:栈上分配性能对比

复制代码
public class AllotOnStack {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100_000_000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("耗时:" + (end - start) + "ms");
    }
    
    private static void alloc() {
        User user = new User();  // 未逃逸对象
        user.setId(1);
        user.setName("test");
    }
}

// 测试结果对比
// 参数1:-Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+EliminateAllocations
// 结果:耗时约5ms,几乎无GC

// 参数2:-Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:-EliminateAllocations
// 结果:耗时约2000ms,频繁GC

三、堆内存分配策略

3.1 Eden区分配(默认策略)

大多数对象在新生代Eden区分配。当Eden区空间不足时,触发Minor GC。

新生代内存布局

复制代码
默认比例 8:1:1
┌─────────────┬─────────────┬─────────────┐
│   Eden区    │ Survivor0区 │ Survivor1区 │
│    (8/10)   │   (1/10)    │   (1/10)    │
└─────────────┴─────────────┴─────────────┘

3.2 大对象直接进入老年代

大对象:需要大量连续内存空间的对象(如长字符串、大数组)。

复制代码
// 参数设置:-XX:PretenureSizeThreshold=1000000(单位字节)
// 仅对Serial和ParNew收集器有效
public class BigObjectTest {
    public static void main(String[] args) {
        // 对象大小超过1MB,直接进入老年代
        byte[] bigArray = new byte[2 * 1024 * 1024]; // 2MB数组
    }
}

为什么大对象直接进入老年代?

避免在Eden区和Survivor区之间发生大量内存复制,降低GC效率。

3.3 长期存活对象进入老年代

对象年龄计数器

  1. 对象在Eden出生,经过第一次Minor GC后存活,移动到Survivor区,年龄设为1

  2. 每次Minor GC后年龄+1

  3. 年龄达到阈值(默认15)后,晋升到老年代

JVM参数-XX:MaxTenuringThreshold=15(设置年龄阈值)

3.4 对象动态年龄判断

规则 :Survivor区中,一批对象的总大小超过该区内存的50%(-XX:TargetSurvivorRatio可调整),则年龄大于等于这批对象中最大年龄的对象直接进入老年代。

目的:让可能长期存活的对象尽早进入老年代。

3.5 老年代空间分配担保机制

流程

复制代码
// Minor GC前检查
if (老年代可用空间 < 年轻代所有对象总大小) {
    if (设置了-XX:-HandlePromotionFailure) {
        // 检查老年代可用空间是否大于历次晋升到老年代对象的平均大小
        if (老年代可用空间 < 平均晋升大小) {
            // 触发Full GC
            fullGC();
        }
    } else {
        // 触发Full GC
        fullGC();
    }
}

触发条件

  1. Minor GC后存活对象大小 > 老年代可用空间

  2. 历次晋升对象平均大小 > 老年代可用空间


四、对象内存回收机制

4.1 判断对象是否可回收

方法1:引用计数法(JVM未采用)
复制代码
// 问题:无法解决循环引用
public class ReferenceCountingGC {
    Object instance = null;
    
    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;  // 循环引用
        objB.instance = objA;
        objA = null;
        objB = null;
        // 引用计数都不为0,但实际已不可达
    }
}
方法2:可达性分析算法(JVM采用)

从GC Roots对象出发,向下搜索,形成引用链。不在引用链上的对象被认为是垃圾。

GC Roots包括

  • 虚拟机栈中引用的对象

  • 方法区中静态属性引用的对象

  • 方法区中常量引用的对象

  • 本地方法栈中JNI引用的对象

4.2 引用类型详解

复制代码
// 1. 强引用(Strong Reference)
Object obj = new Object();  // 强引用,不会被GC回收

// 2. 软引用(Soft Reference)
SoftReference<Object> softRef = new SoftReference<>(new Object());
// 内存不足时会被回收,适合做缓存

// 3. 弱引用(Weak Reference)
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 只要GC就会回收

// 4. 虚引用(Phantom Reference)
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
// 几乎不用,用于对象回收跟踪

4.3 finalize()方法的最后机会

对象被标记为可回收后,还有一次"复活"的机会:

复制代码
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize方法执行");
        FinalizeEscapeGC.SAVE_HOOK = this;  // 重新建立引用,避免被回收
    }
    
    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();
        
        // 第一次GC,finalize方法会执行
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            System.out.println("第一次GC后存活");
        }
        
        // 第二次GC,finalize方法不会再次执行
        SAVE_HOOK = null;
        System.gc();
        Thread.sleep(500);
        if (SAVE_HOOK == null) {
            System.out.println("第二次GC后被回收");
        }
    }
}

注意finalize()方法只会被执行一次!

4.4 如何判断一个类是无用的类

方法区(元空间)回收的主要目标是无用的类,需同时满足:

  1. 该类所有实例都已被回收

  2. 加载该类的ClassLoader已被回收

  3. 该类对应的java.lang.Class对象没有被任何地方引用


五、指针压缩技术详解

5.1 什么是指针压缩?

  • JDK1.6 update14开始支持

  • 64位系统中,使用32位指针表示对象地址

  • 参数:-XX:+UseCompressedOops(默认开启)

5.2 为什么要使用指针压缩?

优势

  1. 减少内存占用(64位指针→32位指针)

  2. 减少GC压力

  3. 提高缓存命中率

限制

  • 堆内存≤4GB:直接使用低32位地址

  • 4GB<堆内存≤32GB:压缩指针有效

  • 堆内存>32GB:压缩指针失效,强制使用64位指针

5.3 查看对象布局示例

复制代码
// 添加依赖
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

// 查看对象布局
public class JOLSample {
    public static void main(String[] args) {
        // 查看普通对象布局
        System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
        
        // 查看数组对象布局
        System.out.println(ClassLayout.parseInstance(new int[3]).toPrintable());
        
        // 查看自定义对象布局
        System.out.println(ClassLayout.parseInstance(new User()).toPrintable());
    }
}

六、总结与最佳实践

6.1 对象创建优化建议

  1. 减少大对象创建:避免长字符串、大数组的频繁创建

  2. 利用栈上分配:尽量使用局部变量,让对象不逃逸

  3. 对象复用:使用对象池或缓存,减少GC压力

  4. 及时释放引用 :使用完对象后及时置为null

6.2 JVM参数调优建议

复制代码
# 基础配置
-Xms2g -Xmx2g                  # 堆大小,避免频繁扩容
-Xmn1g                         # 新生代大小,根据业务调整
-XX:SurvivorRatio=8            # Eden与Survivor比例
-XX:MaxTenuringThreshold=15    # 对象晋升老年代年龄

# 指针压缩(默认开启)
-XX:+UseCompressedOops
-XX:+UseCompressedClassPointers

# 优化选项
-XX:+UseTLAB                   # 启用线程本地分配缓冲
-XX:+DoEscapeAnalysis          # 启用逃逸分析
-XX:+EliminateAllocations      # 启用标量替换

# 大对象处理
-XX:PretenureSizeThreshold=1M  # 大对象直接进入老年代

6.3 监控与诊断

  1. JOL工具:分析对象内存布局

  2. VisualVM:监控对象创建和回收情况

  3. GC日志分析:了解对象生命周期

  4. 堆转储分析:定位内存泄漏

相关推荐
sunywz17 小时前
【JVM】(4)JVM对象创建与内存分配机制深度剖析
开发语言·jvm·python
这周也會开心19 小时前
JVM-类加载子系统
jvm
xxxmine19 小时前
JVM 双亲委派模型
jvm
代码or搬砖19 小时前
JVM 类加载机制
jvm
我尽力学20 小时前
JVM类加载子系统、类加载机制
jvm
小罗和阿泽20 小时前
java [多线程基础 二】
java·开发语言·jvm
小罗和阿泽20 小时前
java 【多线程基础 一】线程概念
java·开发语言·jvm
隐退山林21 小时前
JavaEE:多线程初阶(一)
java·开发语言·jvm
xie_pin_an21 小时前
C++ 类和对象全解析:从基础语法到高级特性
java·jvm·c++