JVM对象
一. 对象创建过程
前置知识
在 Java 程序中,我们拥有多种新建对象的方式。最常见的就是通过 new 语句,除此之外还有反射、系列化、深浅拷贝、通过Unsafe等直接操作内存。
其中深浅拷贝和反序列化通过直接复制已有的数据,Unsafe等操作内存的方法也没有初始化实例字段,而 new 语句和反射机制,则是通过调用构造器来初始化实例字段。
构造器:
1. 如果没有指定构造器方法,Java 编译器会自动添加一个无参数的构造器。
2. 子类的构造器方法,需要调用父类的构造器。如果父类存在无参数构造器的话,该调用可以是隐式的。但是,如果父类没有无参数构造器,那么子类的构造器则需要显式地调用父类带参数的构造器。
3. 显式调用又可分为两种,一是直接使用“super”关键字调用父类构造器,二是使用“this”关键字调用同一个类中的其他构造器。无论是直接的显式调用,还是间接的显式调用,都需要作为构造器的第一条语句,以便优先初始化继承而来的父类字段。(不过这可以通过调用其他生成参数的方法,或者字节码注入来绕开。)
当我们调用一个构造器时,它将优先调用父类的构造器,直至 Object 类。这些构造器的调用者皆为同一对象,也就是通过 new 指令新建而来的对象。也就是说,这个 new 出来的对象包含了所有父类的实例字段(包括私有字段)。
创建过程
检查加载:
首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用(符号引用 :符号引用以一组符号来描述所引用的目标),并且检查类是否已经被加载、 解析和初始化过。
分配内存:
接下来虚拟机将为新生对象分配内存。为对象分配空间的任务等同于把一块确定大小的内存从 Java 堆中划分出来。
分配内存涉及到两个重要的问题:
1. 如何划分内存?
1.1 指针碰撞:如果内存是被整齐的划分为一块已使用和一块未使用,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”。
1.2 空闲列表:如果内存不是规整的,就需要维护一个空闲列表,指向哪些内存区域是可用的。
选用哪种划分方法,和具体的垃圾回收器相关。指针碰撞需要整理内存空间,但是仅需要维护一个移动指针 分配速度很快。空闲列表不用整理内存空间,但是需要额外维护一个空闲列表,而且会造成内存碎片的问题 且分配地址需要遍历空闲列表寻找合适大小的内存 速度较慢。
2. 如何解决并发问题
以指针碰撞为例,如果两个线程同时要划分一块区域过来,就会出现并发安全问题。常见的有两种方式解决这个问题:
2.1 通过 CAS 乐观锁。
2.2 通过 TLAB 本地线程分配缓冲,这个方法的宗旨是:如果每个线程都有一块自己的内存区域,那就不存在竞争问题了。
内存空间初始化
(注意不是构造方法)内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(如 int 值为 0,boolean 值为 false 等等)。这一步操作保证了对象 的实例字段在 Java 代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
设置
接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息(Java classes 在 Java hotspot VM 内部表示为类 元数据)、对象的哈希码、对象的 GC 分代年龄等信息。这些信息存放在对象的对象头之中。
对象初始化
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象创建才刚刚开始,所有的字段都还为零值。 所以,一般来说,执行 new 指令之后会接着把对象按照程序员的意愿进行初始化(构造方法),这样一个真正可用的对象才算完全产生出来。
二. 对象内存布局
对象头中有很多信息
实例数据存储实例字段的值
对象填充:由于 HotSpot VM 的自动内存管理系统要求对对象的大小必须 是 8 字节的整数倍。当对象其他数据部分没有对齐时,就需要通过对齐填充来补全。
对象定位:
句柄:
如果使用句柄访问的话,那么 Java 堆中将会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息。
使用句柄来访问的最大好处就是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实 例数据指针,而 reference 本身不需要修改。
直接指针:
如果使用直接指针访问, reference 中存储的直接就是对象地址。
这两种对象访问方式各有优势,使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在 Java 中非常频 繁,因此这类开销积少成多后也是一项非常可观的执行成本。
对 Sun HotSpot 而言,它是使用直接指针访问方式进行对象访问的。