• 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏吧

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

error 来源:topEngineerray 7次浏览

目录

概述

什么时候我们可以new一个类

虚拟机处理new指令的流程

一些问题

1. 两种new对象方式的区别

2.变量初始化的默认值

3. 构造方法

4.new一个对象时的内存空间担保?

5. 关于局部变量

6. 静态变量是在何时初始化的?与对象实例化有什么关系?

7.使用Class类提供的newInstance和使用关键字new有哪些不同?

概述

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

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

什么时候我们可以new一个类

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

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

如上图所示,程序员写的是java代码,存在对应的.java文件里面,而java编译器会将java文件编译成对应的.class字节码文件,一个.class文件的示例代码如下:

2806 0000 095f 6964 0091 503d 2958 0100
0010 7479 7065 0000 0000 0003 646f 6300
0706 0000 0973 7461 7274 0091 503d 2958
0100 0003 6275 696c 6449 6e66 6f00 3a04
0000 0973 7461 7274 0091 503d 2958 0100
0002 7665 7273 696f 6e00 0700 0000 332e
322e 3130 0002 6769 7456 6572 7369 6f6e
0029 0000 0037 3964 3962 3361 6235 6365
...

JVM虚拟机在获得这个.class文件的字节流之后,经过加载,链接,初始化这几个步骤之后这个class文件就可供我们使用了

加载.class文件时,虚拟机会将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构,并在内存中生产一个代表这个类的java.lang.Class对象作为方法区这个类的各种数据的访问入口,为类变量分配内存并设置类变量的初始值,调用<clinit>()方法初始化类,该方法是由javac编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生(p.s.这一部分的内容属于虚拟机类的加载).  值得强调的是,在加载的所有阶段,只会有静态变量和静态代码块初始化。成员变量并不会初始化。 此时,在虚拟机当中我们就已经做完了new一个类之前的所有动作。

关于详细的类加载信息可以参考这篇博客:[JVM]虚拟机类加载机制

关于java.lang.Class这个类的理解可以参考这篇博客:关于类与对象和Java里的Class类

虚拟机处理new指令的流程

此时,我们在代码里写下关键字new,并在new之后跟上之前定义好的类:

MyClass myClass = new MyClass();

这个关键字new,也在这个时候被虚拟机解析到了,并且在虚拟机识别到new关键字之后就会使用虚拟机提供的new指令去创建所指定的对象。它会大致经历如下过程:

  1. 校验。检查这个指令的参数是否能在常量池中定位到一个符号引用,并检查这个符号引用代表的类是否已经被虚拟机加载过、解析、和初始化过。如果没有,就必须先执行类的加载过程. 如果有,则根据关键new后紧跟的构造方法去创建一个类的对象出来。 
  2. 分配内存。为新生的对象分配内存。对象所需内存大小在类加载完成后就可以完全确定。为对象分配空间的任务等同于把一块大小确定的java内存从java堆中划分出来。划分方式的详细内容可以参考深入理解java虚拟机一书。除了划分可用空间以外还需要考虑并发状态下分配内存的冲突问题,这个时候有两种解决办法:一是采用CAS配上失败重试,二是预先分配一小块的TLAB内存。
  3. 初始化。内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一操作保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用。
  4. 设置。接下来虚拟机要对对象进行必要的设置,例如对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、GC的对象分代年龄等信息。这新信息储存在对象头中(Object Header)之中
  5. 真正按程序员意愿初始化赋值。在上面的工作都完成后,从虚拟机的角度来看,一个新的对象已经产生,但从Java的角度来看,对象的创建才刚刚开始——<init>方法还没有执行,所有的字段还都为零。因此在执行完new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正开用的对象才算创建完成。所谓程序员的意愿,就是我们new对象的时候指定的构造方法,如上面的默认构造方法:
    MyClass myClass = new MyClass();

同时,在这个步骤我们必须要注意到的一点就是,实例变量在这一步已经初始化了,因此我们直接使用编译不会出错。而局部变量我们在编程时就会发现,只是定义一个变量不进行初始化是会报编译错误的,原因是,局部变量必须经过显示初始化之后才能使用,系统不会为局部变量执行初始化。定义了局部变量以后,系统并没有给局部变量进行初始化,直到程序给这个局部变量赋给初值时,系统才会为这个局部变量分配内存空间,并将初始值保存到这块内存中。

一些问题

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

1. 两种new对象方式的区别

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

很好理解,假如我们按照如下方式new一个对象

ClassName obj = new ClassName();

这种new一个对象的方式即为有引用变量指向的情况,这里的引用变量就是这个obj

假如是下面这种方式来new一个对象的话

(new ClassName()).toString();

