Java对象详解——初始化过程,内存布局,内存分配过程

本文详细探讨了Java对象的初始化过程,包括半加载过程和可能的CPU指令重排问题,强调了在多线程环境下使用volatile关键字的重要性。接着分析了对象在内存中的存储布局,分别说明了普通类对象和数组对象的组成部分。文章还深入讲解了压缩指针的概念,解释了其原理和应用场景,以及对象定位的两种方式。最后,概述了对象在堆和栈中的分配过程,提到了TLAB和不同代之间的对象迁移规则。

对象的初始化过程

对象初始化的过程是一个半加载的过程

对于

class T{

int M = 8;
}

T t = new T();
  1. 在存放区域申请空间,为对象开辟空间,并将变量赋予初值(基本数据0,对象null),此时M=0。
  2. 执行构造方法,对变量初始化,此时M=8。
  3. 将栈空间的引用t与存放对象位置的空间建立连接。

过程很简单,但是对于这个过程,可能会由于CPU的指令重排而导致出现2和3顺序互换。即出现:

  1. 将栈空间的引用t与存放对象位置的空间建立连接,此时M=0。
  2. 执行构造方法,对变量初始化,此时M=8。

此时如果我们的代码中出现了:

 类似于这样的Double Check(双重检查)的代码块。

如果是在多线程的情况下,很有可能A线程拿到了锁,去初始化对象,而B线程来的时候,A刚刚执行完上述的两步,但是此时CPU发生了指令重排,也就是说,A线程执行了1和3。此时引用已经连接,B线程发现a已经不是null了,就去输出了。但是a还并没有赋值。就会输出0。会导致错误。

所以在Double Check初始化对象的时候,一定要加上volatile关键字。该关键字可以透明化变量,并防止指令重排的发生。例如:Double Check单例,Double Check自旋。

对象在内存中的存储布局

对象在内存中分为两种:一种是普通的类对象,一种是数组对象。

普通对象分为四块 markword、Class pointer(类型指针)、instance date、padding(对齐)

数组对象分为五块 markword、Class pointer、length(数组长度)、instance date、padding

Markword:8字节,存储了锁信息(synchronized),hashcode,GC信息。

也就是说,synchronized的原理就是修改了对象的Markword信息。

Class pointer:4字节或8字节,类型指针,存储的是自己类Class对象的地址。大小根据内存占用量而定,当内存大于32G,扩张为8字节,平时被压缩为4字节。它与Markword共同组成对象头

Length:4字节,数组对象特有,存储数组长度

Instance date:存储对象中的数据,比如int m 就存储在这里,大小根据变量多少而定。

Padding:对齐位,在64位操作系统下,java采用8字节存储,所以,在使用8的幂次方数存储时效率会很高,该位的意义就在于对位数进行补齐,保证该类的大小为8的幂次方数,对齐是方便压缩指针操作。大小由字节的前几个字段而定。

Eg:Object类占多少个字节? markword 8字节+class pointer4字节+4字节padding补齐

对象头=Markword + Class pointer 共12/16字节。

对象头包括:锁信息,hashcode,GC信息(markword)以及Class类指针(class pointer)

所以,对于一个对象进行上锁,或者打印hashcode,则会修改它的对象头中的内容。

压缩指针分两部分

在JVM中由-XX:+UseCompressedClassPointers 压缩类指针class pointer。

另一个JVM参数 -XX:+UseCompressedOops 压缩普通对象指针

压缩指针的含义:

当我们new出一个对象,在内存中,这个对象分成四段

  1. 第一段markworld

记载了锁,GC的信息以及hashCode,64位机大小固定为8字节。

  1. 第二段class pointer

这一段会指向该对象所对应的的类.class。这一部分指向类的指针是被压缩过的。

当我们是64位系统,一个引用指针占8个字节,但是8个字节太占空间了,而且会对寻址带宽和对象引用造成负担,所以JVM会帮我们压缩这个引用指针,压缩成4个字节。

  1. 第三段InstanceData

