在java中使用new创建对象时,虚拟机创建的过程:
以Object obj = new MyObject();为例(其他创建实例方式也一样)
一、分配内存
1、虚拟机会检查MyObject这个类是否存在,有没有被加载。如果MyObject没有被加载过,那么就先加载这个类;
2、虚拟机根据MyObject类的类信息在堆中分配内存空间,分配内存有两种方式:
(1)指针碰撞:当内存是规整的,此时有一个指针,在该指针一侧的内存是已经被分配了,而另一侧的内存则是空闲的,当创建对象要分配内存时,指针向空闲内存的一侧移动与要创建的对象大小相等的距离,这段距离之间的内存就用于存放要创建的对象。
(2)空闲列表:内存很少会是规整的,大多都是东一块西一块的的破碎内存,此时使用一个列表把空闲的内存记录下来,当要创建对象时,找出最合适的一块空闲内存用于存储这个要创建的对象。
以上两种方法在单线程中是没有问题的,但是,当处于多线程的时候,“指针碰撞”方式,在一个线程中创建第一个对象,读取了指针的地址,正要把指针向空闲内存一侧移动时,第二个线程也要创建第二个对象,此时指针还没有被修改,所以在第二个线程中读取的指针还是原来的地址,然后分配内存也是从那个地址把指针向空闲的一侧移动等于第二个对象大小的距离。。。这个时候,第一个线程创建的第一个对象,与第二个线程创建的第二个对象 会相互覆盖,数据就会出错。
而解决方法有两个:
(1)创建对象的时候进行同步操作,这样就不会出现多个线程读取到的指针是相同的情况了。。但是,同步会导致效率低下。
(2)使用TLAB本地线程分配缓冲,也就是每个线程在创建时都先分配一块堆内存用作缓冲,在哪个线程创建的对象都在这个线程所属的TLAB中创建对象。
3、分配完对象的内存空间后,把这块内存的值置为零置,这样的话,对象中没有被初始化的实例字段就存在初始值了,对象变量字段初始值为null,基本类型为对应的零值。。
4、初始化对象头数据,
对象头数据分为: 对象的运行时数据和类型指针;
对象运行时数据:GC年龄分代、锁状态标志、线程持有的锁、hashcode哈希码等。
类型指针:指向方法区中类型信息的指针。类型信息就是编译后的代码以及其他关于这个类的数据信息。
二、初始化对象
1、调用构造方法进行初始化。
三、赋值
1、给变量赋值,新对象的引用赋给变量。
注意:
其中二、三两个步骤可能会因为jvm的优化导致指令重排序,正常步骤是一、二、三,jvm优化后可能就变成一、三、二。
指令重排序在单线程的时候是很好的优化,但是在多线程的时候可能就会出现问题了。
例子:
public class Singleton {
private static volatile Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
synchronized(Singleton.class){
if(instance==null){
instance = new Singleton();
}
}
}
return instance;
}
}
在双重检验的单例模式中,多线程调用静态方法获取单例,线程A执行完一和三后,单例字段就不会是null了,最后执行创建对象的第三个步骤---即调用构造方法初始化对象,如果构造方法内执行的时间比较长,此时线程B也调用该单例执行静态方法,遇到第一个if (instance == null)时,instance == null的值会返回false,这样的话,就不会执行if内的同步代码块,如果线程A创建对象还没把构造方法执行完,线程B调用该静态方法就已经获取单例对象成功了,但是,此时该单例对象虽然不为null,但是却还没有初始化成功,这样的话,单例就有问题。
所以,为了解决这个问题,就要给单例字段加volatile修饰,防止jvm进行指令重排序。另外,synchronized只保证释放锁之前把对共享变量的修改刷到主内存,但在执行代码期间的共享变量的可见性还是无法保证的。。所以加volatile也是保证了 instance = new Singleton();能够被其他线程立即可见,