java对象的内存布局


对象头(Object Header):


***Mark Word:***用于存储对象的哈希码、GC分代年龄、锁状态标志等信息。Mark Word的具体实现与JVM的实现有关,但通常是一个非固定的数据结构以便在极小的空间内存储尽量多的信息。
***类型指针:***指向对象类型元数据的指针,JVM通过这个指针来确定这个对象是哪个类的实例。


实例数据(Instance Data):


存储对象中各个字段的值。这部分的大小会根据对象实际拥有的字段来决定。如果字段是引用类型,那么这里只保存引用,而真正的对象保存在堆内存中。


对齐填充(Padding):


由于JVM要求对象起始地址必须是8字节的整数倍(这取决于JVM的实际要求,可能是4字节、8字节或其他),因此当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
为什么要进行对其填充:因为jvm是8字节读取数据的,不填充的话会出现一个数据出现跨8情况(这个跨8没有这种叫法,是自己便于理解说出来的),跨8是什么情况呢,请看下图:
在这里插入图片描述
这里我们的long类型存在跨8,long本身是8字节,但是由于前面的 boolean + int + char 长度才到达7字节,jvm读取的时候,在第一次读取时会读取到 boolean + int + char +(long 的部分数据),这样导致long 被拆分为两部分数据,这种情况后期还要将long 两部分拼接起来,才能理解完整的long数据,这样导致数据的处理会比较地下。在这种情况下,人类就设计处理“对齐填充”

对其填充后是什么样子的呢?如下图:(被填充后,long就不存在跨8,这样不需要做long数据的拼接,数据的读取会更高)
在这里插入图片描述
看到这你有疑问吗?
我有疑问,比如你“填充1字节” 这个一字节的空间,如果填充毫无作用的数据,是不是会造成内存的浪费,内存可是很珍贵的资源。那这个空间我还能怎么利用呢?
那我为什么不能填充我其他定义的字段数据呢?比如我的一个类

public class User {
    private boolean isTrue;
    private int id;
    private Char sex;
    private Long cg;
    private boolean isOngy;
 }

这个类对齐填充,如果我能将isOngy 代替上面对齐填充的位置,那我不是可以节约一个字节的空间吗?如下图 (这个样子是不是节约了一个字节)

在这里插入图片描述
这里你当然会问,数据的填充不应该是按我定义的类的顺序来吗,这样不是打乱了顺序吗?
所以 在jvm中 我们是可以 优化 Java对象的字段分配或布局,以减少内存占用和对齐填充的,没想到吧,那有几种方式呢?
在虚拟机中有 -XX:FieldsAllocationStyle 这个参数可以控制 值一般有(0,1 ,2
类型0, 引用在原始类型前面, 然后依次是longs/doubles, ints, shorts/chars, bytes, 最后是填充字段, 以满足对其要求.
类型1, 引用在原始类型后面
类型2, JVM在布局时会尽量使父类对象和子对象挨在一起,

原始类型包括:

byte:8位有符号二进制整数
short:16位有符号二进制整数
int:32位有符号二进制整数
long:64位有符号二进制整数
float:单精度32位IEEE 754浮点数
double:双精度64位IEEE 754浮点数
char:16位Unicode字符
boolean:可以有两个值(true 和 false)

public static void main(String[] args) {
		User user = new User();
		System.out.println(user.hashCode());
		System.out.println(ClassLayout.parseInstance(user).toPrintable());
}
769287236
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
com.example.demo.dao.User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000002dda644401 (hash: 0x2dda6444; age: 0)
  8   4                    (object header: class)    0x01001800
 12   4     java.lang.Long User.id                   null
 16   4   java.lang.String User.username             null
 20   4   java.lang.String User.password             null
 24   4   java.lang.String User.email                null
 28   4   java.lang.String User.phoneNumber          null
 32   4      java.sql.Date User.registrationDate     null
 36   4      java.sql.Date User.lastLogin            null
 40   4   java.lang.String User.status               null
 44   4   java.lang.String User.role                 null
Instance size: 48 bytes

在上面的输出中可以看到
0x0000002dda644401 在这个值中你可以发现jvm中将数据存储在寄存器中 的存储方式是按照大端存储 ,高位字节存储在地位地址中 “0x0000002dda644401”
大端存储方便正负号的判定,有利于进行运算。

小端存储 有利于进制转换

0   8   (object header: mark)     0x0000002dda644401 (hash: 0x2dda6444; age: 0)

回到刚刚的代码中

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

上面的代码中user 是被放在 main 对应的栈帧下的局部变量表中,保存的是对 new User()的引用,当需要访问对象实例时,有两种访问方式:
1 句柄池访问
使用句柄访问对象,会在堆中开辟一块内存作为句柄池,句柄中储存了对象实例数据(属性值结构体) 的内存地址,访问类型数据的内存地址(类信息,方法类型信息),对象实例数据一般也在heap中开 辟,类型数据一般储存在方法区中。
优点 :reference存储(引用)的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为) 时只会改变句柄池中的实例数据指针,而reference(引用)本身不需要改变。当多个reference 引用时,需要修改user实例的指针,只需要修改句柄池中的user实例地址指针,只需修改一次。
缺点 :增加了一次指针定位的时间开销

