java对象的创建、内存布局和访问定位

java是一门面向对象的编程语言,在java程序运行过程中无时无刻都有对象被创建。
创建对象的方式有多种:new、克隆、反序列化

1、JVM中创建对象的过程

虚拟机遇到一条new指令,开始通过以下步骤创建对象:
第一步: 检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否完成加载、解析和初始化过程,如果没有,则必须先执行类加载过程
第二步: 类加载检查通过后,虚拟机将为新生对象分配内存。对象所需的内存空间大小在类加载完成后就可以完全确定。
为对象分配内存空间时,根据java堆内存是否规整可分为两种方式。而java堆内存是否规整与使用的垃圾收集器的种类有关。用Serial、ParNew等采用压缩复制算法的垃圾收集器的java堆内存空间是规整的;而用CMS这种标记清除算法的垃圾收集器的堆内存空间则不是规整的。

  • 指针碰撞:适用于java堆内存规整的情况。所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器。分配内存仅仅是将指针向空闲的一边挪动一段与对象所需空间相等的距离。
  • 空闲列表:适用于java堆内存不规整的情况。java堆内存使用过的内存和空闲内存相互交错,无法使用指针碰撞,所以虚拟机自己维护一个列表,记录空闲内存块的地址,再为对象分配内存的时候直接从列表中找一块足够大的内存分配。

分配空间的线程安全问题:创建对象在虚拟机中是非常频繁的行为,在并发情况下,多线程共享堆内存,可能出现正在给A分配内存,指针还没来得及修改,其他线程又使用了原来的指针给对象B分配内存的情况。
解决这个问题有两种方案:

  • 对分配内存空间的动作进行同步处理。实际上虚拟机采用CAS加上失败重试的方式保证更新操作的原子性
  • 把内存分配的动作按照线程划分在不同的内存空间之中进行,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。

虚拟机是否使用TLAB,可以通过-XX:=/-UseTLAB参数来设定。

**第三步:**内存分配完后,虚拟机将需要分配到的内存空间都初始化为零值(不包括对象头),这一操作保证了对象的实例字段在java代码中可以不赋初始值就直接使用,程序也能访问到这些字段的数据类型零值。
第四步: 虚拟机为对象设置对象头信息,包括这个对象所属的类、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄等。

经过这四步,对象就在内存中被创建出来,可以执行<init>方法进行初始化。

2、对象的内存布局

在HotSpot虚拟机中,对象在内存中的存储布局可以分为三个区域:对象头、实例数据和对齐填充。
对象头
对象头包括对象运行时区域和指向类元数据的指针这两部分信息。对象运行时数据包括哈希吗、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等信息。类型指针数据用来确定这个对象是哪个类的实例,存储指向方法区中对应类型信息的指针。
实例数据
对象真正存储的有效信息,也是程序代码中定义的各种字段类型的内容。
对齐填充
仅仅是起着占位符的作用,没有特别含义。HotSpot虚拟机内存管理要求对象的起始地址必须是8字节的整数倍。对象实例数据部分没有对齐的时候就需要通过对齐填充来补全。

3、对象的访问定位

建立对象是为了使用对象,java程序通过栈上的reference数据来操作堆上的具体对象。访问堆中的具体对象方式有使用句柄和直接指针两种。
使用句柄
该方式java堆中会划分一块内存来作为句柄池,reference中存储着对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自具体的地址信息。
优势:使用句柄方式访问对象的最大好处是reference中存储的是稳定的句柄地址,在对象被移动时只需修改句柄中的实例数据指针,而栈中的reference本身不用需改。
直接指针
如果使用直接指针访问,java堆对象的实例数据中就必须包含访问类型数据的指针,而reference中存储的直接就是对象的地址。
优势:使用直接指针访问方式的最大好处是速度快。节省了一次指针定位的开销。

HotSpot虚拟机采用直接指针的方式进行对象访问。

发布了43 篇原创文章 · 获赞 17 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/weixin_41172473/article/details/85331273
今日推荐