类加载流程简介(一)

一、类加载描述

类加载过程就是将class文件加载到内存,并对进行校验、解析、初始化等的过程,其实就是将编译后的java文件(.class文件)变成虚拟机中的class对象,每个类都有且只有一个class对象(class和类加载器确定一个class对象,类一样但是由不同的加载器加载也是不同的class对象),都是首次主动使用后被加载到JVM的方法区中。类加载过程在程序运行期间完成。
类从被加载到内存开始,到卸载出内存为止,整个生命周期如下:
类加载生命周期
那么虚拟机是如何知道在什么情况下需要对一个类做加载动作?java虚拟机规范并没有强制的要求,但是对于初始化阶段,虚拟机规范给出一下几种情况必须对类执行初始化(通常我们称之为主动使用):

1.在遇到 new、putstatic、getstatic、invokestatic 字节码指令时,如果类尚未初始化,则需要先触发初始化。说通俗就是实例化、访问类(接口)的静态变量或者方法(被final修饰、已在编译期把结果放入常量池的静态字段除外)!
2.对类进行反射调用时,如果类还没有初始化,则需要先触发初始化。
3.初始化一个类时,如果其父类还没有初始化,则需要先初始化父类(初始化一个接口的时候不需要初始化其父接口-_-!)。
4.虚拟机启动时,用于需要指定一个包含 main() 方法的主类,虚拟机会先初始化这个主类。
5.当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发初始化。

上面说的5类会触发类对初始化场景,初次之外都不会触发初始化,称之为被动使用,有2个代码片段:

//编译器常量是不能引起类初始化的(下文会说到)
public class Sup{
    public static final int num = 123;
}
public class Launch {
    public static void main(String[] args) {
        System.out.println(Sup.num);
    }
}
//通过子类调用父类静态变量时不会初始化子类的。只会初始化父类。但是可能会执行加载和验证。
public class Sup{
    public static int num = 123;
}
public class Sub extends Sup{}
public class Launch {
    public static void main(String[] args) {
        System.out.println(Sub.num);
    }
}

下面看下每个阶段的具体动作。

二、类加载流程

加载

加载是类加载过程的第一个阶段,需要完成下面3件事:

1、通过一个类的全限定名获取定义此类的二进制字节流。
2、将字节流代表的静态存储结构转换为方法区的运行数据结构。
3、内存中生存class对象。

上面并没有具体说明二进制字节流到底从哪里获取,除了从编译好的 .class 文件中读取外,还有以下方式:

从 zip 包中读取,如 jar、war 等、从网络中获取、通过动态代理生成代理类的二进制字节流、从数据库中读取等等。

加载阶段是提供给开发者很大的操作空间,比如可以通过自定义类加载器去加载一些特定的类等。
单独说下数组,数组不是由类加载器创建的,它是由虚拟机直接创建的,但是数组中的元素类型是需要类加载器去加载。

连接

连接阶段分为了3步:验证、准备、解析3个阶段组成。

验证

作为连接阶段第一步,就是为了确保class文件包含的信息符合当前虚拟机要求,而且不能危害虚拟机自身安全。大体上会完成一下几个校验:
1、文件格式校验,比如魔数0XCAFEBABY、主次版本号(只能向下兼容)、常量池中是否有指向不存在常量等)。通过文件格式校验后字节流就会进入内存方法区中存储。
2、元数据校验,保证不存在不符合java语言规范的元数据信息。比如:类是否继承不允许被继承的类(final 类)、类是否实现了接口所有的方法、类字段、方法是否和父类有冲突等等校验。
3、字节码验证,是对类中方法体进行验证。
4、符号引用验证,在符号引用变成直接引用的时候需要做如下的验证工作:根据描述是否可以找到对应的类、符号引用中的类、方法、字段是否可以被访问(private、protected、public、default)等

准备

准备阶段是为类中的静态变量分配内存(方法区)以及设置一个初始值,为什么是静态变量?因为非静态的变量是需要对象实例化后随着对象一起分配在堆中的。
比如:
private static int num = 666;
那么在准备阶段这个num的初始值就是0,因为准备阶段并没有开始执行任何java方法,而把 num 赋值为666的putstatic指令是存放在()方法中,在初始化阶段才会被执行到。不同的基本类型初始值是不同的。
如果这个变量被final修饰后,那么就变成常量了,这里常量分为2种:编译时常量和运行时常量。
//编译时常量
private static final int num = 10;
//运行时常量
private static final int rNum = new Random().nextInt();
对于编译时常量在准备阶段直接将 num 赋值为10,而运行时常量则不行。所以编译期常量不依赖类,不会引起类的初始化;而运行时常量依赖类,会引起类的初始化

解析

将常量池中的符号引用替换为直接引用(内存地址)的过程。

符号引用 : 就是一组符号来描述目标,可以是任何字面量。属于编译原理方面的概念如:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
直接引用:就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。如指向方法区某个类的一个指针。

初始化

为静态变量赋值动作,就是执行类构造器方法(())的过程。
1、() 方法由编译器自动收集类中所有静态变量赋值动作和静态代码块(static{})中的语句结合产生的。顺序是由java源文件出现的顺序决定的
2、()非必须的!如果没有静态变量或者静态代码快,那么就没有赋值动作,那么也没有()方法了。
3、在执行()方法时,必须先执行父类的()方法。接口除外,当接口执行()方法不需要执行父接口的()方法(接口的实现类也是)!
4、()方法只执行一次!

上面主要对加载、连接、初始化阶段做了一个简介。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值