目录
3、假如一个类还未加载到内存中,那么创建一个该类的实例,具体的过程
问题:
- JVM什么时候才会加载class文件并初始化(类加载和初始化时机)
- JVM如何加载一个class文件(类加载方式:类加载器,双亲委派)
- JVM加载一个class文件所需要经历的具体步骤(类加载流程)
- 对象创建过程(类初始化和实例化详解)
类加载和初始化时机:
什么情况下虚拟机需要开始初始化一个类呢?这在虚拟机规范中是有严格规定的,虚拟机规范指明 有且只有 五种情况必须立即对类进行初始化(而这一过程自然发生在加载、验证、准备之后)
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的常见的java代码场景有:
- 使用new关键字实例化对象的时候;
- 读取或者设置一个类的静态字段的时候,但是final修饰的除外;
- 调用一个类的静态方法时候。
注意:newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类[Ljava.lang.String的初始化,而直接不会触发String类的初始化。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
- 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化。
这五种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式,都不会触发初始化,称为被动引用。
被动引用几种经典场景:例子可参考代码
- 通过子类引用父类的静态字段,不会导致子类初始化。
- 通过数组定义来引用类,不会触发此类的初始化。
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
注意:类实例化和类初始化时两个完全不同的概念:
类实例化是指创建一个类的实例的过程。
类初始化是指为类中各个类成员(被static修饰的成员变量)赋初始值的过程,是类生命周期的一个阶段。
类加载流程:
每个编写的.java拓展名类文件都存储着需要执行的程序逻辑,这些.java文件经过Java编译器编译成拓展名为.class的文件,.class文件中保存着Java代码经转换后的虚拟机指令,当需要使用某个类时,虚拟机将会加载它的.class文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载。
图说明:
- 类从被加载到内存中开始,到卸载出内存为止,整个生命周期除了上面五个外,还有使用和卸载两个过程,这是类整个的生命周期,只不过前面五个是类加载的过程。
- 加载、验证、准备、初始化和卸载这五个阶段顺序是确定的,但是解析这个步骤未必如此,某些情况下可以在初始化阶段之后再开始,支持了JAVA语言的运行绑定。
- 类的加载过程必须按照这种顺序按部就班地“开始”,而不是按部就班的“进行”或“完成”,因为这些阶段通常都是相互交叉地混合式进行的,也就是说通常会在一个阶段执行的过程中调用或激活另外一个阶段。
第一步:加载
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 静态存储结构转换为方法区的运行时数据结构
- 在java堆对象里(还是在方法区里?)生成该类的class对象,作为方法区该类的各种数据的访问入口
注意:获取类的二进制流是由类加载器来完成的
第二步:验证
目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全
- 验证class文件的标识,魔数
- 验证class文件的版本号
- 验证常量池(常量类型、常量类型数据结构是否正确)
- class文件其他信息(字段表,方法表等)
- 元数据验证(父类验证、继承验证、final验证)
- 字节码验证(指令验证,是否是虚拟机字节码表的指令)
- 符号引用验证(通过符号引用是否能找到字段、方法、类等)
注意:如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间
第三步:准备
为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这些变量所使用的内存都将在方法区中分配。这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
static int n = 2;
初始化值为0,而不是2,因为这个时候没有执行任何java方法(<clinit>存在于类构造器方法中)
static final int n = 2;
对应到常量池constantValue,在准备阶段n必须赋值为2
类变量:一般指的是静态变量。
实例变量:当对象被实例化的时候,实例变量就跟着确定了,随着对象的销毁而销毁。
第四步:解析
对符号引用进行解析。
符号引用:
直接引用:指向目标的指针或者偏移量。
符号引用---> 直接引用
虚拟机将常量池内的符号引用替换为直接引用的过程
第五步:初始化
类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
该阶段直接的说就是:初始化阶段就是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中所有类变量(static修饰的)的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件的出现的顺序所决定的。静态语句块只能访问到定义在它之前的变量,定义在它之后的变量,在静态语句块中可以赋值,但是不能访问。例如:
- public class Test{
- static{
- i=0;
- System.out.println(i);//Error:Cannot reference a field before it is defined(非法向前应用)
- }
- static int i=1;
- }
那么注释报错的那行代码,改成下面情形,程序就可以编译通过并可以正常运行了。
- public class Test{
- static{
- i=0;
- //System.out.println(i);
- }
- static int i=1;
- public static void main(String args[]){
- System.out.println(i);
- }
- }
- /* Output:
- 1
- */
类构造器<clinit>()与实例构造器<init>()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器<clinit>()执行之前,父类的类构造<clinit>()执行完毕。由于父类的构造器<clinit>()先执行,也就意味着父类中定义的静态语句块/静态变量的初始化要优先于子类的静态语句块/静态变量的初始化执行。特别地,类构造器<clinit>()对于类或者接口来说并不是必需的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器<clinit>()。
虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。特别需要注意的是,在这种情形下,其他线程虽然会被阻塞,但如果执行<clinit>()方法的那条线程退出后,其他线程在唤醒之后不会再次进入/执行<clinit>()方法,因为 在同一个类加载器下,一个类型只会被初始化一次。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个线程阻塞,在实际应用中这种阻塞往往是隐藏的。例如:
- public class DealLoopTest {
- static{
- System.out.println("DealLoopTest...");
- }
- static class DeadLoopClass {
- static {
- if (true) {
- System.out.println(Thread.currentThread()
- + "init DeadLoopClass");
- while (true) { // 模拟耗时很长的操作
- }
- }
- }
- }
- public static void main(String[] args) {
- Runnable script = new Runnable() { // 匿名内部类
- public void run() {
- System.out.println(Thread.currentThread() + " start");
- DeadLoopClass dlc = new DeadLoopClass();
- System.out.println(Thread.currentThread() + " run over");
- }
- };
- Thread thread1 = new Thread(script);
- Thread thread2 = new Thread(script);
- thread1.start();
- thread2.start();
- }
- }
- /* Output:
- DealLoopTest...
- Thread[Thread-1,5,main] start
- Thread[Thread-0,5,main] start
- Thread[Thread-1,5,main]init DeadLoopClass
- */
这里说的主要是类初始化,类初始化往往伴随着类的实例化
对象创建过程(类初始化和实例化详解):
在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。一个对象的创建过程往往包括了类初始化和类实例化两个阶段。
一、对象创建的时机
- 使用new关键字创建对象
- 使用Class类的newInstance()方法(反射机制)
事实上这个newInstance()方法调用的无参的构造器创建对象
Student student2 = (Student)Class.forName("Student类全限定名").newInstance();
或者:
Student stu = Student.class.newInstance()
- 使用Constructor类的newInstance方法(反射机制)
可以通过这个newInstance方法调用有参数的和私有的构造函数事实上Class的newInstance方法内部调用的也是Constructor的newInstance方法
- 使用clone方法创建对象
- 使用(反)序列化机制创建对象
注意:从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的。
完整实例:
- public class Student implements Cloneable, Serializable {
- private int id;
- public Student() {
- }
- public Student(Integer id) {
- this.id = id;
- }
- @Override
- protected Object clone() throws CloneNotSupportedException {
- // TODO Auto-generated method stub
- return super.clone();
- }
- @Override
- public String toString() {
- return "Student [id=" + id + "]";
- }
- public static void main(String[] args) throws Exception {
- System.out.println("使用new关键字创建对象:");
- Student stu1 = new Student(123);
- System.out.println(stu1);
- System.out.println("\n---------------------------\n");
- System.out.println("使用Class类的newInstance方法创建对象:");
- Student stu2 = Student.class.newInstance(); //对应类必须具有无参构造方法,且只有这一种创建方式
- System.out.println(stu2);
- System.out.println("\n---------------------------\n");
- System.out.println("使用Constructor类的newInstance方法创建对象:");
- Constructor<Student> constructor = Student.class
- .getConstructor(Integer.class); // 调用有参构造方法
- Student stu3 = constructor.newInstance(123);
- System.out.println(stu3);
- System.out.println("\n---------------------------\n");
- System.out.println("使用Clone方法创建对象:");
- Student stu4 = (Student) stu3.clone();
- System.out.println(stu4);
- System.out.println("\n---------------------------\n");
- System.out.println("使用(反)序列化机制创建对象:");
- // 写对象
- ObjectOutputStream output = new ObjectOutputStream(
- new FileOutputStream("student.bin"));
- output.writeObject(stu4);
- output.close();
- // 读取对象
- ObjectInputStream input = new ObjectInputStream(new FileInputStream(
- "student.bin"));
- Student stu5 = (Student) input.readObject();
- System.out.println(stu5);
- }
- }
- /* Output:
- 使用new关键字创建对象:
- Student [id=123]
- ---------------------------
- 使用Class类的newInstance方法创建对象:
- Student [id=0]
- ---------------------------
- 使用Constructor类的newInstance方法创建对象:
- Student [id=123]
- ---------------------------
- 使用Clone方法创建对象:
- Student [id=123]
- ---------------------------
- 使用(反)序列化机制创建对象:
- Student [id=123]
- */
二、对象创建的过程
当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化。
1、实例变量初始化与实例代码块初始化
我们在定义(声明)实例变量的同时,还可以直接对实例变量进行赋值或者使用实例代码块对其进行赋值。如果我们以这两种方式为实例变量进行初始化,那么它们将在构造函数执行之前完成这些初始化操作。实际上,如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后(还记得吗?Java要求构造函数的第一条语句必须是超类构造函数的调用语句),构造函数本身的代码之前。(这个顺序可以从class文件中分析看出来)例如:
- public class InstanceVariableInitializerFirst {
- private int i = 1;
- private int j = i + 1;
- public InstanceVariableInitializerFirst(int var){
- System.out.println(i);
- System.out.println(j);
- this.i = var;
- System.out.println(i);
- System.out.println(j);
- }
- { // 实例代码块
- j += 3;
- }
- public static void main(String[] args) {
- new InstanceVariableInitializerFirst(8);
- }
- }
上面的例子正好印证了上面的结论。特别需要注意的是,Java是按照编程顺序来执行实例变量初始化器和实例初始化器中的代码的,并且不允许顺序靠前的实例代码块初始化在其后面定义的实例变量,比如:
- public class InstanceInitializer {
- {
- j = i;
- }
- private int i = 1;
- private int j;
- }
- public class InstanceInitializer {
- private int j = i;
- private int i = 1;
- }
上面的这些代码都是无法通过编译的,编译器会抱怨说我们使用了一个未经定义的变量。之所以要这么做是为了保证一个变量在被使用之前已经被正确地初始化。但是我们仍然有办法绕过这种检查,比如:
- public class InstanceInitializer {
- private int j = getI();
- private int i = 1;
- public InstanceInitializer() {
- i = 2;
- }
- private int getI() {
- return i;
- }
- public static void main(String[] args) {
- InstanceInitializer ii = new InstanceInitializer();
- System.out.println(ii.j);
- }
- }
如果我们执行上面这段代码,那么会发现打印的结果是0。因此我们可以确信,变量j被赋予了i的默认值0,这一动作发生在实例变量i初始化之前和构造函数调用之前。
2、构造函数初始化
每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。如果显式的定义了一个带有参数的构造函数,那么编译成的class文件是不生成默认无参的构造函数。
Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。事实上,这一点是在构造函数中保证的:Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用。例如:
- public class ConstructorExample {
- }
对于上面代码中定义的类,我们观察编译之后的字节码,我们会发现编译器为我们生成一个构造函数,如下:
- aload_0
- invokespecial #8; //Method java/lang/Object."<init>":()V
- return
上面代码的第二行就是调用Object类的默认构造函数的指令。也就是说,如果我们显式调用超类的构造函数,那么该调用必须放在构造函数所有代码的最前面,也就是必须是构造函数的第一条指令。正因为如此,Java才可以使得一个对象在初始化之前其所有的超类都被初始化完成,并保证创建一个完整的对象出来。
特别地,如果我们在一个构造函数中调用另外一个构造函数,如下所示:
- public class ConstructorExample {
- private int i;
- ConstructorExample() {
- this(1);
- ....
- }
- ConstructorExample(int i) {
- ....
- this.i = i;
- ....
- }
- }
对于这种情况,Java只允许在ConstructorExample(int i)内调用超类的构造函数,也就是说,下面两种情形的代码编译是无法通过的:
- public class ConstructorExample {
- private int i;
- ConstructorExample() {
- super();
- this(1); // Error:Constructor call must be the first statement in a constructor
- ....
- }
- ConstructorExample(int i) {
- ....
- this.i = i;
- ....
- }
- }
或者:
- public class ConstructorExample {
- private int i;
- ConstructorExample() {
- this(1);
- super(); //Error: Constructor call must be the first statement in a constructor
- ....
- }
- ConstructorExample(int i) {
- this.i = i;
- }
- }
3、小结
总而言之,实例化一个类的对象过程就是一个典型递归的过程。具体的过程:
在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。此时,首先实例化Object类,再依次对以下各类进行实例化,直到完成对目标类的实例化。具体而言,在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。
三、总结:
1、一个实例变量在对象初始化过程中会被赋值几次
JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。如果我们在构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次。
总的来说就是:1、初始分配的时候。2、声明实例变量赋值。3、实例代码块赋值。4、构造函数赋值。
2、类初始化过程和类实例化过程不同
两个不同说白了指的就是<clinit>()和<init>()的不同
类的初始化是指类加载过程中的初始化阶段对类变量按照程序猿的意图进行赋值的过程;
类的实例化是指在类完全加载到内存中后创建对象的过程。
3、假如一个类还未加载到内存中,那么创建一个该类的实例,具体的过程
创建一个类的实例,必须先将类加载到内存并且进行初始化,也就是说,类初始化操作是在类实例化操作之前进行的,但是并不能等同于,只有类初始化操作结束后才能进行类的实例化。例子可见两个经典例子中的第一个。
总体来说类实例化的过程是:
父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数
这里还有一个是成员变量默认赋值。。这个是处于哪个阶段呢。。。。。?
两个经典的例子:
一、类初始化时候进行类实例化
- public class StaticTest {
- public static void main(String[] args) {
- staticFunction();
- }
- static StaticTest st = new StaticTest();
- static { //静态代码块
- System.out.println("1");
- }
- { // 实例代码块
- System.out.println("2");
- }
- StaticTest() { // 实例构造器
- System.out.println("3");
- System.out.println("a=" + a + ",b=" + b);
- }
- public static void staticFunction() { // 静态方法
- System.out.println("4");
- }
- int a = 110; // 实例变量
- static int b = 112; // 静态变量
- }
- /* Output:
- 2
- 3
- a=110,b=0
- 1
- 4
- */
首先明白两点:
- 在同一个类加载器下,一个类只会被初始化一次,所以一旦初始化一个类,无论是否完成,后续都不会再重新触发该类的初始化阶段。
- 实例初始化可以完全发生在静态初始化之前。实际上就是把实例初始化嵌入到静态初始化流程中。
分析过程:
- 该类初始化的时机就是主函数调用了静态方法。
- 初始化发生了,那么肯定是类加载了,类加载过程中有一个准备阶段,在改阶段里会对类变量赋初始值,所以有st变量赋值为null,b为0。
- 到了初始化阶段,就要按源码排列的顺序开始执行<clinit>()方法。
- 执行new StaticTest(),也就是触发了对象创建(这里没有父类不考虑父类了)
- 对实例变量和代码块进行初始化,a = 0;
- 然后就是按源码排序顺序执行<init>()方法
- 执行System.out.println("2"); a = 110;
- System.out.println("3"); System.out.println("a=" + a + ",b=" + b);
- 实例化完毕后,继续执行后续的static
- System.out.println("1"); b = 112;
- 再执行staticFunction()方法,System.out.println("4");
二、构造函数
- class Foo {
- int i = 1;
- Foo() {
- System.out.println(i);
- int x = getValue();
- System.out.println(x);
- }
- {
- i = 2;
- }
- protected int getValue() {
- return i;
- }
- }
- //子类
- class Bar extends Foo {
- int j = 1;
- Bar() {
- j = 2;
- }
- {
- j = 3;
- }
- @Override
- protected int getValue() {
- return j;
- }
- }
- public class ConstructorExample {
- public static void main(String... args) {
- Bar bar = new Bar();
- System.out.println(bar.getValue());
- }
- }
其实构造函数可以转换为:
- //Foo类构造函数的等价变换:
- Foo() {
- i = 1;
- i = 2;
- System.out.println(i);
- int x = getValue();
- System.out.println(x);
- }
- //Bar类构造函数的等价变换
- Bar() {
- Foo();
- j = 1;
- j = 3;
- j = 2
- }
第(2)处输出是0,为什么呢?因为在执行Foo的构造函数的过程中,由于Bar重载了Foo中的getValue方法,所以根据Java的多态特性可以知道,其调用的getValue方法是被Bar重载的那个getValue方法。但由于这时Bar的构造函数还没有被执行,因此此时j的值还是默认值0,因此(2)处输出是0
还有一个问题就是,创造了这个Bar类实例后是否有Foo类实例被创建,如果被创建了,Object类实例应该也被创建。。。多类的时候都要创建Object类实例么?
不过可以想象,应该不会去创建父类对象的,
第一、如果类有多个子类,每个子类去创建,都要创建父类对象,岂不是多余,Object也不会应该存在多个。。
第二、如果子类继承的一个抽象类,如果子类创建对象了,父类是抽象类,难道会创建对象?抽象类可是不能创建对象的。
第三、通过跑代码可以看到创建的当前对象是一个对象。。
具体解释:
1、创建子类对象时,并没有创建父类对象,只是子类对象中有一个父类的亚对象。
2、子类继承了父类的所有属性和方法,但是有不能直接使用父类中的private属性和方法。我可以拥有,但是我使用不了。
文档参考:
深入理解Java对象的创建过程:类的初始化与实例化_实例化和初始化_书呆子Rico的博客-优快云博客
JVM类生命周期概述:加载时机与加载过程_jvm 类加载子系统:加载时机、加载过程_书呆子Rico的博客-优快云博客
工具: