Java 对象创建过程

2,376 阅读6分钟

字节码分析创建过程

简单实现一个创建对象的代码

public class ObjectTest {

    public static void main(String[] args) {
        VM vm = new VM();
    }

}

从字节码分析对象创建的过程

0 new #2 <com/alaske/test/VM>
3 dup
4 invokespecial #3 <com/alaske/test/VM.<init> : ()V>
7 astore_1
8 return

new

new指令相当于去对应的内存空间申请一块内存大小 用于存放对象数据 对象的大小是可以确定的 比如int占用4个字节 引用也占用4个字节 所以这个时候申请的内存空间大小是固定的

dup

dup相当于将对应的内存地址的引用复制了一份压到栈中,那么对应栈中会有2个对象的引用,这2个对象的引用一个用于操作对象赋值,一个用于对象的方法调用

invokespecial

invokespecial指令 是调用对象的构造器初始化对象 我这里使用的是默认构造器 空参构造 一般用于初始化对象数据,在new的时候JVM会给对象的全局变量赋默认值

astore

astore指令就是将对应对象的引用存储到局部变量表中

image.png

JVM分析创建过程

Class加载

在创建一个对象的时候,会先判断对象以及父类接口等的Class文件是否被加载 具体加载流程可以查看我之前写的Class加载流程,如果没有被加载 则采用双亲委派机制 加载对应的Class以及Class关联的其他Class到方法区中,关于方法区中的数据结构可以查看JVM运行时数据区

内存分配

在字节码角度我们知道了new指令是申请内存空间,那么JVM又是如何分配内存空间的呢?

分两种分配方式,一种为内存空间如果是连续的 比如采用的复制和标记整理算法的方式 那么内存空间是连续的,在内存空间为连续的时候分配方式叫指针碰撞的方式分配内存 ,如果内存空间不是连续的比如采用的标记清除算法 那么JVM会维护一个空闲列表 FreeList记录可用的内存空间,之前也有介绍过垃圾回收的几种算法

那么在分配内存的时候还需要考虑到线程安全问题比如多个线程同时new不同的对象,堆的存储空间也是线程共享的,同样在分配空间的时候会出现线程安全问题,Hotspot中的做法是采用2中方式保证分配空间的安全性,第一种是采用TLAB的方式 给每一个线程分配1%的空间在eden区做为内存分配的首选区域,如果没有采用TLAB的方式则会采用CAS重试的方式确保线程安全问题,在之前我有贴出过源码说明这一块TLAB

指针碰撞

上面说到内存连续的时候是会使用指针碰撞的方式分配内存空间 image.png

将指针移动到上一次申请内存地址的尾部区域,在下一次申请内存空间的时候,指针向空闲内存区域移动对象大小

空闲列表

image.png

当内存不是连续的,比如我们采用标记清除的算法做为内存管理的方式,那么内存空间就不是连续空间 会产生内存碎片 比如我之前说过的CMS垃圾回收器采用的就是标记清除方式 ,这个时候肯定就无法采用指针碰撞的方式去开辟空间,所以会维护一个列表存储空闲的空间,在分配的时候去查找合适大小的内存空间

默认初始化

public class VM  {

    private int value = 100;
    private Object data;

}

比如我们对象中 定义了valuedata

JVM为了实现我们不做赋值操作的对象也可以拿来直接使用在申请空间的会给对象赋值为null基本数据类型会赋值为默认值,引用数据为赋值为null

比如上面代码value会赋值为0 ,data会赋值为null ,这里是赋为默认值,不是赋值

初始化 init

接着默认初始化的代码查看,在执行到对象的<init>构造器函数时,我们可以看下字节码

 0 aload_0
 1 invokespecial #1 <java/lang/Object.<init> : ()V>
 4 aload_0
 5 bipush 100
 7 putfield #2 <com/alaske/test/VM.value : I>
 10 return

aload_0从局部变量表中拿出索引为0的,在非静态方法中索引为0的就是我们this当前对象,bipushputfield,一个将100 push到我们的操作数栈中,putfield赋值字段VM.value

设置对象头

对象头包含三个部分,这里涉及到对象的内存布局JOL(Java Object Layout),我这里使用JOL一个引用打印对象头,我使用的Gradle

compile "org.openjdk.jol:jol-core:0.9"
com.alaske.test.VM object internals:
OFFSET  SIZE               TYPE DESCRIPTION                               VALUE
     0     4                    (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
     4     4                    (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     8     4                    (object header)                           44 c0 00 f8 (01000100 11000000 00000000 11111000) (-134168508)
    12     4                int VM.value                                  100
    16     4   java.lang.Object VM.data                                   null
    20     4                    (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

image.png

关于对象头存储的数据以及作用我后面单独一篇文章去描述

对象访问方式

句柄池

在堆区域 划分了一个区域 存储对象的句柄叫句柄池,句柄池中持有引用对象实体数据 和 对象类型数据(方法区中) 两个引用指正

image.png

缺点:

显然而见 需要单独开辟一块空间记录句柄池存储,并且访问的时候需要中转 访问效率偏低一点

优点:

当对象数据变更的时候比如垃圾回收器 需要整理对象 需要移动复制,Stack栈空间中的引用 是不需要修改的 只需要修改句柄池中的引用

直接引用

Hotspot采用的是直接引用方式,具体方式就是通过上面写的对象头中Klass Pointer 指针访问方法区中的类型数据

image.png

缺点:

当对象数据变更引用的时候 需要修改Stack的指针

优点:

不需要额外的开辟一块空间存储引用,在原有对象头中添加指针指向即可,访问对象实体数据的时候效率高