JVM学习4--类装载、生命周期

一、类的生命周期

 

  首先要明确,类的生命周期是有五个阶段,而我们平时所说的类加载或者类装载,是指前三个阶段,即:加载、连接、初始化。

二、类装载概述

  在这块,大体上讲一下类装载的概念,然后有几个demo,产生的现象我们在细说加载、连接、初始化三个阶段时解释。

  类装载的条件: Class只有在被使用的时候才会被装载,Java虚拟机不会无条件地装载Class。Java虚拟机规定,一个类或接口被使用之前一定要进行初始化。这里的“使用”,指的是直接使用,也称直接引用,直接引用有以下几种情况:

  •  当创建一个类的实例时,比如new,或者用过反射、克隆、类序列化。
  •  当调用类的静态方法时,即当使用了字节码invokestatic指令。
  •  当使用类或接口的静态字段时(final常量除外),比如,使用getstatic或者putstatic指令。
  •  当使用java.lang.reflect包中的方法反射类的方法时。
  •  当初始化子类时,要求先初始化父类。
  •  作为启动虚拟机,含有main()方法的那个类。

  例①  是一个直接引用和间接引用的例子:

class Parent{
    static {
        System.out.println("Parent init");
    }
    public static int v = 100;
}

class Child extends Parent{
    static {
        System.out.println("Child init");
    }
}

public class IndirectReferencing {
    public static void main(String[] args) {
        // 直接引用(父类会先初始化,子类也会初始化)
        // 如果两步不分开执行,父类也只会初始化一次,这是ClassLoader做的保障,下面会提到
        Child child = new Child();

        //间接引用(子类不会初始化)
        System.out.println(Child.v);
    }
}

  例②  引用常量也不会引起初始化:

class FinalField{
    public static final String cons = "CONST";
    static {
        System.out.println("FinalField init");
    }
}

public class UseConstant {
    public static void main(String[] args) {
        // FinalField类不会初始化
        System.out.println(FinalField.cons);
    }
}

  我们可以在运行程序时加上-XX:+TraceClassLoading参数观察类的加载情况:

  例① 间接引用的情况:可以看到子类没有被初始化,但是子类其实也被加载进来了,这就是类生命周期第三阶段初始化的特点:直接引用初始化,间接引用不初始化。

    [Loaded JvmTest.ClassLoaderT.Parent from file:/D:/IntelliJ-idea/experiment/out/production/experiment/]
    [Loaded JvmTest.ClassLoaderT.Child from file:/D:/IntelliJ-idea/experiment/out/production/experiment/]
    Parent init
    100
    [Loaded java.lang.Shutdown from C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar]

  例②  可以发现这次FinalField都没有加载,那么我们可以看出一个事情,类就算不加载,常量依然可以使用,这是为什么呢?下面说连接阶段细说。

    [Loaded java.lang.Void from C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar]
    CONST
    [Loaded java.lang.Shutdown from C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar]

  也可以用字节码观察一下程序,javap -c xxx.class javap -verbose xxx.class :

  例② 

public class JvmTest.ClassLoaderT.UseConstant {
  public JvmTest.ClassLoaderT.UseConstant();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #4                  // String CONST
       5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

  观察main函数,使用了ldc,在字节码偏移3的位置,将常量池第4项入栈,在这里第四项是:

   #4 = String             #25            // CONST

  可以看到,在编译后的UseConstant类中,并没有引用FinalField类,而是直接把常量存到了常量池中,所以FinalField自然不会加载。

三、类装载之加载阶段

  加载类处于类装载的第一阶段。在加载类时,JVM完成了以下工作:

  1.  通过类的全名,获得类的二进制数据流。
  2.  解析类的二进制流为方法区内的数据结构。
  3.  创建java.lang.Class类的实例,表示该类型。

  在获得到类的二进制信息后,java虚拟机就会处理这些数据,最终转换为一个java.lang.Class的实例,java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据。通过Class类提供的接口,可以访问一个类型的方法、字段等信息。例子:Class.forname("java.lang.String")

四、 类装载之连接阶段

  1. 验证:目的是保证加载的字节码是合法、合理并符合规范的。

   

  2. 准备:虚拟机为类分配相应的内存空间,并设置初始值。

  final常量,直接就被赋值了。

  static普通静态变量,准备阶段只赋默认的初始值,在clinit中才会被设置为制定个值。例如:

  public static int v = 1; 准备阶段v被设置为0,clinit(初始化阶段)中设置为1。

  回答上面例2的疑问,为什么FinalField没有被加载呢?

    因为在连接阶段,常量可以直接被赋值,因此,FinalField类根本没被引用(直接引用或间接引用),自然不会被加载。

  3. 解析类: 将类、接口、字段、方法的符号引用转为直接引用

    在程序实际运行时,只有符号引用是不够的,例如当println()方法被调用时,系统需要明确知道该方法中的位置。以方法为例,Java虚拟机为每个类都准备了一张方法表,将其所有的方法都列在表中,当需要调用一个方法时,只要知道这个方法在方法表中的偏移量就可以直接调用该方法。通过解析操作,符号引用就可以转变为目标方法在类中方法表中的位置,从而使得方法被成功调用。可以说,如果直接引用存在,那么可以肯定系统中存在该类、方法、或者字段,但只存在符号引用,不能确定系统中一定存在该对象。

  

四、类装载之初始化阶段

  初始化是类装载的最后一个阶段。此时,类才会开始执行Java字节码。初始化阶段的重要工作是执行类的初始化方法<clinit>。方法<clinit>是由编译器自动生成的,它是由静态成员变量的赋值语句以及static语句块合并产生的。

  

 

posted @ 2018-05-05 10:51  NoYone  阅读(278)  评论(0编辑  收藏  举报