则我们在初始化这个对象的时候并没有指定任何一个引用变量指向这个对象。

2.变量初始化的默认值

创建对象时,对象的成员变量先进行默认初始化,其中:

  • 基本类型初始化为基本类型默认值,如: int 类型的默认值为0,boolean类型的默认值为false;
  • 对象引用类型默认初始化为null,如: Object obj = new xxx();

引用变量的引用地址obj存放在栈(stack)内存中,对象的成员变量及值存放在堆内存中。

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

  • 局部变量肯定是线程安全的。每个线程执行时将会把局部变量放在各自栈帧的工作内存中,线程间不共享,故不存在线程安全问题。在方法中声明的变量可以是基本类型的变量,也可以是引用类型的变量:
  1. 当声明是基本类型的变量的时,其变量名及值(变量名及值是两个概念)是放在方法栈中。

  2. 当声明的是引用变量时,所声明的变量(该变量实际上是在方法中存储的是内存地址值)是放在方法的栈中,该变量所指向的对象是放在堆类存中的。

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

当创建一个对象的时候,堆里面只会存放这个对象的头信息(这里面会包含有指向这个对象对应的class以及methods的引用)以及它的字段。 

3. 构造方法

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

4.new一个对象时的内存空间担保?

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

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

5. 关于局部变量

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

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

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

6. 静态变量是在何时初始化的?与对象实例化有什么关系?

静态变量,属于类,不属于任何一个实例,也不属于任何一个方法,类变量存储的区域是在方法区method area当中。

静态变量初始化的时间在初始化阶段,而常量则在链接阶段就会被初始化。

关于这部分的详细内容可以参考博客:[JVM]虚拟机类加载机制

7.使用Class类提供的newInstance和使用关键字new有哪些不同?

首先,从虚拟机的指令角度来说,这两种方式所使用的指令是不一样的。

下面是使用newInstance方法的代码

package basic;

public class TestNewInstance {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {

        TestNewInstance newInstance = TestNewInstance.class.newInstance();
    
    }
}

它的字节码如下:

Compiled from "TestNewInstance.java"
public class basic.TestNewInstance {
  public basic.TestNewInstance();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.IllegalAccessException, java.lang.InstantiationException, java.lang.ClassNotFoundException;
    Code:
       0: ldc           #2                  // class basic/TestNewInstance
       2: invokevirtual #3                  // Method java/lang/Class.newInstance:()Ljava/lang/Object;
       5: checkcast     #2                  // class basic/TestNewInstance
       8: astore_1
       9: return
}

可以看到使用的虚拟机指令是invokevirtual指令。

现在加上使用关键字new的java代码:

package basic;

public class TestNewInstance {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {

        TestNewInstance newInstance = TestNewInstance.class.newInstance();
        
        TestNewInstance test = new TestNewInstance();
    }
}

上面的代码对应的字节码如下:

Compiled from "TestNewInstance.java"
public class basic.TestNewInstance {
  public basic.TestNewInstance();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]) throws java.lang.IllegalAccessException, java.lang.InstantiationException, java.lang.ClassNotFoundException;
    Code:
       0: ldc           #2                  // class basic/TestNewInstance
       2: invokevirtual #3                  // Method java/lang/Class.newInstance:()Ljava/lang/Object;
       5: checkcast     #2                  // class basic/TestNewInstance
       8: astore_1
       9: new           #2                  // class basic/TestNewInstance
      12: dup
      13: invokespecial #4                  // Method "<init>":()V
      16: astore_2
      17: return
}
  

我们可以看到,新加入的使用关键字new的代码,使用的是虚拟机的new指令。

除此之外,从虚拟机做的事情上来讲,这两种创建对象的方式还主要有下面两点不同:

一是使用关键字new进行实例化时,虚拟机会根据new后面所紧接着的我们指定的构造方法进行实例化,而Class类提供的newInstance方法则默认使用参数为空的构造方法进行实例化。

二是在使用new关键字进行实例化时我们的实例化的类可以是没有被加载的,所谓未加载就是指虚拟机当中还没有根据我们写的类的代码去获取字节流并创建一个对应的Class对象出来。 而使用newInstance方法则必须先对类进行加载。 因此回到这个问题上来说,在jvm底层的计算机指令两者是不完全一样的,但是newInstance方法进行实例化和new进行实例化的逻辑肯定是同一块代码执行的。但是这两个方法执行时虚拟机所做的事情不完全一样。 

 

最后,回过头来看new关键字是如何实现的呢?直接的回答就是通过new指令实现的。至于更详细的解释,则需要结合上面的内容进行说明了。


版权声明:本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系管理员进行删除。
喜欢 (0)