类的加载过程

时间:2019-12-07
本文章向大家介绍类的加载过程,主要包括类的加载过程使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

类的加载过程

JVM运行期间完成类的加载、连接和初始化:

装载

  查找并获取被加载类的字节码的二进制数据。如通过网络获取、本地文件系统、数据库中提取、动态生成(如动态代理)。

连接/链接:将二进制数据合并到JVM的运行时环境中去

a)      验证:确保字节码文件的内容是正确的,内容是符合JVM规范的(结构检查、语义检查)

b)      准备:为静态变量分配内存,并初始化为对应数据类型的默认值

c)      解析:将类中的符号引用(类、接口、字段和方法)转换为直接引用

初始化

  初始化过程只会有一次。会依次执行静态成员的赋值语句和静态代码块。所以可以通过为类添加静态代码块来判断类是否被初始化。

  静态变量的赋值以及静态代码块都是从上往下执行的,所以静态代码块中访问到的静态变量,必须要在静态代码块之上,否则编译就会报错。

通过添加参数 -XX:+TraceClassLoading ,运行程序时会有日志输出,能观察到哪个类被装载和连接(前两步),并且来源于哪个文件。

以上这几个阶段都是运行时期完成的,编译时期不会执行以上操作。这几个步骤的最终结果就是JVM中的一个字节码对象(java.lang.Class)。字节码对象可能会出现只装载了,却还没初始化的情况。HotSpot虚拟机将字节码对象存放在方法区。

关于静态代码块和构造代码块:

静态代码块:可以存在多个,类初始化时依次执行。

构造代码块:可以存在多个,类实例被创建时依次执行,执行完后才执行构造函数。构造函数与构造代码块的次序可随意。匿名内部类不能显式指定构造函数,所以可以通过构造代码块来完成类似构造函数的工作。

上面提到初始化过程中会依次执行静态成员的赋值语句和静态代码块,如果次序不正确的话,会导致数据问题:

static class Singleton {
    private Singleton() {
        data++;
    }

    static Singleton singleton = new Singleton();
    public static int data = 0;
}

public static void main(String[] args) {
    System.out.println(Singleton.singleton.data); // 输出 0
}

解决办法就是把两个赋值语句换一下次序,结果就正常输出1了。

类的主动使用和被动使用

主动使用会触发字节码对象的初始化,被动使用不会触发初始化。

主动使用的七种情况:

  1. 创建类的实例
  2. 访问类或接口的静态变量,或对该静态变量赋值
  3. 反射,如Class.forName
  4. 初始化一个类的子类,即子类被触发初始化时,父类字节码对象如果没被初始化,则进行初始化。如果一个接口被触发初始化,则父接口不会被初始化
  5. 启动类,如程序启动时,会对被标记为启动类的类进行初始化
  6. JDK1.7提供的动态语言支持。

class.forName这个方法最终调用了3个参数的重载方法,里面有一个参数来确定是否对类进行初始化,默认值为true,可以设置为false,这样就不会初始化类了。

对类的使用,除了以上7中情况外,其余都属于被动使用。

被动使用不会导致类的初始化(如classLoader.loadClass加载的类就不会被初始化),但可能会触发这个类的装载过程,这称为“预先加载”。

当JVM认为某个类可能要被使用时,才会触发这个预先加载过程,期间如果被预先加载的类的class文件缺失、或校验有问题,则等到这个类被主动使用时,才会抛出LinkageError错误,否则不会报告错误。

被动使用的示例演示

一个通用的观察类或接口的初始化过程是否有被触发:

静态代码块有被执行,说明类被初始化。但是接口没有静态代码块,所以可以通过观察静态成员是否有被初始化:

interface Api {
    static Thread handler = new Thread(){
        {
            System.out.println("interface init");
        }   
    };
}

通过这个技巧来观察类在不同情况下的初始化行为,后续列举的示例都是通过这个技巧来确定类或接口是否有被初始化。

1. 通过子类来访问父类的静态字段,如Child.str 。父类的静态代码块会被触发,而子类不会,此时子类属于被动使用。因为实际访问的是父类中的字段,而不是子类的字段,实际被访问的类才会被初始化,这对应主动使用的第2条规律。

2. 访问类的静态常量时并且这是个编译时期能确定下来的常量,则通过访问这个字段时,不会触发类的初始化,也属于类的被动使用。

// 运行时不会发生变化,即编译时期能确定具体的值
public static final String  str = "123";

// 运行时,具体的值才能确定下来,编译时期无法确定
public static final int r = new Random().nextInt();

这属于编译器的优化:反编译代码可以发现,这个常量被存放到了调用类的常量池中,获取数据时是直接从当前类的常量池中获取,从而使这次静态成员的访问与被调用类无关。

这个规律也同样可以应用在接口上。

3. 仅仅创建引用,如 A a; 不会导致类A的初始化。同理,A[] arr = new A[1] ,也不会导致A的初始化。

关于数组类

数组类都是由JVM运行时动态创建出来。对应的字节码的命名规则:

一维数组:[ + 元素的全限定名

二维数组:[[ + 元素的全限定名

对于基本类型的数组,基本类型没有全限定名,但用一个字母来代替,如int 对应I,char对应C等。

原文地址:https://www.cnblogs.com/hellohello/p/12002335.html