欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 财经 > 金融 > JVM对象创建全流程解析

JVM对象创建全流程解析

2025/6/18 13:11:05 来源:https://blog.csdn.net/qq_45043381/article/details/148720244  浏览:    关键词:JVM对象创建全流程解析

一、JVM对象创建流程

在这里插入图片描述

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

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

0: new           #2                  // class com/example/demo/Calculate

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

常量池:
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'(即空字符)。

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

  • 局部变量:在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位的操作系统下默认开启指针压缩的对象头:

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

Ⅴ、执行方法

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

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

1、查看对象的内存布局

引入依赖

        <dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.17</version></dependency>

示例代码

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 Pointerspublic static class A {//8B mark word//4B Klass Pointer 如果关闭压缩‐XX:‐UseCompressedClassPointers或‐XX:‐UseCompressedOops,则占用8Bint id; //4BString name; //4B 如果关闭压缩‐XX:‐UseCompressedOops,则占用8Bbyte b; //1BObject 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 字节。

内存布局图示

偏移量大小(字节)内容说明
08Mark Word锁、GC 状态等
84Klass Word类元数据指针
124int id整型字段
161byte b字节字段
173对齐填充补齐至 4 字节边界
204String name字符串引用(null
244Object o对象引用(null
284对象对齐填充补齐至 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)

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词