Java 类加载机制

一.类加载时机

生命周期: 加载, 验证, 准备, 解析, 初始化, 使用, 卸载

其中加载, 验证, 准备,初始化卸载这五个阶段顺序是确定的.

类加载时机:

以下5种情况必须立即对类进行"初始化"(加载.验证.准备会在这之前开始)

1) 遇到new, getstatic, putstatic 或 invokestatic 这四条字节码命令时, 如果类没有初始化, 就先初始化. 生成这4条指令最常见的Java代码的场景是:使用new实例化对象, 读取或设置一个类的静态字段(被final修饰, 在编译期把结果放入常量池的静态字段除外), 调用静态方法

2) 使用反射对类进行调用的时候

3) 当初始化一个类的时候, 发现其父类还没有初始化, 则要先初始化其父类

4) 虚拟机启动时, 用户指定一个要执行的主类(包含main的类), 先要初始化这个主类

5) 使用动态语言支持时

这5种场景中的行为称为对一个类进行主动引用, 除此之外, 所有引用类的方式都不会触法初始化, 称为被动引用. 

注意:a.通过子类读取或设置父类的静态字段, 不会导致子类被初始化

        b.定义某一个类的数组(没有创建对象),不会导致该类被初始化,但是会创建一个可以存放这个类的一维数组对象(虚拟机自动生成的),里面包含了length属性和clone()方法

        c.静态常量在编译阶段会存入调用类的常量池中, 本质上并没有直接引用到定义常量的类, 在编译阶段通过常量传播优化将此常量存储到了NotInitialization类的常量池中,以后对此常量的引用都被转化为NotInitialization类对自身常量池的引用, 因此包含此常量的类不会被加载

接口加载时机:只与类加载的第三条有区别, 接口在初始化时, 并不要求父接口全部完成初始化,只有在真正使用到父接口时(如父接口中定义的常量)才初始化.

二.类加载的过程

1.加载

1) 通过一个类的全限定名来获取定义此类的二进制字节流

2) 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构

3) 在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入口

类的二进制字节流没有指明从哪里获取,可以从class文件, 也可以从其他地方.

如: a.从ZIP包中获取(JAR, EAR, WAR)

     b.从网络中获取(Applet)

     c.运行时计算生成, 这种场景使用的最多的就是动态代理技术, 可以使用java.lang.reflect.Proxy中的工具来为特定接口生成代理类的二进制字节流,

     d.由其他文件生成(JSP)

     e.从数据库读取(少见)

类加载的阶段可以使用系统提供的类加载器, 也可以自己自己定义类加载器, 从而改变获取类字节流的方式.

定义自已的类加载器分为两步:

    1、继承java.lang.ClassLoader

    2、重写父类的findClass方法(loadClass()方法搜索不到类时就会调用findClass()方法)

类加载完成之后, 虚拟机就二进制流按照所需格式存储在方法区中, 然后在内存中实例化一个java.lang.Class类的对象(存储在方法区中), 这个对象作为程序访问方法区中的这些类型数据的外部接口.

2.验证

1) 文件格式验证

是否以魔数(0xcafebabe)开头, 主.次版本号是否在当前虚拟机处理范围之内, 常量池中是否有不被支持的常量类型, 指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量, Class文件各个部分是否有被删除或附加其他信息.....

2) 元数据验证

这个类是否有父类, 这个类是否继承了不允许被继承的类(final修饰的类不能被继承), 如果这个类不是抽象类那么是否实现类父类或接口之中的要求实现的方法, 类中的字段.方法是否与父类产生矛盾

3) 字节码验证

保证跳转指令不会跳转到方法体以外的字节码指令, 保证方法体中的类型转换时有效的

4) 符号引用验证

符号引用中听过字符串描述的全名限定能否找到对应的类, 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段, 符号引用中的类.字段.方法的访问性是否可以被当前类访问

验证阶段时一个非常重要的但是不必要的阶段(对程序运行没有影响), 如果所运行的代码已经被反复使用和验证过, 可选择使用-Xverify:none参数来关闭大部分的验证措施, 以缩短类加载时间

3.准备

准备阶段是正式为类变量(不包括实例变量)分配内存并设置类变量初始值(数据类型的0值)的阶段, 这些变量所使用的内存都将在方法区中进行分配.假设一个变量为:

public static int value=123;

那么value在准备阶段之后的初始值为0, 而不是123, 把value赋值为123是在程序被编译之后(初始化阶段)

注意:如果是静态常量, 在准备阶段就会将其赋值为123

4.解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程.

符号引用:符号引用以一组符号来描述所引用的目标, 可以是任何形式的字面量, 只要能无歧义的定位到目标即可.

直接引用:直接引用可以是直接指向目标的指针.相对偏移量或是一个能间接定位到目标的句柄.

1) 类或接口的解析

2) 字段解析

3) 类方法解析

4) 接口方法解析

5.初始化

初始化是类加载过程的最后一步, 在准备阶段变量已经赋过一次系统要求的初始值, 而在初始化阶段就会执行类构造器<clinit>()方法.

  • <clinit>()方法是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的, 顺序是由语句在源文件中出现的顺序所决定的, 静态语句块只能访问定义在静态语句块之前的静态变量, 定义在它之后的变量, 在前面的静态语句块可以赋值, 但是不能访问.
  • <clinit>()方法与构造函数不同, 它不需要显式调用父类的<clinint>()方法, 因为虚拟机会保证子类<clinit>()方法直接之前父类的已经执行完毕, 所有虚拟机中第一个被执行<clinit>()方法的类一定是java.lang.Object
  • 因为父类的<clinit>()方法会被先执行, 所以父类中的静态语句块也优先执行
  • <clinit>()方法对于类或接口来说并不是必须的, 如果类中没有静态语句块, 也没有对变量的赋值操作, 就不用生成<clinit>()方法
  • 接口中不能使用静态语句块, 但可以有变量初始化赋值操作, 因此接口也会生成<clinit>()方法, 但执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法, 只有当父接口中定义的变量使用时, 父接口才会初始化, 接口的实现类在初始化时也不会执行接口的<clinit>()方法
  • 虚拟机会保证一个类的<clinit>()方法在多线程中被正确的加锁.同步, 如果多个线程同时去初始化一个类, 那么只有一个线程去执行这个类的<clinit>()方法, 其他线程会阻塞等待, 直到<clinit>()执行完毕, 如果一个类的<clinit>()方法中有耗时很长的操作, 就可能造成多个进程阻塞

三.类加载器

1.类与类加载器

比较两个类是否相等, 只有在这两个类是由同一类加载器加载的前提下才有意义, 否则即使这两个类来源于同一个Class文件, 被同一个虚拟机加载, 只要他们的类加载器不同, 那么这两个类就必定不相等.相等包括类Class对象的equals()方法/isInstance()方法/isAssignableFrom()方法返回的结果, 也包括使用instanceof关键字的判断结果.

类加载器总结

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值