JVM--JVM类加载机制(下)

在深入理解JVM–JVM类加载机制(上)中,只写到Java类加载机制的加载阶段,在类的生命周期中,还有后面的验证、准备、解析、初始化、使用和卸载这些阶段。

验证

验证阶段的主要工作是为了确保Class文件流中包含的内容符合虚拟机的要求,而且不会威胁到虚拟机自身的安全,主要有以下几个方面:

  • 文件格式验证:验证Class文件格式
  • 元数据验证:Java语言级语义分析
  • 字节码验证:验证方法体不会危害虚拟机自身
  • 符号引用验证:确保符号引用是可匹配的

准备

准备阶段主要为类变量方法区中分配内存并设置其初始值,此时的初始值为各种变量的默认零值。

解析

解析阶段主要是将Class文件中常量池内的符号引用替换为直接引用。

初始化

初始化触发条件

类的初始化是类加载过程的最后一步,在且仅在以下情况下,将触发类的初始化,它们是:

  1. 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有初始化,则必须先初始化类。反映在Java源代码中即:使用new新建对象,访问类静态变量和静态方法时
  2. 使用反射调用某个类时
  3. 子类初始化时父类尚未初始化,触发其父类的初始化
  4. 含有main方法的类优先初始化
  5. 使用MethodHandle调用类的静态方法时,如果类没有初始化,则触发其初始化

以上5种情况统称为对类的主动引用,对类的主动引用将触发类的初始化。其余情况对类的引用称为对类的被动引用。
以下被动引用不会触发类的初始化:

  1. 通过子类引用父类的静态字段,不会触发子类的初始化
  2. 通过数组定义引用类类型,不会触发引用类的初始化
  3. 对常量型静态字段的引用不会触发类的初始化,因为常量入了常量池,对该字段的引用变为对常量池中该字段的引用
初始化主要工作

这一步的主要工作为生成并执行()方法

生成

()方法由编译器收集类中所有类变量的赋值动作和静态语句块合并而成,其顺序即类变量和静态语句块在Java源文件中出现的顺序。静态语句块中的代码可以提前赋值给其后面的类变量,但是提前不能访问。

执行

初始化子类前会初始化父类。虚拟机会优先执行父类的()方法,再执行子类的()方法。

实例化

初始化阶段完成后,就是类的使用了,如果使用创建了类的实例对象,就涉及到类的实例化了。

实例化方式
方式 是否调用构造函数
new关键字
Constructor类的newInstance方法
clone方法
反序列化

PS:Class类的newInstance方法是Constructor类的newInstance方法支撑的。

在调用构造函数来实例化对象的过程中,满足类初始化的触发条件,将会触发类的初始化,再进行实例化。

实例化过程

注:静态变量和静态代码块的初始化在类的初始化过程已经执行了,这里将不再赘述。

对象的实例化操作在字节码层面分为两步:1、使用new关键字在堆上分配内存,创建对象。2、执行()方法进行实例变量等的初始化工作。
我们定义一个Father类:

public class Father {
    public Father(){
        System.out.println("no param");
    }
    public Father(String s){
        System.out.println("param s");
    }
    public static void main(String[] args) {
        Father f = new Father();
    }
}

其对应字节码如下:

// class version 51.0 (51)
// access flags 0x21
public class cn/john/test/Father {

  // compiled from: Father.java

  // access flags 0x1
  //对应无参构造函数Father()
  public <init>()V
   L0
    LINENUMBER 31 L0
    ALOAD 0
    //Father继承自Object,优先调用Object的方法
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 32 L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    //再执行Father()中的剩余初始化逻辑
    LDC "no param"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L2
    LINENUMBER 33 L2
    RETURN
   L3
    LOCALVARIABLE this Lcn/john/test/Father; L0 L3 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1
  //对应有参构造函数Father(String s)
  public <init>(Ljava/lang/String;)V
   L0
    LINENUMBER 35 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 36 L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "param s"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L2
    LINENUMBER 37 L2
    RETURN
   L3
    LOCALVARIABLE this Lcn/john/test/Father; L0 L3 0
    LOCALVARIABLE s Ljava/lang/String; L0 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 40 L0
    //新建对象,首先通过NEW关键字新建Father对象
    NEW cn/john/test/Father
    DUP
    //调用Father类的构造函数进行初始化工作
    INVOKESPECIAL cn/john/test/Father.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 41 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE f Lcn/john/test/Father; L1 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

由此我们可知,调用Father的构造函数新建Father类的实例,会先通过NEW关键字分配内存新建一个对象,然后优先调用Object的()方法进行初始化工作不是实例化一个父类),再执行构造函数剩下的逻辑,相当于将直接父类的初始化逻辑插入到子类的初始化逻辑之前

