深入理解Java虚拟机笔记(6)-------类加载时机与类加载过程

类加载时机

类从加载到卸载共有7个阶段,如下图
在这里插入图片描述
加载、验证、准备、初始化和卸载这五个阶段的开始顺序是确定的,并不意味着完成的阶段也是这个顺序,因为某一个阶段可能会调用其他阶段。其中解析阶段有可能在初始化阶段之后执行。

类加载阶段开始的时机会根据虚拟机而不同,但初始化阶段在在下面6中情况中必须立即开始:

  1. 遇到newgetstaticputstaticinvokestatic这四条字节码指令时:
    能够生成这4中指令的场景有:使用new关键字实例化对象;读取或者设置一个静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外) ;调用一个类型的静态方法时。
  2. 使用java.lang.reflect包的方法对类型进行反射调用,如果类没有初始化则需要先初始化
  3. 初始化一个类的时候,如果父类没有初始化则初始化父类
  4. 虚拟机启动时会先初始化包含main方法的类。
  5. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  6. 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

类加载过程

加载

加载阶段主要做三件事:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
    获取字节流的方式可以有很多,如从压缩包中读取;从网络上读取;运行时计算生成的(JAVA动态代理类Proxy,使用ProxyGenerator.generateProxyClass()来为特定接口生成形式为“*$Proxy”的代理类的二进制字节流);从数据库中读取;从加密文件中读取;从其他文件中读取(JSP文件生成Class文件)
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

类型的加载可以是由JVM内置的类加载器加载的,也可以是有开发者自定义的类加载器加载的(重写加载器的findClass()方法或者loadClass()方法),但是数组不同,数组是由JVM直接在内存中动态构造出来的,具体过程较为复杂,暂不讨论。

验证

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。(简单来说就是确保这些字节流信息不会让程序出错)
验证阶段大致有4个验证动作:

  1. 文件格式验证
    主要验证字节流文件的格式,检查它存放的信息是不是描述Java类型的信息,如果这个验证没有通过的话,JVM就不允许这段字节流存放在方法区内存中。
  2. 元数据验证
    验证类的元数据信息是不是满足《Java语言规范》,也就是我们写的代码正不正确(一般IDE会提醒我们的错误,如继承了final修饰的类,没有实现接口的方法等)。可以理解为类的定义阶段要满足要求
  3. 字节码验证
    主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的(保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作,例如不会出现类似于“在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中”这样的情况;保证任何跳转指令都不会跳转到方法体以外的字节码指令上,)
  4. 符号引用验证
    这一阶段的校验行为发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。(如:符号引用中通过字符串描述的全限定名是否能找到对应的类;在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段;符号引用中的类、字段、方法的可访问性(private、protected、public、)是否可被当前类访问)

验证阶段不是必须的,如果程序运行的全部代码(包括第三方的)都已经反复的使用过验证过,那么就可以考虑使用-Xverify:none参数关闭大部分的验证措施,优化加载时间。

准备

准备阶段就是为类的类变量(static修饰的成员变量)分配内存空间并且设置初始值(也就是默认值,如int 默认为0,类型默认为null等)

解析

  • 解析阶段是JVM将常量池内的符号引用替换为直接引用的过程。解析阶段我还没完全搞懂,之后再补吧。

初始化

这里说的初始化是类加载的初始化阶段,而不是对象的初始化,不要弄混。类初始化阶段会执行类构造器的<clinit>()方法(不会调用“构造方法”——对应类构造器的<init>()方法),<clinit>()是javac编译器的自动生成物,主要包括类变量的赋值语句和静态语句块。

public class Test {
    static int i = 1;//1.类变量的赋值
    static {
        System.out.print("Class Initializing"); //2.静态代码块
    }

    public Test() {
        i = 100;//3.构造方法
    }
}                   

在类初始化阶段会执行代码1、2,而3的构造方法是在对象实例化阶段才会执行。
静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,只可以赋值,不能访问。如下代码。

public class Test {
    static int i = 1;
    static {
        i = 2;
        System.out.print(i); 
        System.out.print(j);
    }
    static int j = 10;
}

在编译阶段就会爆出非法向前引用的Error,可见源代码的顺序影响编译。
在这里插入图片描述
<clinit>()方法与类的构造函数(即在虚拟机视角中的实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,Java虚拟机会保证在子类的<clinit>()方法执行前,父类的<clinit>()方法已经执行完毕。因此在Java虚拟机中第一个被执行的<clinit>()方法的类型肯定是java.lang.Object。

<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

当多个线程同时初始化一个类的时候,只能有一个线程(假设是A)执行<clinit>()方法。当线程A进行类初始化的时候其他线程会进入阻塞状态,当线程A完成类初始化之后,其他线程由阻塞状态变为运行状态,但是不会再去执行<clinit>()方法。同一个类加载器只会初始化一个类型一次。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值