1、对象的实例化

1.1 创建对象的方式

  • 使用new关键字:

这是Java中最常用的:user user = new User();

  • 通过clone()方法创建对象:

想要让一个对象支持clone,必须让这个对象对应的类实现Cloneable接口,同时此类中也要重写clone()方法。

  • 使用反射机制创建对象:

反射机制:
Java反射机制是在程序运行时,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。这种动态地获取信息以及动态调用对象方法的功能称为Java的反射机制。
使用反射创建对象,主要有三个步骤:
① 获取类的Class对象实例,获取方式有:

  1. - Class.forName("类的全路径")
  2. - 类名.class;如:User.class
  3. - 对象名.getClass();

② 通过反射创建类对象的实例对象;
获取Class对象实例后,可以通过Java反射机制创建类对象的实例对象,主要有两种方式:

  - 调用无参的构造方法:Class.newInstance();此方法必须确保类中有无参的可以见的构方法,否则会抛出异常;
  - 调用类对象的构造方法

③ 将创建出的实例对象强制转换为所需的类型;

  • 使用反序列化创建对象:

序列化反序列化:
Java中,序列化是指将Java对象转换为字节序列的过程,而反序列化是将字节序列转换为Java对象的过程。也可以理解序列化是为:把变量从内存中变成可以存储或者传输的过程。
其实就是把对象写入IO流中,Java中要序列化的类必须实现Serializable接口。
使用反序列化从文件、网络等获取一个对象的二进制流。

1.2 对象创建的步骤

  1. 判断对象对应的类是否加载、链接、初始化;

当虚拟机遇到一条字节码new指令时。首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否被加载解析初始化过。如果没有,在双亲委派模式下,使用当前类加载器以ClassLoader+包名+类名为key值进行查找对应的.class文件,如果没有找到文件,则抛出ClassNotFoundException异常。

  1. 为对象分配内存;
    1. 首先计算对象占用空间的大小,接着在堆中划分一块内存给新对象,如果实例成员变量是引用变量,仅分配引用变量空间即可,即4个字节大小;
    2. 如果Java堆内存中不规则,虚拟机就必须维护一个列表,记录哪些内存可用,哪些不可用。分配的时候在列表中找一个足够大的空间分配,然后更新列表。这种分配方式叫空闲列表(Free List);
    3. 选择哪种由Java堆是否规整决定,Java堆是否规整由所采用的的垃圾收集器是否带有空间压缩整理(Compact)的能力决定:
      1. 当使用Serial,ParNew等带有压缩整理过程的收集器,指针碰撞简单高效;
      2. 当使用CMS基于清除(Sweep)算法收集器时,只能采用空闲列表来分配内存;(CMS为了能在多数情况下分配内存更快,设计了一个Linear Allocatioin Buffer的分配缓冲区,通过空闲列表拿到一大块分配缓冲区后,在它里面仍可使用指针碰撞方式分配);
    4. 假设Java 堆中内存时绝对规整的,所有被使用过的内存放在一边,空闲的内存放在另一边,中间放一个指针作为分界点指示器。那么内存分配就是指针指向空闲的方向,挪动一段与对象大小相等的举例。这种分配方式成为指针碰撞(Bump The Pointer)。
  2. 处理并发安全问题;
    1. 对象创建是非常频繁的行为,还需要考虑并发情况下,仅仅修改一个指针所指向的位置也是不安全的,例如正在给对象A分配内存,指针还未修改,对象B又使用原来的指针分配内存。解决问题有两种可选方案:
    2. 对分配内存空间的动作进行同步处理。实际上虚拟机采取CAS配上失败重试的方式保证更新操作的原子性。
    3. 把内存分配的动作按照线程划分到不同的空间中进行,每个线程在Java堆中,预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。
    4. 虚拟机是否使用TLAB,可以通过-XX: +/-UseTLAB参数来设定。
  3. 初始化分配到的空间;
    1. 内存分配完成后,虚拟机将分配到的内存空间(不包括对象头)都初始化为零值。如果使用了TLAB,这个工作可以提前到TLAB分配时进行。这步操作保证对象的实例字段在Java代码中,可以不赋初始值就直接使用,程序可以访问到字段对应数据类型所对应的零值。
  4. 设置对象的对象头;
    1. 接下来Java虚拟机还要对对象进行必要的设置,例如对象时哪个类的实例、如何才能找到类的元数据信息,对象的哈希码(实际上对象的HashCode会延后真正调用Object::hashCode()方法时才计算)、对象的GC分代年龄等信息。这些信息存放到对象的对象头(Object Header)
  5. 执行init方法进行初始化;
    1. 上面工作完成后,从虚拟机角度来说,一个新的对象已经产生了,但是从Java程序的视角来说,对象创建才刚刚开始,对象的构造方法(Class文件中init()方法)还未执行,所有字段都是默认的零值。new指令之后接着执行init方法,按照程序员的意愿对对象进行初始化,这样一个真正可用的对象才算完全构造出来。

