JVM对象创建全流程解析

一、JVM对象创建流程

Ⅰ、类加载检查------JVM创建对象时先检查类是否加载

在虚拟机遇到new指令时,比如new关键字、对象克隆、对象序列化时,如下字节码

java 复制代码
0: new           #2                  // class com/example/demo/Calculate

检查指令的参数(#2)是否能在常量池中定位到一个类的符号引用

java 复制代码
常量池:
Constant pool:
   #1 = Methodref          #7.#27         // java/lang/Object."<init>":()V
   #2 = Class              #28            // my/Calculate
   #3 = Methodref          #2.#27         // my/Calculate."<init>":()V
   #4 = Fieldref           #29.#30        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #2.#31         // my/Calculate.compute:()I
   #6 = Methodref          #32.#33        // java/io/PrintStream.println:(I)V
   #7 = Class              #34            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               LocalVariableTable
  #13 = Utf8               this
  #14 = Utf8               Lmy/Calculate;
  #15 = Utf8               compute
  #16 = Utf8               ()I
  #17 = Utf8               a
  #18 = Utf8               I
  #19 = Utf8               b
  #20 = Utf8               main
  #21 = Utf8               ([Ljava/lang/String;)V
  #22 = Utf8               args
  #23 = Utf8               [Ljava/lang/String;
  #24 = Utf8               calculate
  #25 = Utf8               SourceFile
  #26 = Utf8               Calculate.java
  #27 = NameAndType        #8:#9          // "<init>":()V
  #28 = Utf8               my/Calculate
  #29 = Class              #35            // java/lang/System
  #30 = NameAndType        #36:#37        // out:Ljava/io/PrintStream;
  #31 = NameAndType        #15:#16        // compute:()I
  #32 = Class              #38            // java/io/PrintStream
  #33 = NameAndType        #39:#40        // println:(I)V
  #34 = Utf8               java/lang/Object
  #35 = Utf8               java/lang/System
  #36 = Utf8               out
  #37 = Utf8               Ljava/io/PrintStream;
  #38 = Utf8               java/io/PrintStream
  #39 = Utf8               println
  #40 = Utf8               (I)V

检查符号引用代表的类是否已经被加载、校验、准备、解析和初始化,如果没有加载,通过类加载机制加载类。

Ⅱ、分配内存------创建对象的一大工作就是分配内存

由于类一旦被加载,就可知该类对象所占内存空间(因为对象头大小、属性-每个类型占用多少字节是固定的)

为对象分配内存,就是从堆或者栈(一般是堆)中为分配一块确定大小的空间

划分内存的方式------通过指针碰撞或者空闲列表的方式分配内存空间:

  • 指针碰撞:默认使用的方式,通过一个指针标识当前已经使用到位置,指针一侧是已分配的空间、另一次是未使用的空闲内存,通过指针移动对象所需空间大小来分配内存。要求java堆内存绝对规整,已用空间分配在一侧。
  • 空闲列表:通过维护一张空闲列表维护空闲空间的初始位置和块大小,通过在空闲列表寻找可用的内存块(对象所需空间>空闲块时,该空闲块不可用),分配并更新空闲列表。

并发分配问题------在分配内存的时必然存在多个线程为对象在堆中分配空间(堆是线程共享的区域),就是存在并发分配内存的问题,解决方法:

  • CAS锁+失败重试:CAS-Compare And Swap

  • TLAB:本地线程分配缓存-Thread Local Allocate Buffer,先为每个线程在java堆中分配一块空间,当为该线程的对象分配内存时,先从预分配内存中进行分配(打破了线程竞争同一块堆空间的问题)

    -XX:+UseTLAB(默认开启)、-XX:TLABSize设定预分配内存空间大小

Ⅲ、初始化------为分配给对象的内存空间赋0值,不包括对象头

如果是TLAB(本地线程分配缓存)的分配方式,则初始化提前到为每个线程在java堆中分配一块空间时进行。

这一过程使java的实例变量和类变量可以在不赋初始值就可使用,只是访问出的是该类型的0值。

  • 对于基本数据类型(如 intdoublechar 等),如果没有显式初始化,它们的默认值如下:

    • int 类型的变量默认值为 0

    • double 类型的变量默认值为 0.0

    • char 类型的变量默认值为 '\u0000'(即空字符)。

    java 复制代码
    public class Person {
        int age; // 没有初始化,默认为0
        String name; // 没有初始化,默认为null
    }
    Person person = new Person();
    System.out.println(person.age); // 输出 0
    System.out.println(person.name); // 输出 null
  • 对于对象引用类型(如类、接口、数组等),如果没有显式初始化,它们的默认值是 null

  • 局部变量:在Java中,局部变量(在方法内部声明的变量)如果不初始化就直接使用,编译器会报错,因为局部变量在使用前必须显式初始化。

java 复制代码
public void test() {
    int x; // 编译错误:局部变量x可能尚未初始化
    System.out.println(x);
}

Ⅳ、设置对象头

对象

  • 对象头
    • 标记字段(Mark Word):占用内存视操作系统,32位的占4字节(32bit),64位的占8字节(64bit),包括锁标志位、对象的hashcode、分代年龄、偏向线程ID、偏向锁时间戳(Epoch)、锁指针。锁标志位内容不同则保存的对象信息不同。
    • 类型指针(Klass Pointer):占用内存视是否开启指针压缩,开启指针压缩占用4字节,不开启占用8字节,默认开启。是指向元空间中类的元数据信息的指针,JVM通过这个指针判断该对象是哪个类的实例。
    • 数组长度(如果对象是数组类型):如果对象是数据类型,存储数组长度,占用4字节。
  • 实例数据
  • 对齐填充

以下表格是32位的操作系统下默认开启指针压缩的对象头:

标记字段的结构 类型指针
25bit 4bit 1bit 2bit 4字节
23bit 2bit 是否偏向锁 锁标志位
对象的哈希码 分代年龄 0 01(无锁)
线程ID:持有偏向锁的线程ID,标识哪个线程偏向该对象 Epoch:偏向锁的时间戳,用于批量撤销偏向锁 分代年龄 1 01(无锁)
指向栈中锁记录的指针 00(轻量级锁)
指向重量级锁指针(操作系统级互斥锁) 10(重量级锁)
11(GC标记,表示对象待回收,由GC算法确定)

Ⅴ、执行方法

执行方法,按照程序员的意愿进行初始化,为属性赋值(赋程序员给的值)和执行构造方法。

二、查看对象大小和指针压缩

1、查看对象的内存布局

引入依赖

java 复制代码
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.17</version>
        </dependency>

示例代码

java 复制代码
package com.example.demo;

import org.openjdk.jol.info.ClassLayout;

/**
 * 计算对象大小
 */
public class JOLSample {

    public static void main(String[] args) {
        ClassLayout layout2 = ClassLayout.parseInstance(new A());
        System.out.println(layout2.toPrintable());
    }

    // ‐XX:+UseCompressedOops 默认开启的压缩所有指针
    // ‐XX:+UseCompressedClassPointers 默认开启的压缩对象头里的类型指针Klass Pointer
    // Oops : Ordinary Object Pointers
    public static class A {
        //8B mark word
        //4B Klass Pointer 如果关闭压缩‐XX:‐UseCompressedClassPointers或‐XX:‐UseCompressedOops,则占用8B
        int id; //4B
        String name; //4B 如果关闭压缩‐XX:‐UseCompressedOops,则占用8B
        byte b; //1B
        Object o; //4B 如果关闭压缩‐XX:‐UseCompressedOops,则占用8B
    }
}

在64位操作系统上的执行结果(默认开启指针压缩):

关闭指针压缩后,引用类型占用的空间变成8字节:

根据提供的 JOL(Java Object Layout)输出,以下是 com.example.demo.JOLSample$A 对象的内存布局分析:

  1. 对象头(Object Header)

    • Mark Word (标记字):
      • 偏移量:0,大小:8 字节
      • 值:0x0000000000000005
      • 含义:表示对象处于 可偏向状态biasable),分代年龄为 0age: 0),存储锁、GC 状态等信息。
    • Klass Word (类指针):
      • 偏移量:8,大小:4 字节
      • 值:0xf800cf18
      • 含义:指向类元数据的指针(JVM 开启指针压缩后为 4 字节)。
  2. 实例字段(Instance Fields)

    • int id
      • 偏移量:12,大小:4 字节
      • 值:0(默认初始值)。
    • byte b
      • 偏移量:16,大小:1 字节
      • 值:0(默认初始值)。
    • 对齐填充(Padding Gap)
      • 偏移量:17,大小:3 字节
      • 原因:下一个字段 String name 需对齐到 4 字节边界(20 是 4 的倍数),因此在 byte b 后填充 3 字节。
    • String name
      • 偏移量:20,大小:4 字节
      • 值:null(引用类型,指针压缩后占 4 字节)。
    • Object o
      • 偏移量:24,大小:4 字节
      • 值:null(引用类型)。
  3. 对象对齐填充(Object Alignment Gap)

    • 偏移量:28,大小:4 字节
    • 原因:对象总大小需对齐至 8 字节(64 位 JVM 的默认对齐)。当前已用 28 字节(0~27),需填充至 32 字节(28 + 4 = 32)。

