Java对象在堆和栈上的存储(对象布局,待完善)

发布于:2024-04-29 ⋅ 阅读:(33) ⋅ 点赞:(0)

0、前言

这里提到的 Java 对象不仅仅包含引用类型(Object),还包含基本数据类型(boolean、int、long、float、double)。文中部分图片来源于 B站 黑马程序员

1、在栈上的数据存储

1.1、局部变量

局部变量包含以下情况:

  • 方法中定义的变量
  • 方法的形参

注:在非 static 修饰的成员方法中,第一个形参是 this,代表当前类的实例对象

1.2、槽位(slot)

各种类型变量在堆空间和栈空间中的内存分配,常说的 int 占用 4B 是针对堆中变量,而在栈中是按照槽位(slot)进行分配的。

数据类型 字节数(堆空间) 槽位数(栈空间)
boolean 1B 1
char 2B 1
byte 1B 1
short 2B 1
int 4B 1
long 8B 2
float 4B 1
double 8B 2
Object 见对象在堆上的数据存储中的相关讨论 1

总结:

  • 1 slot = 机器字长(32 位机中 32 bit,64 位机中 64 bit)
  • longdouble 占用 2 个 slot
  • 其它类型占用 1 个 slot

1.3、堆数据和栈数据的赋值过程

一般情况而言,同类型变量在堆中的长度更短,在栈中的长度更长。总的转换思路为:

  • 堆 -> 栈:按符号位进行填充,负数在前面填充1,正数在前面填充0,能够保证在补码意义上值保持不变
  • 栈 -> 堆:截断 (boolean 类型比较特殊,只取最后 1bit,而不是 1B)

下面分别是 -5 和 5 的补码表示
在这里插入图片描述

复习点:给定一个负数,写出其补码

  1. 先写出其倒数(正数)的补码(即原码)
  2. 从右到左找到第一个1,左取反,右不变

1.4、测试案例

Java 代码

public class ObjectStackLayout {
    static class MyObject {
        String aString;

        Integer aInteger;
        int anInt;

        boolean aBoolean;

        double aDouble;
    }


    public static void main(String[] args) {
    
        boolean b = true;
        
        char ch = 'a';
        short sh = 10;
        int x = 1;
        
        float f = 1.0f;
        double d = 2.2;

        String s = "hello world";
        MyObject myObject = new MyObject();
    }


    static short num = -5;


    private void calculate(int x) {
        // 堆数据 -> 栈数据
        short y = num;

        // 栈数据 -> 堆数据
        num = y;
    }
}

main 方法的字节码

// boolean b = true
// 从istore_1指令可以看出,将b作为int类型处理(istore含义是int store)
0 iconst_1
1 istore_1
 
// char ch = 'a'
2 bipush 97
4 istore_2
 
// short sh = 10
5 bipush 10
7 istore_3

// int x = 1
8 iconst_1
9 istore 4

// float = 1.0f
// 使用fstore,说明float类型数据和int类型数据在栈上的存储不同
11 fconst_1
12 fstore 5


14 ldc2_w #2 <2.2>
17 dstore 6

19 ldc #4 <hello world>
21 astore 8

23 new #5 <org/example/layout/stack/ObjectStackLayout$MyObject>
26 dup
27 invokespecial #6 <org/example/layout/stack/ObjectStackLayout$MyObject.<init> : ()V>
30 astore 9

32 return

main 方法的局部变量表

槽总数 = 1 + 1 + 1 + 1 + 1 + 1 + 2 + 1 + 1 = 10

在这里插入图片描述
在这里插入图片描述

总结:

  • 形参也是局部变量,测试 calculate 方法可以看到为局部变量 this 分配槽位
  • 浮点数和整数之间使用不同的字节码指令

2、在堆上的数据存储

在这里插入图片描述

2.1、Java 对象的堆内存布局

在这里插入图片描述

标记字段(Mark Word)

标记字段取决于机器字长、是否开启指针压缩这两个因素,下图是 **64 位机开启指针压缩(默认情况)**的情况
在这里插入图片描述
上面共有 5 种状态,原本应该使用 3 bit 来表示锁的状态位,这会导致处于轻量级锁状态和重量级锁状态的对象少了 1 bit 的指针,这样锁数量的上限就变为原来的 1/2。因此,将正常状态和偏向锁的最后 2bit 相同,使用额外的 1bit 来区分正常状态和偏向锁状态。

可能需要注意的点:其中有 1bit 提供给 CMS 垃圾收集器进行使用,后面在 GC 相关文章中再考虑之间的关联

在 64 位机关闭指针压缩的情况下,只是简单地将 cms使用位 弃用。
在这里插入图片描述

在 32 位机的情况下,不存在 cms使用位,同时将高位 32 bit舍弃即可。
在这里插入图片描述

元数据指针(Klass pointer)

在这里插入图片描述

hsdb 工具进行验证:

2.2、布局规则

规则优先级从高到低依次为:

  1. 对象的总长度需要对齐 8B,不够则进行零填充
  2. 父类变量在子类变量之前
  3. 引用数据类型(Object)在基本数据类型(int、double)之后
  4. 若变量类型的长度为 n,则该变量的起始偏移必须是 k × \times × n,单位为 Byte
  5. 变量可以进行重排列,不一定要按照定义的先后顺序排列(满足规则3)
  6. 类型更长的变量排在前面,类型更短的变量排在后面(满足规则3)

2.3.1、内存对齐

在没有开启指针压缩的情况下,同样会进行内存对齐,原因是 64 位机上的 CPU 缓存行大小是 8B,保证对象对齐 8B,可以保证并发情况下,两个对象的修改不会因为缓存行而相互影响。

在这里插入图片描述

2.3.2、父类优先

2.3.3、基本类型优先

2.3.4、字段对齐

2.3.5、字段重排列

2.3、测试案例

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.7</version>
</dependency>
class Parent {
    long l;
    int i;
}

class Child extends Parent {
    String name;
    boolean b;
    int i;
    long l;
}


public class ObjectHeapLayout {
    public static void main(String[] args) {
        // 测试父类中的实例变量一定在子类之前分配,并且引用类型一定在每个类的最后分配
        System.out.println(ClassLayout.parseInstance(new Child()).toPrintable());
        
        // 测试字符串的实际占用空间
        System.out.println(ClassLayout.parseInstance("123").toPrintable());
        
        // 测试数组(nums也是一个引用指针)
        int[] nums = new int[]{1, 2, 3};
        System.out.println(ClassLayout.parseInstance(nums).toPrintable());
        
        System.out.println(ClassLayout.parseInstance(new Object()).toPrintable());
        // System.out.println(ClassLayout.parseInstance(null).toPrintable());//抛出异常
    }
}

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到