2、对象的内存布局

在JVM中,一个对象在内存中分为三个部分:对象头、实例数据和对齐填充
对象头(Header):
对象头包含两个部分,一部分是标记字(Mark Word),另一部分是类型指针,如果对象是数组,还需要记录数组的长度
标记字(Mark Word):

  • 官方对标记字的称呼是Mark Word。
  • 用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
  • 以上描述的数据的长度,在32位和64为虚拟机中分别为32bit和64bit。
  • 实际上,对象需要存储的运行时数据很多,其实已经超出了32位或者64位bit能记录的限度,但是对象头信息时与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构,以便在极小的空间内存储尽量多的信息。

类型指针:
即对象指向它的类型元数据的指针,JVM通过这个指针来确认该对象属于哪个类的实例。
数组长度:
如果对象是一个Java数组,那么在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
实例数据(Instance Data):

  • 实例数据部分是对象真正存储的有效信息,也即我们在程序代码中定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的,都需要记录起来。
  • 这部分的存储顺序会收到虚拟机分配策略参数(-XX:FieldsAllocationStyle)和字段在J ava源码中定义顺序的影响:
    • HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers);默认的分配策略中,相同宽度的字段总是被分配到一起存放。在满足这个前提的情况下,在父类中定义的变量会出现在子类之前。
    • 如果HotSpot虚拟机的+XX:CompactFields参数值为true(默认也是true),那么子类中较窄的变量也允许插入父类变量的空隙之间,以节省一点点空间。

对齐填充(Padding):
对其填充,这并不是必然存在,没有特别的意义,它仅仅起着占位符的作用。因为HotSpot虚拟机自动内存管理系统,要对对象的起始地址必须是8字节的整数倍,换句话就是任何对象的大小都必须是8字节的整数倍。对象头已经精心设计为8字节的整数倍,1倍或者2倍。对象实例数据部分如果没有对齐的话,就需要通过对其填充来补全。

3、对象的访问定位

我们创建对象是为了使用对象。Java程序需要通过栈上的reference数据来操作堆上的具体对象。而reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中对象的具体位置,所以对象访问方式是取决于虚拟机实现而定的。
目前主流的访问方式有两种:使用句柄和直接指针。
使用句柄:
使用句柄,Java堆中将画出一块内存最为句柄池,reference中存储的就是对象的句柄地址,句柄包含对象实例数据与类型数据各自的具体信息:
HotSpot虚拟机中对象相关信息 - 图1
直接指针:
使用指针,reference中存储的直接就是对象地址,如果访问对象本身,不需要多一次的间接访问的开销。
HotSpot虚拟机中对象相关信息 - 图2
两种方式各有优势:
使用句柄的优势:reference中存放的是稳定的句柄地址,在对象被移动(垃圾收集时会产生)时只改变句柄中实例数据指针,reference本身不用改变;
使用指针的优势:最大好处在于速度快,节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,所以积少成多也是一项可观的执行成本。
就HotSpot虚拟机来说,它主要使用直接指针的方式进行对象访问。当然也有例外情况,如果使用Shenandoah收集器的话,也会有一次额外的转发。