Salmon的全栈知识 Salmon的全栈知识
首页
  • JavaSE
  • JavaWeb
  • Spring生态
  • JUC
  • JVM
  • Netty
  • Java各版本特性
  • 23种设计模式
  • Maven
  • Java常用框架
  • Dubbo
  • OpenFeign
  • Nacos
  • Zookeeper
  • Sentinel
  • Seata
  • Gateway
  • Go基础
  • Gin
  • SQL数据库

    • MySQL
    • Oracle
  • NoSQL数据库

    • Redis
    • MongoDB
    • ElasticSearch
  • 消息中间件

    • RabbitMQ
    • RocketMQ
    • Kafka
    • ActiveMQ
    • MQTT
    • NATS
  • 网关中间件

    • Nginx
  • Linux
  • Docker
  • Git
  • K8s
  • Solidity
  • Java
  • 计算机网络
  • 操作系统
GitHub (opens new window)
首页
  • JavaSE
  • JavaWeb
  • Spring生态
  • JUC
  • JVM
  • Netty
  • Java各版本特性
  • 23种设计模式
  • Maven
  • Java常用框架
  • Dubbo
  • OpenFeign
  • Nacos
  • Zookeeper
  • Sentinel
  • Seata
  • Gateway
  • Go基础
  • Gin
  • SQL数据库

    • MySQL
    • Oracle
  • NoSQL数据库

    • Redis
    • MongoDB
    • ElasticSearch
  • 消息中间件

    • RabbitMQ
    • RocketMQ
    • Kafka
    • ActiveMQ
    • MQTT
    • NATS
  • 网关中间件

    • Nginx
  • Linux
  • Docker
  • Git
  • K8s
  • Solidity
  • Java
  • 计算机网络
  • 操作系统
GitHub (opens new window)
npm

(进入注册为作者充电)

  • JVM基础

    • 初识JVM
    • 字节码文件详解
    • 运行时数据区
    • 垃圾回收
  • JVM实战

    • 内存调优
    • GC调优
    • 性能调优
  • JVM高级

    • GraalVM
    • 新一代的GC
    • 揭秘Java工具
  • JVM原理

    • 栈上的数据存储
    • 对象在堆上是如何存储的?
      • 标记字段
      • 元数据的指针
      • 指针压缩
        • 案例:在hsdb工具中验证klass pointer正确性
      • 内存对齐
        • 案例:子类和父类的偏移量
    • 方法调用的原理
    • 异常捕获的原理
    • JIT即时编译器
    • 垃圾回收器原理
  • JVM面试

    • 什么是JVM?
    • 了解过字节码文件的组成吗?
    • 说一下运行时数据区
    • 哪些区域会出现内存溢出,会有什么现象?
    • JVM在JDK6-8之间在内存区域上有什么不同
    • 类的生命周期
    • 什么是类加载器?
    • 什么是双亲委派机制
    • 如何打破双亲委派机制
    • Tomcat的自定义类加载器
    • 如何判断堆上的对象没有被引用??
    • JVM 中都有哪些引用类型?
    • ThreadLocal中为什么要使用弱引用?
    • 有哪些常见的垃圾回收算法?
    • 有哪些常用的垃圾回收器?
    • 如何解决内存泄漏问题?
    • 常见的JVM参数?
  • 《JVM》笔记
  • JVM原理
Salmon
2024-03-11
目录

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

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

img

# 标记字段

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

img

怎么确认标记字段的内容呢?我们可以使用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());

代码:

package oop1;

import org.openjdk.jol.info.ClassLayout;

import java.io.IOException;
//-XX:-UseCompressedOops 关闭压缩指针
public class Student {
    private long id;
    private int age;
    private String name;

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public static void main(String[] args) throws IOException {
        Student student = new Student();
        System.out.println(Integer.toBinaryString(student.hashCode()));
        System.out.println(ClassLayout.parseInstance(student).toPrintable());
        System.in.read();
    }
}
//0010011 01011111 10111010 10100100
//  0x26163608

打印结果如下:

img

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

img

img

# 元数据的指针

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

img

# 指针压缩

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

img

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

img

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

img

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

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

img

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

img

# 案例:在hsdb工具中验证klass pointer正确性

操作步骤:

1、使用JOL打印对象的Klass Pointer。

2、使用Klass Pointer的地址,在hsdb工具中使用Inspector找到InstanceKlass对象。

注意:由于使用了小端存储,打印的地址要反着读。

img

这个0x254d3608就是klass对象的地址:

img

# 内存对齐

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

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

img

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

img

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

image-20240311233344007

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

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

img

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

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

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

img

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

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

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

img

# 案例:子类和父类的偏移量

需求:

通过如下代码验证下:子类继承自父类的属性,属性的偏移量和父类是一致的。

package oop1;

class A {
    long l;
    int i;
    String name;
}

class B extends A {
    long l;
    int i;
}

class C{
    long l1;
    int i1;
    String name;
    long l2;
    int i2;
}

结果如下:

img

总结:

img

上次更新: 2025/03/09, 18:29:07
栈上的数据存储
方法调用的原理

← 栈上的数据存储 方法调用的原理→

Theme by Vdoing | Copyright © 2022-2025 Salmon's Blog
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式