图解
2 直接指针访访问
在这里插入图片描述
优点 :节省了一次指针定位的开销。

缺点 :在对象被移动时(如进行GC后的内存重新排列),reference(引用)本身需要被修改


你可能好奇


com.example.demo.dao.User object internals:
OFF  SZ               TYPE DESCRIPTION               VALUE
  0   8                    (object header: mark)     0x0000002dda644401 (hash: 0x2dda6444; age: 0)
  8   4                    (object header: class)    0x01001800
 12   4     java.lang.Long User.id                   2
 16   4   java.lang.String User.username             null
 20   4   java.lang.String User.password             null
 24   4   java.lang.String User.email                null
 28   4   java.lang.String User.phoneNumber          null
 32   4      java.sql.Date User.registrationDate     null
 36   4      java.sql.Date User.lastLogin            null
 40   4   java.lang.String User.status               null
 44   4   java.lang.String User.role                 null
Instance size: 48 bytes

这个里输出的为什么是4个字节,像String 不应该是8个字节吗,为什么会是4个字节,这里其实涉及到了指针压缩技术。
关闭指针压缩 -XX:-UseCompressedOops(调整这个参数) 这个参数默认是开启的
在这里插入图片描述
这里我们思考一下两种情况:
1 当我使用的32 位寻址,但是我的堆内存配置的是大于4G, 32位寻址无法表示超过4G外的地址,这个时候jvm或出现什么情况
开启了32位指针压缩(例如使用CompressedOops),但是将堆内存大小设置为超过压缩指针所能表示的范围(对于32位指针压缩,这个范围大约是4GB),那么JVM在启动时就会报错,并不会允许你设置超过限制的堆内存大小。

Error occurred during initialization of VM  
Could not reserve enough space for object heap  
Error: Could not create the Java Virtual Machine.  
Error: A fatal exception has occurred. Program will exit.

2 堆内存小于 32位寻址的内存大小,这个时候会有什么报错?
如果在程序运行过程中出现了内存不足的情况,即使堆内存设置小于32位寻址的限制,JVM仍然可能会抛出OutOfMemoryError。这种错误通常发生在以下几种情况:

堆内存耗尽:尽管你的堆内存设置小于4GB,但如果你的应用程序创建了太多的对象,或者某些对象占用了大量的内存,并且这些对象无法被垃圾回收器及时回收,那么堆内存仍然可能会耗尽,导致OutOfMemoryError。

元数据空间不足:除了堆内存外,JVM还有元数据空间(Metaspace)用于存储类的元数据。如果加载了太多的类,或者某些类占用了大量的元数据空间,也可能导致OutOfMemoryError,即使堆内存设置得足够小。

直接内存溢出:如果你的应用程序使用了大量的直接内存(通过ByteBuffer.allocateDirect等方式分配),并且没有正确管理这些内存,那么即使堆内存没有耗尽,也可能因为直接内存溢出而导致程序崩溃或异常。

线程栈溢出:每个线程都有自己的栈空间,如果线程栈大小设置不当或者线程中递归调用过深,可能导致栈溢出错误(StackOverflowError),而不是OutOfMemoryError,但这也是与内存管理相关的一个常见问题。

如果确实需要在32位系统中,jvm需要大于4G的内存空间,可以使用PAE(物理地址扩展) 的特殊内核区支持32位 寻址大于4G的内存空间的功能需求。

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值