关键指标

  • 实例总大小(Instance Size)32 字节。
  • 空间损失(Space Losses)
    • 内部(Internal)3 字节(字段间填充)。
    • 外部(External)4 字节(对象末尾填充)。
    • 总计损失7 字节。

内存布局图示

偏移量 大小(字节) 内容 说明
0 8 Mark Word 锁、GC 状态等
8 4 Klass Word 类元数据指针
12 4 int id 整型字段
16 1 byte b 字节字段
17 3 对齐填充 补齐至 4 字节边界
20 4 String name 字符串引用(null
24 4 Object o 对象引用(null
28 4 对象对齐填充 补齐至 8 字节边界

总结

  • 对象头占 12 字节8 + 4),字段数据占 13 字节4 + 1 + 4 + 4),但实际占用 20 字节(含内部填充)。
  • JVM 通过填充确保字段对齐和对象对齐,提高内存访问效率。
  • 优化建议:若需减少空间,可调整字段顺序(如将 byte b 放在末尾),但 JVM 会自动重排,通常无需手动干预。
2、指针压缩的JVM配置参数

‐XX:+UseCompressedOops :开启的压缩所有指针,默认开启

‐XX:+UseCompressedClassPointers :开启的压缩对象头里的类型指针Klass Pointer,默认开启

