14栈与堆上的数据存储

1.栈上的数据存储

在Java中有8大基本数据类型:

这里的内存占用,指的是堆上或者数组中内存分配的空间大小,栈上的实现更加复杂。

Java中的8大数据类型在虚拟机中的实现:

boolean、byte、char、short在栈上是不是存在空间浪费?

是的,Java虚拟机采用的是空间换时间方案,在栈上不存储具体的类型,只根据slot槽进行数据的处理,浪费了一些内存空间但是避免不同数据类型不同处理方式带来的时间开销。

同时,像long型在64位系统中占用2个slot,使用了16字节空间,但实际上在Hotspot虚拟机中,它的高8个字节没有使用,这样就满足了long型使用8个字节的需要。

2.栈中的数据要保存到堆上或者从堆中加载到栈上时怎么处理?

1、堆中的数据加载到栈上,由于栈上的空间大于或者等于堆上的空间,所以直接处理但是需要注意下符号位。

boolean、char为无符号,低位复制,高位补0

byte、short为有符号,低位复制,高位非负则补0,负则补1

2、栈中的数据要保存到堆上,byte、char、short由于堆上存储空间较小,需要将高位去掉。boolean比较特殊,只取低位的最后一位保存。

3.对象在堆上是如何存储的?

对象在堆中的内存布局,指的是对象在堆中存放时的各个组成部分,主要分为以下几个部分:

3.1标记字段

标记字段相对比较复杂。在不同的对象状态(有无锁、是否处于垃圾回收的标记中)下存放的内容是不同的,同时在64位(又分为是否开启指针压缩)、32位虚拟机中的布局都不同。以64位开启指针压缩为例:

怎么确认标记字段的内容呢?我们可以使用JOL,JOL是用于分析 JVM 中对象布局的一款专业工具。工具中使用 Unsafe、JVMTI 和 Serviceability Agent (SA)等虚拟机技术来打印实际的对象内存布局。

使用方法:

1、添加依赖

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

2、使用如下代码打印对象内存布局:

System.out.println(ClassLayout.parseInstance(对象).toPrintable());

代码:

packageoop1;

importorg.openjdk.jol.info.ClassLayout;

importjava.io.IOException;
//-XX:-UseCompressedOops 关闭压缩指针publicclassStudent {
    privatelong id;
    privateint age;
    privateString name;

    publiclonggetId() {
        return id;
    }

    publicvoidsetId(longid) {
        this.id = id;
    }

    publicintgetAge() {
        return age;
    }

    publicvoidsetAge(intage) {
        this.age = age;
    }

    publicString getName() {
        return name;
    }

    publicvoidsetName(String name) {
        this.name = name;
    }

    publicstaticvoidmain(String[] args) throwsIOException {
        Student student = newStudent();
        System.out.println(Integer.toBinaryString(student.hashCode()));
        System.out.println(ClassLayout.parseInstance(student).toPrintable());
        System.in.read();
    }
}
//0010011 01011111 10111010 10100100//  0x26163608

打印结果如下:

和hashcode值是一致的(注意小端存储,结果会倒着写)

3.2元数据的指针

Klass pointer元数据的指针指向方法区中保存的InstanceKlass对象:

3.3指针压缩

在64位的Java虚拟机中,Klass Pointer以及对象数据中的对象引用都需要占用8个字节,为了减少这部分的内存使用量,64 位 Java 虚拟机使用指针压缩技术,将堆中原本 8个字节的 指针压缩成 4个字节 ,此功能默认开启,可以使用-XX:-UseCompressedOops关闭。

指针压缩的思想是将寻址的单位放大,比如原来按1字节去寻址,现在可以按8字节寻址。如下图所示,原来按1去寻址,能拿到1字节开始的数据,现在按1去寻址,就可以拿到8个字节开始的数据。

这与停车场是类似的。以前需要记录我的车占用了那几米的空间,现在只需要记下停车场的编号:

这样将编号当成地址,就可以用更小的内存访问更多的数据。但是这样的做法有两个问题:

1、需要进行内存对齐,指的是将对象的内存占用填充至8字节的倍数。存在空间浪费(对于Hotspot来说不存在,即便不开启指针压缩,也需要进行内存对齐)

2、寻址大小仅仅能支持2的35 次方个字节(32GB,如果超过32GB指针压缩会自动关闭)。不用压缩指针,应该是2的64次方 = 16EB,用了压缩指针就变成了8(字节) = 2的3次方 * 2的32次方 = 2的35次方

3.4内存对齐

对象中还有一部分内容就是对齐。内存对齐指的是对象中会空出来几个字节,不做任何数据存储。内存对齐主要目的是为了解决并发情况下CPU缓存失效的问题:

在内存中缓存了A和B的数据

A的数据写入时,由于A和B在同一个缓存行中,所以A和B的缓存数据都会被清空:

这样就需要再从内存中读取一次:

我们只修改了A对象的数据,引起了B对象的缓存失效。

内存对齐解决了这个问题:内存对齐之后,同一个缓存行中不会出现不同对象的属性。在并发情况下,如果让A对象一个缓存行失效,是不会影响到B对象的缓存行的。

内存对齐要求每个对象字节数是8的倍数,除了添加字节填充之外,还有字段的要求。

在Hotspot中,要求每个属性的偏移量Offset(字段地址 – 起始地址)必须是字段长度的N倍。

比如下图中,Student类中的id属性类型为long,那么偏移量就必须是8的倍数。所以将id和age的字段顺序进行了调整,这种方式叫字段重排列

这样可以更容易让一个字段在一整个缓存行中,提升缓存行读取的效率。

如果不满足要求,会尝试使用内存对齐,通过在属性之间插入一块对齐区域达到目的。

如下图中,name字段是引用占用8个字节(关闭了指针压缩),所以Offset必须是8的倍数,在age和name之间插入了4个字节的空白区域。

19 条评论

  1. Felicity2826 2025-11-28 19:21 回复
  2. Dustin971 2025-11-30 19:31 回复
  3. Leanne620 2025-12-02 10:22 回复
  4. Bryan2214 2025-12-17 19:53 回复
  5. Gina2415 2025-12-17 23:43 回复
  6. Haven1185 2025-12-20 06:25 回复
  7. Vivian3995 2025-12-24 05:33 回复
  8. Marisa4526 2025-12-27 13:37 回复
  9. Alicia2681 2026-01-05 07:05 回复
  10. June4770 2026-01-08 16:59 回复
  11. Faith3009 2026-01-12 12:16 回复
  12. Walter4640 2026-01-15 15:24 回复

回复 Dustin971 取消回复

您的邮箱地址不会被公开。 必填项已用 * 标注