[java] 关键字new是如何实现的

1. 背景信息

在java里面创建一个类的一个实例,或者说创建一个对象的时候,我们只需使用关键字new即可。事实上,new既是一个关键字,它也是虚拟机的一条指令,并且属于对象创建指令。

要弄明白new这个关键字,或者new这个指令是如何实现的。需要结合虚拟机的类加载机制,以及反射技术才能加以说明。

第一个问题,什么时候我们可以new一个类?

这个问题是一个既愚蠢又深奥的问题。答:当然是定义好一个类(class)之后就可以new它了。真的是这样吗?我们new的真的是我们定义的这个class类吗?

我们大多数时候都在编辑器里编写代码,编辑器为我们做的事情容易被我们忽略。我们已经习惯于写好java代码直接运行。让我们来梳理一下这个流程。

程序员写的是java代码,存在对应的.java文件里面,java编译器会将java文件编译成对应的.class字节码文件,JVM虚拟机在获得这个.class文件的字节流之后,经过加载,链接,初始化这几个步骤之后这个class文件就可供使用了。在加载.class文件时,虚拟机会将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,并在内存中生产一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口,为类变量分配内存并设置类变量的初始值,调用<clinit>()方法初始化类,该法发是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生(这一部分的内容属于虚拟机类的加载). 值得一提的是,在加载的所有阶段,只会有静态变量和静态代码块初始化。成员变量并不会初始化。

因此我们在代码里写的关键字new,也在这个时候被虚拟机解析到了,并且会使用虚拟机提供的new指令去创建所指定的对象。会大致经历如下过程:

  1. 检查这个指令的参数是否能在常量池中定位到一个符号引用,并检查这个符号引用代表的类是否已经被虚拟机加载过、解析、和初始化过。如果没有,就必须先执行类的加载过程.
  2. 为新生的对象分配内存
  3. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一操作保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用。
  4. 接下来虚拟机要对对象进行必要的设置,例如对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、GC的对象分代年龄等信息。这新信息储存在对象头中(Object Header)之中
  5. 从虚拟机的角度来看,一个新的对象已经产生,但从Java的角度来看,对象的创建才刚刚开始,方法还没有执行,所有的字段还都为零,因此在执行完new指令之后会接着执行 方法,把对象按照程序员的意愿进行初始化,这样一个真正开用的对象才算创建完成。

除此之外,有以下几点需要注意

1. 如果new对象时有引用变量指向这个对象,栈内存会存放引用变量指向null对象(未初始化时,默认为null); 

很好理解,

ClassName obj = new ClassName();

即为有引用变量指向的情况,这个引用变量就是obj。

假如是:

(new ClassName()).toString();

则在初始化这个对象的时候并没有引用变量指向这个它。
2. 创建对象时,对象的成员(变量)先进行默认初始化;基本类型为基本类型默认值,引用类型为null,即引用变量的引用地址存放在栈(stack)内存中,对象的成员变量及值存放在堆内存中. 注意,是成员变量而非局部变量。

这里可以额外引申一点的就是关于线程安全的问题,在这里我们已经知道了,对象的成员变量会在堆中分配,但是局部变量不会,我们可以得到的的启示是:

  • 局部变量肯定是线程安全的。每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。
  • 而成员变量就要看它初始化的模式。若是单例模式, 实例变量为对象实例私有,在虚拟机的堆中分配,若在系统中只存在一个此对象的实例,在多线程环境下,“犹如”静态变量那样,被某个线程修改后,其他线程对修改均可见,故线程非安全;如果每个线程执行都是在不同的对象中,那对象与对象之间的实例变量的修改将互不影响,故线程安全。
  • 静态变量肯定不是线程安全的。静态变量在内存中的位置既不在堆中,也不在栈上,而是在运行时常量池中,而这个部分是所有线程共享的。

3. 对象成员初始化,对栈内存中的成员变量指定值。  

当创建一个对象的时候,堆里面只会存放这个对象的头信息(这里面会包含有指向这个对象对应的class以及methods的引用)以及它的字段。(来源于stackover flow上的回答:When you create a new object on the heap, you just create space for the header of the object (which points to the class and it's methods) and space for it's fields. (Those the JVM doesn't optimise away)). 

注意,以上过程和局部变量一点关系也没有,关于局部变量,注意以下三点即可:

  • 局部变量必须经过显示初始化之后才能使用,系统不会为局部变量执行初始化。定义了局部变量以后,系统并没有给局部变量进行初始化,直到程序给这个局部变量赋给初值时,系统才会为这个局部变量分配内存空间,并将初始值保存到这块内存中。
  • 局部变量不属于任何类或者实例,因此它总是保存在方法的栈内存中。如果局部变量是基本数据类型,则该变量直接存储在方法的栈内存中,如果是引用变量则将引用的地址存储在方法的栈内存中
  • 栈内存中的变量无需系统垃圾回收,随着方法或者代码块的运行结束而结束。局部变量通常只保存了具体的值或者引用地址,所以所占的内存比较小

而成员变量的生命周期是跟随着一个对象的。

4. 使用构造方法创建对象,每个类都会有一个默认的构造方法,程序员也可以自己定义构造方法,虚拟机会根据构造方法完成初始化。

5.那么当new一个对象,因为某种原因,比如空间不够失败了怎么办呢。关于这个问题可以在CAS的博客中找到答案。主要通过如下办法:

  • CAS加上失败重试保证更新操作的原子性。
  • 为每个线程预先分配一块内存,使用【TLAB】。

猜你喜欢

转载自blog.csdn.net/topdeveloperr/article/details/81194654