JVM[1]-对象创建-基础
过程
检查类加载
检查虚拟机是否加载了所要 new 的类,若没加载,则首先执行相应的类加载过程。虚拟机遇到 new 指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个引用代表的类是否已经被加载、解析和初始化过。
内存分配
在类加载检查通过后,对象所需内存的大小在类加载完成后便可完全确定,虚拟机就会为新生对象分配内存。一般来说,根据 Java 堆中内存是否绝对规整,内存的分配有两种方式:
- 指针碰撞:如果 Java 堆中内存绝对规整,所有用过的内存放在一边,空闲内存放在另一边,中间一个指针作为分界点的指示器,那分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相同的距离。
- 空闲列表:如果 Java 堆中内存并不规整,那么虚拟机就需要维护一个列表,记录哪些内存块是可用的,以便在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
除了如何划分可用空间之外,还需要考虑修改指针 (该指针用于划分内存使用空间和空闲空间)时的线程安全问题,因为存在可能出现正在给对象 A 分配内存,指针还未修改,对象 B 又同时使用原来的指针分配内存的情况。解决这个问题有两种方案:
- 对分配内存空间的动作进行同步处理:采用 CAS+失败重试的方式保证更新操作的原子性;
- 把内存分配的动作按照线程划分的不同的空间中:每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),哪个线程要分配内存,就在自己的 TLAB 上分配,如果 TLAB 用完并分配新的 TLAB 时,再加同步锁定。
内存空间初始化
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值。如果使用 TLAB,也可以提前到 TLAB 分配时进行。这一步操作保证了对象的实例字段在 Java 代码中可以不赋初值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
<init>
方法执行
在上面的工作完成之后,从虚拟机的角度来看,一个新的对象已经产生了,但从 Java 程序的视角来看,对象的创建才刚刚开始,此时会执行<init>
方法把对象按照程序员的意愿进行初始化,从而产生一个真正可用的对象。
访问定位
创建对象是为了使用对象,我们的 Java 程序通过栈上的 reference 数据来操作堆上的具体对象。在虚拟机规范中,reference 类型中只规定了一个指向对象的引用,并没有定义这个引用使用什么方式去定位、访问堆中的对象的具体位置。目前的主流的访问方式有使用句柄访问和直接指针访问两种。
- 句柄访问:Java 堆中会划分出一块内存作为句柄池,栈中的 reference 指向对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息。
- 直接指针访问:reference 中存储的就是对象地址。
总的来说,这两种对象访问定位方式各有千秋。
使用句柄访问的最大好处就是 reference 中存储的是稳定的句柄地址,对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,reference 本身不需要修改;
而使用直接指针访问的最大好处就是速度快,节省了一次指针定位的时间开销。