对象的创建


虚拟机遇到一条new指令时,首先会去常量池中查看能否定位到该类的符号引用,并且检查这个类是否有加载连接初始化过。如果没有,那么要先执行该类的加载过程,将类加载到虚拟机中。
内存分配方式
在类的加载通过之后,接下来就要给该类的实例对象分配内存空间了。对象的内存空间在类加载完之后就确定了,为类分配空间也就是在堆内存中取出一块划分给该对象拥有。

假设堆的内存是完整的,所有用过的内存放在一边,没用过的放在另一边,中间用一个指针隔开当成分界指示器,当我们给一个对象分配内存时,也就是将指针往空闲的方向移动类的大小个单位,这种分配方式被称为“指针碰撞”(Bump the pointer);

如果堆中的内存不是完整的,也就是已使用的内存与空闲内存交替占用,那么我们分配空间就不能够通过指针碰撞了,虚拟机必须维护一个列表用来记录哪些内存是可用的,当我们给对象分配内存时,就可在列表中找到足够的内存空间分配给该对象并更新列表记录,这种分配方式被称为“空闲列表”(Free List)

选择哪种内存分配方式由java堆是否完整决定,而java堆是否完整由垃圾收集器的机制决定。当使用带有压缩(compact)机制的收集器比如Serial/ParNew时,系统采用的是指针碰撞,而采用CMS基于标记清理(Mark-Sweep)机制的算法收集器时,采用的是空闲列表
内存分配中的并发问题
对象创建行为时一个非常频繁的行为,即使是修改一个指针,在并发环境里也不是线程安全的。可能出现正在给A分配内存空间,指针还没来得及修改,对象B又使用了指针来分配内存。两种解决思路:一种是对分配的内存空间采用同步处理:实际上虚拟机采用CAS(乐观锁的一种)配上失败重试的方式来保证更新的原子性;另一种是将内存的分配以线程隔开,也就是在创建线程的时候,每个线程在java堆中先分配一块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),用来给本线程的对象分配内存空间,只有TLAB用完时,才需要同步锁。
当分配完内存之后,接下来就要对对象做必要的设置了,例如该对象是哪个类的实例、如何才能找到类的元数据类型、对象的哈希吗、对象的GC分代年龄等信息。这部分信息存放在对象的对象头部分。
当上面工作都完成后,其实就可以使用该对象了,不过对象只是进行了默认初始化里面数据都为默认的,然后就会执行init方法了,将对象按照创建时的代码进行初始化,这样才算是一个真正的对象。

CAS:一项乐观锁技术,当多个线程尝试使用CAS同时更新一个变量时,只有一个线程能够改变变量的值,失败的线程不会被挂起,而是被告知竞争失败可重新尝试这个行为。

对象的内存布局
Hotspot虚拟机中,对象在内存中的存储布局可分为三个区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
对象头
hotspot虚拟机中,对象头分为两部分信息。一部分用于存储对象自身的运行时数据(主要表示对象特征或者状态的),如哈希吗、GC分代年龄、锁状态标志、线程持有的锁、偏向线程id、偏向时间戳等,这部分数据长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为’Mark Word’。对象头的另一部分是类型指针,即对象指向他的类元数据的指针,虚拟机可通过这个指针来确定该对象是哪个类的实例。也并不是所有的对象都有类型指针,也就是并不是所有的查找对象的元类数据都经过对象本身。另外当对象是一个数组时,对象头还需要增加一个记录数组长度的数据,因为虚拟机可以通过普通类来判断对象的大小,但是数组的元数据却无法确定大小。
实例数据
实例数据部分是对象真正存储有效信息的地方,也是在程序中所定义的各种类型的字段内容。无论是从父类继承的还是子类中定义的都需要记录下来,该部分的存储顺序受到虚拟机分配策略参数和字段在java源码中定义顺序的影响。Hotspot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/boolens、oop(Ordinary Object Pointers普通对象指针),相同宽度的字段总是被分到一起。在满足此前提下,父类定义的变量在前,如果CompactFields参数值为true,那么子类中较窄的变量也可能插入到父类变量的空隙之中。
对齐填充
对齐填充并不是必然的,它仅仅起占位符的作用。由于hotspot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小就是8字节的整数倍,而对象头正好是8字节的倍数,所以当实例数据部分没有对齐8的倍数时就需要通过对齐填充来补全。

对象的访问定位
建立对象就是为了使用对象,我们的java程序只需要栈上的refrence数据来操作堆上的具体对象。由于refrence类型在java虚拟机规范中只定义了一个指向对象的引用,并没有规定怎么去定位访问堆中的对象。当前主流的访问方式有句柄以及直接指针两种:
句柄访问
如果使用句柄访问,java堆中多出一块内存用来做句柄池,refrence中存储的就是对象的句柄信息,句柄中包含了对象的实例数据与类型数据的地址信息。

 

 

指针访问
使用指针访问refrence存储的就是对象地址(sun的hotspot就是使用的这种)
两种方式优缺点:
使用句柄访问最大好处是refrence存储的是稳定的句柄信息,在对象被移动(垃圾收集行为非常普遍)时只需要改变句柄中实际数据指针,refrence本身不需要修改。
使用指针访问优点就是快速,他节省了一次指针定位的时间开销,由于对象的的访问在java中非常频繁,积少成多也是可观的提升。

 

 


————————————————
原文链接:https://blog.csdn.net/k1234o/article/details/80602849

posted @ 2022-02-28 21:23  Nausicaa0505  阅读(39)  评论(0编辑  收藏  举报