Java 类加载相关知识点
本文摘抄自文末的链接中的几篇文章,因为自己写,也写不出什么新意,相关的知识就那些,我也没有什么更加深入的理解,只能摘抄了,哈哈。很早以前就像写一篇类加载相关的文章,但是一些发现网上都有,所以一直没写。今天将网上的文章整理归纳了一下,留作参考。
在JVM中,一个类用其全限定类名和加载该类的类加载器作为其唯一标识。
类加载器
从虚拟机的角度来说,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),该类加载器使用C++语言实现,属于虚拟机自身的一部分。另外一种就是所有其它的类加载器,这些类加载器是由Java语言实现,独立于JVM外部,并且全部继承自抽象类java.lang.ClassLoader。
- Bootstrap ClassLoader:启动类加载器
- Extension ClassLoader:扩展类加载器
- System ClassLoader:应用类加载器
- 启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中,如果名称不符合的类库即使放在lib目录中也不会被加载。该类加载器无法被Java程序直接引用。
- 扩展类加载器(Extension ClassLoader):该加载器主要是负责加载JAVA_HOME\lib\ext目录中的所有类库,该加载器可以被开发者直接使用。
- 应用程序类加载器(Application ClassLoader):该类加载器也称为系统类加载器,它负责加载用户类路径(Classpath)上所指定的类库,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
- 我们还可以继承ClassLoader来实现自定义类加载器。
双亲委派模型
该模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。子类加载器和父类加载器不是以继承的关系来实现,而是通过组合关系来复用父加载器的代码。
双亲委派模型的工作过程为:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的加载器都是如此,因此所有的类加载请求都会传给顶层的启动类加载器,只有当父加载器反馈自己无法完成该加载请求(该加载器的搜索范围中没有找到对应的类)时,子加载器才会尝试自己去加载。
使用这种模型来组织类加载器之间的关系的好处是Java类和它的类加载器一起具备了一种带有优先级的层次关系。例如java.lang.Object类,无论哪个类加载器去加载该类,最终都是由启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。如果不使用该模型的话,如果用户自定义一个java.lang.Object类且存放在classpath中,那么应用类加载器会加载这个类,那么系统中将会出现多个类加载器不同但是类名都是Object的类,应用程序也会变得很混乱。
类加载器结构体系.png类加载过程
类加载过程.jpg在Java中,类加载器把一个类加载到JVM中,要经过以下步骤:
-
加载:查找和导入Class文件;
-
链接:把类的二进制数据合并到JRE中;
2.1 校验:检查载入Class文件数据的正确性;
2.2 准备:给类的静态变量分配存储空间;
2.3 解析:将符号引用转成直接引用;
-
初始化:对类的静态变量,静态代码块执行初始化操作。
加载
类的加载指的是将类的class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供了访问方法区内的数据结构的接口。
加载class文件的方式
- 从本地系统中直接加载
- 通过网络下载.class文件
- 从zip,jar等归档文件中加载.class文件
- 从专有数据库中提取.class文件
- 将Java源文件动态编译为.class文件
验证
验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:文件格式的验证、元数据的验证、字节码验证和符号引用验证。
准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中进行分配。
注意:
- 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。
- 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值。
解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号引用可以是任何形式的字面量,符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经在内存中。
举个例子
public class TestMain {
private static int age;
private double height;
public static void main(String[] args) {
}
}
使 javap -v 命令反编译这个类的class文件。
javap -v TestMain
截取部分
Compiled from "TestMain.java"
public class com.hm.classloader.TestMain
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#21 // java/lang/Object."<init>":()V
#2 = Class #22 // com/hm/classloader/TestMain
#3 = Class #23 // java/lang/Object
#4 = Utf8 age
#5 = Utf8 I
#6 = Utf8 height
#7 = Utf8 D
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
//...
看到Constant Pool也就是常量池中都是符号引用(这一点有待考证)。比如#2,表示类的全限定类名com/hm/classloader/TestMain,比如#4为age,#5为I,#4和#5是一对表示一个int类型的变量,变量名是age。
直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般都不相同,如果有了直接引用,那引用的目标必定已经在内存中存在。
初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码。初始化,为类的静态变量赋予正确的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
- 声明类变量时指定初始值;
- 使用静态代码块为类变量指定初始值;
参考链接