类加载过程

  • 类从被加载到虚拟机内存开始,直到卸载出内存,它的整个生命周期包括:加载(Loading), 验证(Verification), 准备(Preparation), 解析(Resolution), 初始化(Initialization), 使用(Using)和卸载(Unloading) 7个阶段。其中准备、验证、解析3个部分统称为连接(Linking)。如下图:

  • 其中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,但解析阶段则不一定。在某些情况下解析可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(动态绑定)。

加载

  • 在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需完成以下3件事情:
    1. 通过一个类的全限定名来获取此类的二进制字节流(并未指明要从一个Class文件获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
    3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
  • 加载阶段和连接阶段的部分内容是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。

验证

  • 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
  • 验证阶段大致会完成4个阶段的检验动作:
    1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
    2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
    3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
    4. 符号引用验证:确保解析动作能正确执行
  • 验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备

  • 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。(这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。)
  • 其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:
public static int value=123;

   那变量value在准备阶段过后的初始值为0而不是123。因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
  至于“特殊情况”是指:public static final int value=123,即当类字段的字段属性是ConstantValue时,会在准备阶段初始化为指定的值,所以标注为final之后,value的值在准备阶段初始化为123而非0。

解析

  • 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
  • 这里讲到的符号引用和直接引用:
    • 符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
    • 直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,如果有了直接引用,那么引用的目标必定已经在内存中存在

初始化

  • 类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码
  • 初始化阶段是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块static{}中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。比如:
public class Test
{
    static
    {
        i=0;
        System.out.println(i);//这句编译器会报错:Cannot reference a field before it is defined(非法向前应用)
    }
    static int i=1;
}
  • <clinit>()方法与实例构造器<init>()方法不同,它不需要显示地调用父类构造器,虚拟机会保证在子类<init>()方法执行之前,父类的<clinit>()方法方法已经执行完毕 
  • 类初始化的场景
    • new指令:使用new关键字实例化对象时;
    • getstatic/putstatic指令:读取或设置一个类的静态字段时(被final修饰、已在编译期把结果放入常量池的静态字段除外);【原因:常量是一种特殊的变量,编译器将它们当做值而不是域来对待。编译器会在编译时将常量的值插到字节码中。这是一种很有用的优化。】
    • invokestatic指令:调用一个类的静态方法时。
    • 使用java.lang.reflect包的方法对类进行反射调用时,若类还未进行过初始化,则需要先触发其初始化。
    • 当初始化一个类时,发现其父类还未初始化,则需先触发父类的初始化。
    • 当虚拟机启动时,用户需要一个执行的主类(main()方法所在的类),虚拟机会先初始化该类。
  • 除此之外,还有一些不会触发初始化的例子:
    • 子类调用父类的静态变量,子类不会被初始化,只有父类被初始化。也即,对于静态变量,只有直接定义该字段的类会被初始化
    • 通过数组定义来引用类,不会触发初始化。
    • 访问类的常量,不会初始化类。

  看下面的例子
  

class SuperClass {
    static {
        System.out.println("superclass init");
    }
    public static int value = 123;
}
 
class SubClass extends SuperClass {
    static {
        System.out.println("subclass init");
    }
}
 
public class Test {
    public static void main(String[] args) {
        System.out.println(SubClass.value);// 调用父类的静态变量,只初始化父类
        SubClass[] sca = new SubClass[10];// 用数组定义来引用类,SubClass未被初始化
    // 程序输出: superclass init 123 } }
class ConstClass {
    static {
        System.out.println("ConstClass init");
    }
    public static final String HELLOWORLD = "hello world";
}
 
public class Test {
    public static void main(String[] args) {
        System.out.println(ConstClass.HELLOWORLD);// 调用类常量,ConstClass未被初始化
    // 程序输出: hello world } }

实例分析

class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    public static int count1;
    public static int count2 = 0;
 
    private SingleTon() {
        count1++;
        count2++;
    }
 
    public static SingleTon getInstance() {
        return singleTon;
    }
}
 
public class Test {
    public static void main(String[] args) {
        SingleTon singleTon = SingleTon.getInstance();
        System.out.println("count1=" + singleTon.count1);
        System.out.println("count2=" + singleTon.count2);
    }
}

 上述程序的输出是 1, 0而不是1, 1。为什么呢?我们分析一下上述程序的过程:

  1. 首先类加载、验证,然后在准备阶段为类的静态变量分配内存并初始化默认值 singleton=null count1=0,count2=0;
  2. 然后singleTon.getInstance()触发了类的初始化,为类的静态变量赋值并执行静态代码块。
    1. 首先,执行new SingleTon()方法,之后又count1 = 1, count2 = 1;
    2. 然后继续为count1和count2赋值,此时count1没有赋值操作,所有count1为1,而count2被赋值为0。

反射

  • 反射机制允许程序在执行过程中,利用Reflection API取得任何已知名称的类的内部信息,包括:package、type parameters、superclass、implemented interfaces、inner classes、outer classes、fileds、constructors、methods、modifiers等,并在执行过程中,动态生成instances、变更fileds内容或唤起methods。
    • 获取构造方法:
      • Constructor getConstructor(Class[] params)  --> 根据构造函数的参数,返回一个具体的具有public属性的构造函数;
      • Constructor getConstructors()  -->  返回所有具有public属性的构造函数数组
      • Constructor getDeclaredConstructor(Class[] params)  -->  根据构造函数的参数,返回一个具体的构造函数(不分public和非public属性)
      • Constructor getDeclaredConstructors()   -->  返回该类中所有的构造函数数组(不分public和非public属性)
    • 获取类的成员方法:也是4种,跟获取构造方法类似。  
    • 获取类的成员变量:同上。

反射机制能做什么

  • 在运行时判断任意一个对象所属的类;

  • 在运行时构造任意一个类的对象;

  • 在运行时判断任意一个类所具有的成员变量和方法;

  • 在运行时调用任意一个对象的方法;

  • 生成动态代理。

    System.out.println(tt.getClass().getName());  // 通过一个对象获得完整的包名和类名

     Class<?> class1 = null;
        Class<?> class2 = null;
        Class<?> class3 = null;
        // 一般采用这种形式
        class1 = Class.forName("net.xsoftlab.baike.TestReflect");
        class2 = new TestReflect().getClass();
        class3 = TestReflect.class;
        System.out.println("类名称   " + class1.getName());
        System.out.println("类名称   " + class2.getName());
        System.out.println("类名称   " + class3.getName());