微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

类的加载过程

编译:将.java文件通过javac命令编译成.class文件

运行:将.class文件交给JVM运行

类加载:

其中的 链接 可以细分为三个小部分:

  • 验证
  • 准备
  • 解析

加载

主要完成三件事:

  1. 通过全类名获取定义此类的二进制字节流
  2. 字节流所代表的静态存储结构转换为方法区的运行时数据结构
  3. 在内存中生成一个代表该类的Class对象,作为方法区这些数据的访问入口

 这个地方要注意两点:

  • .class文件的来源:一般的加载来源有本地路径下编译生成的.class文件jar包中的.class文件,远程网络和动态代理实时编译
  • 类加载器:一个非数组类的加载阶段一般包括启动类加载器拓展类加载器系统类加载器用户自定义类加载器(重写一个类加载器的loadClass()方法,数组类型不通过类加载器创建,它由JVM直接创建

为什么需要自定义类加载器?

  • 一方面,由于Java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载
  • 另一方面,如果是非标注来源的加载代码,比如从网络来源,那就需要自己实现一个自定义类加载器,从指定源进行加载

加载阶段和链接阶段的部分内容是交叉进行的,加载阶段尚未结束,连接阶段可能就已经开始了

链接

验证

主要是为了保证加载进来的字节流符合虚拟机规范,不会造成安全错误

  • 文件格式验证:验证字节流是否符合Class文件格式的规范,例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围内、常量池中的常量是否有不被支持的类型
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求,例如:这个类是否有父类,除java.lang.Object之外所有类都有父类、这个类是否被继承了不允许继承的类(被final修饰的类)
  • 字节码验证:最复杂的阶段。通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。比如保证任意时刻操作数栈和指令代码序列都能配合工作
  • 符号引用验证:确保解析动作能正确执行

准备

准备阶段是正式为类变量(静态变量)分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。对于该阶段有以下几点需要注意:

这时候进行内存分配的仅包括类变量,而不包括实例变量。实例变量会在对象实例化时随着对象一块分配在堆中

从概念上讲,类变量所使用的内存都应当在方法区中进行进行分配。不过,在JDK 7 之前,HotSpot使用永久代来实现方法区的时候,实现是完全符合这种逻辑概念的。而在JDK 7 之后,HotSpot已经在永久代改为元空间,同时移到堆中,所以类变量则会随着Class对象一起存放在堆中。

这里所设置的初始值,通常情况下是数据类型的认值(如0、0L、null、false等),比如定义

public static int value = 111

 那么value变量在准备阶段的初始值就是0,而不是111(初始化才会赋值)。特殊情况:比如给value变量加上final关键字,那么准备阶段value的值就被赋值为111

 解析

 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法方法类型、方法句柄和调用限定符 七类符号引用进行

符号引用就是一组符号来描述目标,可以是任何字面量。直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。在程序实际运行时,只有符号引用是不够的,举个栗子:在程序执行方法时,系统需要明确知道这个方法所在的位置。而JVM为每个类都准备了一个方法表来存在类中所有的方法。当需要调用一个类的方法时,只要知道这个方法方法表中的偏移量就可以直接调用方法了,所以通过解析操作 符号引用 就可以直接转变为目标方法在类中方法表的位置,从而使得方法可以被调用

综上,解析阶段就是得到类、字段或方法在内存中的指针或偏移量

初始化

初始化阶段是执行初始化方法 <clinit> () 方法的过程,是类加载的最后一步,这一步JVM才开始真正执行类中定义的Java程序代码(字节码)

<clinit> () 方法是编译后生成

对于 <clinit> () 方法调用,JVM会自己确保其在多线程环境中的安全性。因为 <clinit> () 方法是带锁的,所以在多线程下进行类初始化的话可能会引起多个线程阻塞,并且这种阻塞很难被发现

对于初始化阶段,JVM严格规定有且只有在物种情况下,必须对类进行初始化(只有主动去试用类才会初始化类):

  1. 当遇到 new 、 getstatic 、putstatic 或 invokestatic 这四条直接码指令时,比如new一个类,读取一个静态字段(未被final修饰)或调用一个类的静态方法
  2. 使用 java.lang.reflect 包的方法对类进行反射调用时,如 Class.forname(),newInstance() 等等,如果类没有初始化,需要触发其初始化
  3. 初始化一个类,如果其父类还未初始化,则先触发该父类的初始化
  4. 当JVM启动时,用户需要定义一个要执行的主类(包含main方法的那个类),JVM会先初始化这个类
  5. MethodHandle 和 VarHandle 可以看作是轻量级的反射调用机制,而要想使用这 2 个调用, 就必须先使用 findStaticVarHandle 来初始化要调用的类
  • new:创建类的实例对象
  • getstatic:访问类的静态变量(不是静态常量,常量会被加载到运行时常量池)
  • putstatic:给类的静态变量赋值
  • invokestatic:调用类的静态方法

卸载

参考文章JavaGuide/类加载过程.md at master · Snailclimb/JavaGuide · GitHub

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