类的加载过程

类的加载过程是指类加载器尝试加载class二进制文件,并在JVM中生成对应的数据结构,然后使其分布在JVM对应的内存区域。

一、类的主动引用和被动引用

JVM虚拟机规范规定,每个类或者接口被java程序首次主动使用时才会对其进行初始化,JVM同时规范了一下6种主动使用类的场景:

1、通过new关键字会导致类的初始化
2、访问类的静态变量,包括读取和更新会导致类的初始化。示例代码:

public class Demo {
    static{
        System.out.println("进行初始化");
    }
    //访问简单静态变量x,会导致Demo类的初始化。
    public static int x = 10;
}

3、访问类的静态方法会导致类的初始化。示例代码:

public class Demo{
    static{
        System.out.println("进行初始化");
    }
    //在其他类中调用静态方法test,会导致Demo类的初始化。
    public static void test(){
    }
}

4、对某个类进行反射操作会导致类的初始化。示例代码:

public static void main(String[] args) throws ClassNotFoundException {
        //通过反射导致Demo类初始化
        Class.forName("co.youzi.test.Demo");
    }

5、初始化子类会导致父类初始化。示例代码:

public class Parent {
    static{
        System.out.println("父类初始化");
    }
    public static int x = 10;
}
public class Child{
    static{
        System.out.println("子类初始化");
    }
    public static int y = 100;
}
public class Test {
    public static void main(String[] args) throws ClassNotFoundException {
        System.out.println(Child.y);//输出 父类初始化 子类初始化 100
        //System.out.println(Child.x);//输出 父类初始化 10
    }
}

注意:通过子类使用父类的静态变量只会导致父类的初始化,子类不会初始化。
6、启动类所在类会被初始化
也就是main函数所在类会被初始化,如上面的Test类。

除了上述六种情况,其他情况都属于被动使用,不会导致类的初始化,比如:
1、构造某个类的数组时不会导致类的初始化,代码示例:

public class Test05 {
    public static void main(String[] args) throws ClassNotFoundException {
        //Parent类不会被初始化
        Parent[] parent = new Parent[10];
        System.out.println(parent.length);
    }
}

2、引用类的简单静态常量不会导致类的初始化,代码示例:

public class Simple {
    static{
        System.out.println("进行初始化");
    }
    //访问静态常量MAX不会导致Simple类的初始化
    public final static int MAX = 10;
    //虽然RANDOM是静态常量,但是由于计算复杂,只有初始化后才能得到结果,因此其他类使用RANDOM会导致Simple类的初始化
    public final static int RANDOM = new java.util.Random(10).nextInt();
}

二、类的加载过程详解

类的加载过程分为三个大的阶段,分别是加载阶段、连接阶段和初始化阶段。连接阶段又包括验证、准备和解析三个阶段。
这里写图片描述

1、加载阶段
类的加载阶段是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转化为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.class对象,作为方法区数据结构的入口。
类加载阶段的最终产物的堆内存中的class对象,对于同一个Classloader对象,不管某各类被加载多少次,对应堆内存中的class对象始终只有一个。
类加载阶段发生在连接阶段之前,但连接阶段不必等加载阶段结束才开始,可以交叉工作。
下图是类加载阶段后的内存分布情况:
类加载阶段后的内存分布情况

2、连接阶段
类的连接阶段包括三个小的过程:分别是验证、准备和解析。
(1)验证
验证在连接阶段中的主要目地是确保class文件的字节流所包含的内容符合JVM规范,并且不会出现危害JVM自身安全的代码。但验证不符合要求时,会抛出VerifyError这样的异常后其子异常。
主要验证内容有:

  • 验证文件格式:包括文件头部的魔术因子、class文件主次版本号、class文件的MD5指纹、变量类型是否支持等
  • 元数据的验证:对class的字节流进行语义分析,判断是否符合JVM规范。简单来说就是java语法的正确性
  • 字符码验证:主要验证程序的控制流程,如循环、分支等
  • 符号引用验证:验证符号引用转换为直接引用时的合法性,保证解析动作的顺利执行。比如不能访问引用类的私有方法、全限定名称是否能找到相关的类。

(2)准备
准备阶段主要做的事就是在方法区为静态变量发配内存以及赋初始默认值(区别于初始化阶段赋的程序指定的真实值)
注意:final修饰的静态常量在编译阶段就已经赋值,不会导致类的初始化,是一种被动引用,因此也不存在连接阶段。

(3)解析
解析就是在常量池中寻找类、接口、字段和方法的符号引用,并且将这些符号引用替换成直接引用的过程。
解析过程主要针对类接口、字段、类方法和接口方法四类进行。

3、初始化阶段
初始化阶段是类的加载过程的最后一个阶段,该阶段主要做一件事情就是执行< clinit>(),该方法会为所有的静态变量赋予正确的值。
关于静态变量赋值注意以下几点:

  • 静态语句块可以对后面的静态变量赋值,但不能对其进行访问。代码示例:
public class Test {
    static{
        System.out.println(x);//此处不能通过编译
        x=100;
    }
    private static int x = 10;
}
  • 父类静态变量总是能优先赋值。代码示例:
public class Parent02 {
    static int x = 10;//①
    static{ x = 20; }//②
}
public class Child02 extends Parent02{
    static int i = x;

    public static void main(String[] args) {
        System.out.println(Child02.i);//输出20
    }
}

父类优先执行< clinit>()方法,x被赋值20。所以程序输出20.
注意:如果以上代码中①行和②行位置调换,则程序输出10。准备阶段在方法区分配x的内存,值为0,初始化阶段< clinit>()方法顺序执行。


  • 5
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值