再来看Son类:

public class Son extends Father{
    private String name = new String("son");
    {
        System.out.println("construct block");
    }
    public Son(){
        System.out.println("son no param");
    }
    public Son(String s){
        System.out.println("son param s");
    }
    public static void main(String[] args) {
        Son s = new Son();
    }
}

其对应的字节码:

// class version 51.0 (51)
// access flags 0x21
public class cn/john/test/Son extends cn/john/test/Father  {

  // compiled from: Son.java

  // access flags 0x2
  private Ljava/lang/String; name

  // access flags 0x1
  //对应无参构造器
  public <init>()V
   L0
    LINENUMBER 38 L0
    ALOAD 0
    //优先调用父类初始化方法
    INVOKESPECIAL cn/john/test/Father.<init> ()V
   L1
    LINENUMBER 32 L1
    ALOAD 0
    NEW java/lang/String
    DUP
    //初始化实例变量
    LDC "son"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    PUTFIELD cn/john/test/Son.name : Ljava/lang/String;
   L2
    LINENUMBER 35 L2
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    //调用构造代码块
    LDC "construct block"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L3
    LINENUMBER 39 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    //执行构造函数逻辑
    LDC "son no param"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 40 L4
    RETURN
   L5
    LOCALVARIABLE this Lcn/john/test/Son; L0 L5 0
    MAXSTACK = 4
    MAXLOCALS = 1

  // access flags 0x1
  //对应有参构造函数
  public <init>(Ljava/lang/String;)V
   L0
    LINENUMBER 42 L0
    ALOAD 0
    //为什么这里调用的是父类无参构造方法,下文再作说明
    INVOKESPECIAL cn/john/test/Father.<init> ()V
   L1
    LINENUMBER 32 L1
    ALOAD 0
    NEW java/lang/String
    DUP
    LDC "son"
    INVOKESPECIAL java/lang/String.<init> (Ljava/lang/String;)V
    PUTFIELD cn/john/test/Son.name : Ljava/lang/String;
   L2
    LINENUMBER 35 L2
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "construct block"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L3
    LINENUMBER 43 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "son param s"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 44 L4
    RETURN
   L5
    LOCALVARIABLE this Lcn/john/test/Son; L0 L5 0
    LOCALVARIABLE s Ljava/lang/String; L0 L5 1
    MAXSTACK = 4
    MAXLOCALS = 2

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 48 L0
    NEW cn/john/test/Son
    DUP
    INVOKESPECIAL cn/john/test/Son.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 49 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    LOCALVARIABLE s Lcn/john/test/Son; L1 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

看过这两个例子,可见实例化的过程实际上分两步:

  1. new分配内存创建对象
  2. 执行对象的初始化方法(即构造方法)

这里执行对象的初始化方法顺序为:先执行父类的构造方法,再执行本类的构造代码块,再执行本类的构造方法。

至此,在通过构造函数实例化对象过程中,实际上按照顺序发生了如下操作:

  1. 父类静态变量的初始化与静态代码块的执行(按书写顺序)
  2. 子类静态变量的初始化与静态代码块的执行(按书写顺序)
  3. 父类实例变量初始化
  4. 父类构造代码块执行
  5. 父类相应构造方法执行
  6. 子类实例变量初始化
  7. 子类构造代码块执行
  8. 子类相应构造方法执行

其中1~2为类加载过程中的类初始化,3~8为实例化过程中的初始化。

构造代码块,this与super

构造代码块存在的意义是抽取了类中多个构造函数的公共部分或需要提前执行初始化的部分优先执行,更加优雅,实际上跟将构造代码块分散到各个构造函数的最前部分是一致的,从字节码的结果可以看到编译器实际上也是这么做的。

this关键字为在类的构造函数之间互相调用提供了条件,同样是更加优雅的书写构造函数的方式。注意在构造代码块和this同时存在的情况下构造代码块只会执行一次,同时this调用必须位于使用它的构造函数的第一行。

super关键字可以调用父类构造器。上文分析字节码时可以看到子类在初始化过程中总是优先调用父类的初始化方法,这里可以看成子类的构造函数在第一行加入了super(),调用了父类的无参构造器。当然,也可以指定调用有参构造器super(Type …args),只是在没有写明的情况下,编译器默认调用的是父类的无参构造器。

最后,类的生命周期中卸载问题,就是GC的事了。

深入理解Java虚拟机

你可能感兴趣的:(JVM,JVM)