· 本文摘要
· 罗列Java创建对象的各种方式;
· 讲解Java对象创建的流程步骤;
一、Java创建对象的各种方式
· 1. 用关键字new
,老少皆知的方法:StringBuffer sb = new StringBuffer();
· 2. 在单例模式(一种设计模式)中创建对象,本质上也是用关键字new
,见下面的代码示例:
/** * 单例模式 * 饿汉式(静态常量) */ public class BaseConfig { private final static BaseConfig INSTANCE = new BaseConfig(); private BaseConfig () {}//私有构造子 public static BaseConfig getInstance() {//在这里返回对象,其实也用了new return INSTANCE; } }
· 3. 在工厂模式Factory
、建造者模式Builder
(也是设计模式)中创建对象,本质上也是用关键字new
。
· 4. 反射机制,利用Class.newInstance()
。值得注意的是,Class.newInstance()
创建对象时的特点有:弱类型,低效率,只能调用无参构造。这里还需要区别Class.forName()
,Class.forName()
返回的是一个类,并不是对象。
· 5. 利用Constructor.newInstance()
。Constructor.newInstance()
可以调用任意构造函数。
· 6. 使用克隆clone()
,使用前保证对象实现了cloneable()
。
· 7. 反序列化。开发中经常会对类实现序列化接口,反序列化支持我们把二进制数据、网络数据转化为Java对象保存在内存中。
· 8. 使用第三方库Objenesis
。
二、Java创建对象的步骤
· 第一步:检测类是否已经加载。
当JVM即将创建对象前,先去检查常量池中是否有此类的符号引用,并且检查此类是否已加载、链接、初始化。如果没有,需要类加载器来加载此类,参考:类加载器基础知识。
· 第二步:为对象分配内存。
类加载完成以后,虚拟机就开始为对象分配内存,此时所需内存的大小就已经确定了。只需要在堆上分配所需要的内存即可。
具体的分配内存有两种情况:第一种情况是内存空间绝对规整,第二种情况是内存空间是不连续的。
· 指针碰撞:假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。
· 空闲列表:如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记 录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例, 并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。
· 第三步:处理并发安全问题。
由于对象分配的内存是放在堆中的,堆作为线程共享的数据区,当线程数大于1时,会涉及到线程安全问题。需要通过一定的方式来处理并发安全问题。
第一种是采用同步的办法,为对象空间加锁,使用CAS来保证操作的原子性。
另一种是每个线程分配内存都在自己的空间内进行,即是每个线程都在堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),分配内存的时候再TLAB上分配,互不干扰。
· 第四步:初始化分配的空间。
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作解释了对象的实例字段在Java代码中为什么可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
· 第五步:设置对象头。
分配完内存空间,初始化零值之后,虚拟机还需要对对象进行其他必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的hashcode,GC分代年龄等信息。
在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了。但从Java程 序的视角来看,对象创建才刚刚开始<init>方法还没有执行,所有的字段都还为零。
· 第六步:执行init方法(执行构造器)。