java · 2019-08-10 0

Java类的加载、连接、初始化和卸载

一、加载

加载是查找并加载类的二进制数据

把类的.class文件中的二进制数据读入到内存中,把它放在运行时数据区的方法区中,然后在堆区创建一个Class对象,用于封装类在方法区内的数据结构。

Class对象封装了类在方法区内的数据结构,并且向Java程序提供访问类在方法区的数据结构接口。

Java程序 - - - -(调用Class对象方法)- - - - > 堆中:描述Dog类的Class对象 - - - - - - - - > 方法区:Dog类的数据结构

类的加载是由加载器完成的,加载器有:启动类加载器、扩展类加载器、系统类加载器、用户自定义类加载器。

类加载器并不需要等到某个类被“首次主动使用”时才加载它,Java虚拟机规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在加载的过程中遇到.class文件缺失或者存在错误,类加载器必须等到首次主动使用该类时才报告错误,如果这个类一直没有被程序主动使用,那么类加载器将不会报告错误。

二、连接

连接就是把已经读到内存的二进制数据合并到虚拟机运行环境中去

连接阶段包括:验证、准备、解析

验证:确保加载类的正确性,例如,类文件的结构检查、语义检查、final类型的类有没有子类、字节码验证等;

准备:为类的静态变量分配内存,并设置默认的初始值,例如,

//Dog类有静态变量a
private static int a = 1;//在准备阶段,将为a分配4字节的内存空间,并赋予默认值0;

解析:把类中的符号引用转换为直接引用,例如,

/**
 *   在当前类的二进制数据中,包含了一个对Dog类的run()方法的符号引用,它有run()方法的全名和相关描述    
 * 符组成。
 *   在解析阶段,Java虚拟机会把这个符号引用替换为一个指针,该指针指向Dog类的run()方法在方法区内    
 * 的内存位置,
 *   这个指针就是直接引用。
 */
public void goRun(){
    dog.run();  //这段代码在worker类的二进制数据中表示为符号以引用           
}

三、初始化

初始化是为静态变量赋予正确的初始值(此处不是指常量,Java编译器和虚拟机对常量有特殊的处理方式)

Java虚拟机执行类的初始化语句,为类的静态变量赋予初始值,静态变量初始化有两种途径:

(1)在静态变量的声明出进行初始化;

(2)在静态代码块进行初始化。

假如类存在父类,并且这个父类还没有被初始化,那就先初始化父类。

在类被加载和连接的时机上,Java虚拟机提供了一定的灵活性,但是它严格定义了初始化的时机,所有的Java虚拟机实现必须在每个被Java程序“首次主动使用”时才初始化他们。

Java程序对类的使用方式可分为两种:主动使用(导致类的初始化)、被动使用(不会导致类的初始化)

只有6种活动别看做程序对类的主动使用:

(1)创建类的实例,通过new、反射、克隆、反序列化手段来创建实例

(2)调用类的静态方法

(3)访问类的静态变量

(4)调用某些反射方法,forName()方法

(5)初始化一个类的子类

(6)被表明启动类的类

栗子:

(1)对于final类型的静态变量,如果在编译时就能计算出变量的取值,那么这种变量被看做编译时常量,如下面的栗子,当Java编译器生成Test类的.class文件时,它不会在main()方法的字节流中保存一个表示Dog.b的符号引用,而是直接在字节流中嵌入常量6,因此当程序访问Dog,b时,客观上无须初始化Dog。

如果在编译时就不能计算出变量的取值,那么程序对这种变量的使用,被看做对类的主动使用。

public class Test {

    public static void main(String[] args) throws ClassNotFoundException {
//        int a = Dog.a;     //输出"Dog static"
        int b = Dog.b;       //没有输出
//        int c = Dog.c;     //输出"Dog static"
    }

}

class Dog{
//    访问类的静态变量,被看做类的主动使用,会导致类的初始化
    public static int a = 20;
//    访问类的编译时常量,被看做类的被动使用,不会导致类的初始化
    public static final int b = 2*3;
//      c不是编译时常量,被看做类的主动使用,会导致类的初始化
    public static final int c = (int)Math.random();
    static {
        System.out.println("Dog static");
    }
}

(2)当Java虚拟机初始化一个类时,要求它的所有父类都已经被初始化,但是这条规则并不适用于接口。

(3)只有当程序访问的静态变量或静态方法的确在当前类定义时,才可以看做对类的主动调用。

public class Test {

    public static void main(String[] args) throws ClassNotFoundException {
        int a = Dog.a;      //输出 "Animal static"
//        int b = Dog.b;  //输出 "Animal static" "Dog static"
    }

}

class Dog extends Animal{
    public static int b = 20;
    static {
        System.out.println("Dog static");
    }
}

class Animal{
    public static int a = 10;
    static {
        System.out.println("Animal static");
    }
}

(4)调用ClassLoader类的loadClass方法加载一个类,并不会对类的主动使用,不会导致类的初始化;调用Class类的forName()方法,是对类的主动使用,会导致类的初始化。

package com.example.object;

public class Test {

    public static void main(String[] args) throws ClassNotFoundException {
//        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
//        classLoader.loadClass("com.example.object.Dog");  //无输出
        Class.forName("com.example.object.Dog");            //输出 "Dog static"
    }

}

class Dog {
    public static int a = 10;
    static {
        System.out.println("Dog static");
    }
}

四、卸载

当类的Class对象不再被引用,Class对象就会结束生命周期,方法区的数据也会被卸载。

有Java虚拟机自带的类加载器,所加载的类,在虚拟机的生命周期中,始终不会被卸载,Java虚拟机本身会始终引用这些类加载器,而这些类加载器则始终引用它们锁加载类的Class对象。

当类处于生命周期中时:类二进制数据位于方法区中;在堆内会有一个相应的描述这个类的Class对象。