JAVA对象的创建、内存布局和访问

目录

一、JAVA对象的创建

1、内存分配的并发问题

1.1、CAS(compare and swap)乐观锁

1.2、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

2、init()方法和clinit()方法

2.1、init()方法

2.2、clinit()方法

二、Java对象的内存布局

三、Java对象的访问(实例数据+类型数据)

1、直接指针访问

2、句柄访问

3、两种访问方式的对比

4、什么是reference类型数据


一、JAVA对象的创建

Java对象的创建过程图解:

JAVA对象的创建、内存布局和访问

1、内存分配的并发问题

内存分配需要执行两个步骤:1、内存分配;2、指针修改;可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。

解决并发问题的方法:

1.1、CAS(compare and swap)乐观锁

虚拟机采用CAS配置上失败重试的方式保证更新操作的原子性来对分配内存空间的动作进行同步处理。

乐观锁和悲观锁的区分:

悲观锁:独占锁是一种悲观锁,而 synchronized 就是一种独占锁,synchronized 会导致其它所有未持有锁的线程阻塞,而等待持有锁的线程释放锁。 

乐观锁: 所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。而乐观锁用到的机制就是CAS。

什么是CAS?

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

这个操作就是使用期待值地址的实际值进行比较,如果地址的实际值跟预期值一样,说明我们这个数据就是我们需要操作的数据,可以执行修改操作(操作正确的数据),但是地址的实际值与期待值不一致时,说明此时想要操作的值已经被篡改过了,因此不能再操作这个篡改后的值(在错误的数据上操作只能得到错误的结果)。

当操作失败时,线程重新获取内存地址V的当前值,并重新计算想要修改的新值。这个重新尝试的过程被成为自旋

对于CAS的理解请尝试这篇文章:

https://blog.csdn.net/qq_35571554/article/details/82892806

1.2、本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)

把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一小块内存。

2、init()方法和clinit()方法

2.1、init()方法

Java在编译之后会在字节码文件中生成init方法,称之为实例构造器,该构造器会将语句块,变量初始化,调用父类的构造器等操作收敛到init方法中(收敛到init方法的意思是:将这些操作放到init中去执行),收敛顺序为:

  1. 父类变量初始化
  2. 父类语句块
  3. 父类构造函数
  4. 子类变量初始化
  5. 子类语句块
  6. 子类构造函数

2.2、clinit()方法

Java在编译之后会在字节码文件中生成clinit()方法,称之为类构造器。类构造器同实例构造器一样,也会将静态语句块,静态变量初始化,收敛到clinit()方法中,收敛顺序为:

  1. 父类静态变量初始化
  2. 父类静态语句块
  3. 子类静态变量初始化
  4. 子类静态语句块
  • 父类为接口,则不会调用父类的clinit()方法,一个类可以没有clinit()方法。
  • clinit()方法是在类加载过程中执行的,而init()方法是在对象实例化执行的,所以clinit()一定比init()方法先执行

整个顺序就是:

  1. 父类静态变量初始化
  2. 父类静态语句块
  3. 子类静态变量初始化
  4. 子类静态语句块
  5. 父类变量初始化
  6. 父类语句块
  7. 父类构造函数
  8. 子类变量初始化
  9. 子类语句块
  10. 子类构造函数

二、Java对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)实例数据(Instance Data)对齐填充(Padding)。 HotSpot虚拟机的对象头包括两部分信息,第一部分用于存储对象自身的运行时数据, 如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

初始化零值之后,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。

内存布局图示:

JAVA对象的创建、内存布局和访问

三、Java对象的访问(实例数据+类型数据)

建立对象是为了使用对象,我们的java程序需要通过栈上的reference数据来操作堆上的具体对象。由于reference类型在java虚拟机规范中之规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式定位、访问堆中的对象的具体位置,所以对象访问方式也取决于虚拟机的实现。目前主流的访问方式有使用句柄和直接指针两种。

1、直接指针访问

reference变量中直接存储的就是对象的地址,而java堆对象一部分存储了对象实例数据,另外一部分存储了对象类型数据。

JAVA对象的创建、内存布局和访问

2、句柄访问

java堆中将划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和类型数据各自的具体地址信息。

JAVA对象的创建、内存布局和访问

3、两种访问方式的对比

1、使用句柄最大的好处就是reference存储的是稳定的句柄地址,在对象被移动时只会改变句柄中的实例数据指针,而reference本身不需要修改(垃圾回收效率高,访问效率低)

2、使用直接指针的方式最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在java中非常频繁,因此这类开销积少成多后也是一项非常可观的执行成本(垃圾回收效率低,访问效率高)

注:HotSpot虚拟机使用了直接指针的方式访问对象。

4、什么是reference类型数据

 如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用;

强引用(Strong Reference)

最常的创建对象方式就是 new 一个对象,然后将其赋值给一个声明为这个对象的类型及其父类的引用。如果对象有一个 StrongReference ,那么这个对象将不会被gc回收。

软引用(Soft Reference)

一个对象没有 StrongReference 但存在一个 SoftReference ,那么 gc 将会在虚拟机需要释放一些内存的时候回收这个对象。可以通过对对象的 SoftReference 调用 get() 方法获取该对象。如果这个对象没有被 gc 回收,则返回此对象,否则返回 null 。

弱引用(Weak Reference)

一个对象没有 StrongReference 但有存在一个 WeakReference ,那么 gc 将会在下一次运行时对其进行回收,哪怕虚拟机的内存还足够多。

虚引用(Phantom Reference)

reference 主要是用来与虚拟机 gc 进行交互,使得虚拟机根据对象的不同引用类型,对其采用不同的内存回收策略。strong 引用的对象正常情况下不会被回收,soft 引用的对象会在出现 OOM 错误之前被回收,而 weak 引用的对象在下一次 gc 的时候就会被回收。