JAVA 类加载

什么是类加载器?

类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java 类的虚拟机使用 Java 方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。

类加载器是怎么工作的?

类加载器是java运行时环境的一部分,负责动态的加载java类到虚拟机的内存中,类加载器通常是按需加载,即第一次使用时加载,由于有了类加载器,java运行时不需要知道文件或文件系统。

想要更好的理解类加载器的工作原理,就先需要理解委派概念。

每个Java类必须由某个类加载器加载到内存

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:

  • 引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader
  • 扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

除了引导类加载器外,其他的加载器都有父类,可以使用getParent()方法获取父类加载器。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器


类加载器的代理模式

类加载器在尝试自己去查找某个类的字节代码并定义它时,会先代理给其父类加载器,由父类加载器先去尝试加载这个类,依次类推。在介绍代理模式背后的动机之前,首先需要说明一下 Java 虚拟机是如何判定两个 Java 类是相同的。Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。

双亲委派模型:

  • 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
  • 每一个层次的类加载器都是如此。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。
  • 只有当父加载器反馈自己无法完成这个加载请求时(搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。

代理模式是为了保证 Java 核心库的类型安全。所有 Java 应用都至少需要引用 java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到 Java 虚拟机中。如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object类,而且这些类之间是不兼容的。通过代理模式,对于 Java 核心库的类的加载工作由引导类加载器来统一完成,保证了 Java 应用所使用的都是同一个版本的 Java 核心库的类,是互相兼容的。

加载类的过程

加载阶段

类的加载阶段是将class文件中的二进制数据读取到内存中,然后将该字节流所代表的静态存储结构转化为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.class对象,作为方法区数据结构的入口。

类加载阶段的最终产物的堆内存中的class对象,对于同一个Classloader对象,不管某各类被加载多少次,对应堆内存中的class对象始终只有一个。

类加载阶段发生在连接阶段之前,但连接阶段不必等加载阶段结束才开始,可以交叉工作。

连接阶段

类的连接阶段包括三个小的过程:分别是验证、准备和解析。

(1)验证

验证在连接阶段中的主要目地是确保class文件的字节流所包含的内容符合JVM规范,并且不会出现危害JVM自身安全的代码。但验证不符合要求时,会抛出VerifyError这样的异常后其子异常。

主要验证内容有:

验证文件格式:包括文件头部的魔术因子、class文件主次版本号、class文件的MD5指纹、变量类型是否支持等

元数据的验证:对class的字节流进行语义分析,判断是否符合JVM规范。简单来说就是java语法的正确性

字符码验证:主要验证程序的控制流程,如循环、分支等

符号引用验证:验证符号引用转换为直接引用时的合法性,保证解析动作的顺利执行。比如不能访问引用类的私有方法、全限定名称是否能找到相关的类。

(2)准备

准备阶段主要做的事就是在方法区为静态变量发配内存以及赋初始默认值(区别于初始化阶段赋的程序指定的真实值)

注意:final修饰的静态常量在编译阶段就已经赋值,不会导致类的初始化,是一种被动引用,因此也不存在连接阶段。

(3)解析

解析就是在常量池中寻找类、接口、字段和方法的符号引用,并且将这些符号引用替换成直接引用的过程。

解析过程主要针对类接口、字段、类方法和接口方法四类进行。

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注