3、为什么要有指针压缩

1、在64位的平台中节约空间和带宽:在主内存和缓存之间复制较大指针会占用更多带宽;

2、32位地址最大支持4G内存,通过对对象指针的压缩编码、解码以支持更大的内存配置(不超过32G);

3、堆内存小于4G时不需要开启指针压缩,JVM会自动去除高32位地址,使用低虚拟地址空间;

4、堆内存大于32G时,压缩指针失效,强制使用64位对java对象寻址。(所以堆内存不建议大于32G)

ressedOops :开启的压缩所有指针,默认开启

‐XX:+UseCompressedClassPointers :开启的压缩对象头里的类型指针Klass Pointer,默认开启

3、为什么要有指针压缩

1、在64位的平台中节约空间和带宽:在主内存和缓存之间复制较大指针会占用更多带宽;

2、32位地址最大支持4G内存,通过对对象指针的压缩编码、解码以支持更大的内存配置(不超过32G);

3、堆内存小于4G时不需要开启指针压缩,JVM会自动去除高32位地址,使用低虚拟地址空间;

4、堆内存大于32G时,压缩指针失效,强制使用64位对java对象寻址。(所以堆内存不建议大于32G)

相关推荐
OxYGC6 小时前
[AI工程] Spring AI 第十四篇:Agent 五种模式在 2.0 里怎么写
java·spring·ai·ai编程
code2cat6 小时前
【随笔】从聊天到调用工具:理解MCP在AI应用中的位置
java·人工智能
2501_933923256 小时前
Spring Bean作用域揭秘:单例、原型、请求与会话的区别
java·后端·spring·java-ee
学编程就要猛6 小时前
基于Spring AI 的智能聊天机器人
java·spring ai·chat robot
修炼室6 小时前
Java开发工程师笔试经验贴【高频知识】
java
木井巳7 小时前
【记忆化搜索】最长递增子序列
java·算法·leetcode·深度优先·剪枝·推荐算法
杨杨杨大侠7 小时前
知识库已经有了,Java 程序员还要做什么?Spring AI RAG 实战
java·openai·ai编程
伊信7 小时前
Spring AI 基础学习与应用
java·后端
夜郎king7 小时前
基于 Java + Playwright 实现网站自动访问与数据采集(以腾讯云开发者社区为例)
java·playwright·网页数据获取
Anastasiozzzz7 小时前
重新定义 Agent 基建:Redis 在现代 AI 与智能体系统中的工程实践
java·人工智能·redis·ai