这一段存放对象中的成员变量之类的数据。

  1. 第四段padding

填补作用,让整个对象的引用指针对齐8字节,当我们的对象大小为12字节时,由于不能被8整除,JVM会将其填补至16字节。

所以,以上得出,所有对象都是可以被8整除的。这是因为8字节正好对齐64位,当JVM寄存器是64位时特别方便。

对于class pointer的引用指针压缩的过程和成员变量中引用的压缩过程,称之为压缩指针。可以节省空间,提高JVM效率。

对于成员变量中的引用类型要压缩的对象有:

1.对象的全局静态变量(即类属性)

2.对象头信息:64位平台下,原生对象头大小为16字节,压缩后为12字节

3.对象的引用类型:64位平台下,引用类型本身大小为8字节,压缩后为4字节

4.对象数组类型:64位平台下,数组类型本身大小为24字节,压缩后16字节

在此处要区分清楚引用类型和基本类型。

 所以此处启用指针压缩的话,A中的int还是4字节不压缩,而B中的Integer就要从8字节压缩为4字节。

压缩指针的原理:

  那么原本8字节才能存储的地址,压缩为4个字节,那JVM怎么做到的呢?如果直接去掉32位那JVM岂不是乱掉了。4字节对应的32位地址仅仅能读取到4GB大小的内存空间,为什么JVM用这4字节可以读取到超越4GB大小的内存空间呢?

实际上class pointer中的这个地址并不是直接地址,而是JVM处理过后的一个编码。由于刚才讲述的:java所有的对象都可以被8整除。8的二进制为1000,所以在8位对齐情况下的所有java对象地址末尾都是000。所以JVM在存放地址时会在末尾去除3个0,当我们要使用的时候再为其加上这3个0。由于原本32位的地址空间又加上了三个0,所以实际可以搜索到的内存空间为2的35次方,也就是32G。这也正是为什么,压缩指针在超过32G的内存下会失效。因为该算法最高可达内存容量为32G,超过了32G为了保证能够搜索到整个内存空间。自然就要加大地址位数,扩容到64位地址。

Oop是什么

Oops:普通对象指针

普通对象指针:一个对象的成员变量的引用指针,例如String name = “....” 这个name的指针。普通对象指针不开压缩为8,开压缩为4。

即:Object o = new Object(); 32G内存以下这一行内容所占的是20字节(o:4 new Object():16)

对象怎么定位

对象定位共分为两种

Hotspot默认的:直接指针式

引用直接指向对象并通过堆中对象的类型指针去指向该类的Class文件。

句柄式

引用首先指向内存中的两个指针,通过实例数据指针指向对象,类型数值指针指向Class对象。

对比:

二者的内存消耗是相同的;

直接指针访问速度更快,但是GC在进行工作时复制对象的过程(对象的分配过程第7,8步)更加复杂(需要修改t的指向位置);

句柄式访问速度较慢,但是GC在进行工作时只需要修改指针即可,速度更快。

对象的分配过程

  1. 先判断对象初始化时在栈还是堆中初始化,如果在栈则直接在栈初始化,直到不用了直接弹出。
  2. 判断该对象是不是较大的对象,如果是,则直接丢入老年代,直到FGC清除。
  3. 如果对象较小,判断能否存入TLAB(Thread Local Allocation buffer,线程本地缓冲区)中,如果可以,则存入TLAB让对应线程直接存入伊甸区。
  4. 如果不能存入TLAB,则自己放到伊甸区(TLAB是伊甸区的一部分)。
  5. 第一轮YGC清除伊甸区的对象,如果清除则结束。
  6. 没有被清除的对象,会转移至Servive1区。
  7. YGC清理Serviv1区,没有被清除的对象转移至Serviv2区。
  8. YGC清理Servic2区,没有被清除的对象转移至Serviv1区。
  9. Serviv1,2区如果大小超过一半,则将年龄较大的部分对象直接移动至老年代。
  10. 等到年龄(GC清理次数)大于15,该对象转移至老年代,直到FGC清除。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值