一、类加载过程是什么?
类加载过程是类的生命周期的一部分。
类生命周期:类编译—》类加载—》类的使用—》方法区进行垃圾回收
类加载过程:将编译以后的类的二进制数据加载进入内存、然后生成对应的对象的过程。
类加载的最终产品是位于运行时数据区的堆区的Class对象。Class对象封装了类在方法区内的数据结构,并且向Java程序提供了访问类在方法区内的数据结构对的接口。
二、过程步骤
类加载过程包括三部分:
1、加载:加载二进制数据到内存
1)加载就是将.class文件中的二进制数据加载到内存中、放入运行时数据区的方法区。
2)java通过不同的类加载器通过不同的源加载二进制数据:
001.从本地文件系统中加载class文件,也是最常见的方式;
002.从JAR、ZIP或者其他类型的归档文件中提取class文件(例如使用JDBC编程时用到的数据库驱动类就是放在JAR文件中,JVM可以从JAR文件中直接加载所需的class文件);
003.通过网络加载class文件;
004.把一个Java源文件动态的编译为class文件,并执行加载;
3)java类加载器
类加载器是一个用来加载类文件的类。Java源代码通过javac编译器编译成类文件。然后JVM来执行类文件中的字节码来执行程序。类加载器负责加载文件系统、网络或其他来源的类文件。有三种默认使用的类加载器:Bootstrap类加载器、Extension类加载器和System类加载器(或者叫作Application类加载器)。每种类加载器都有设定好从哪里加载类
001.Bootstrap类加载器负责加载rt.jar中的JDK类文件,它是所有类加载器的父加载器。Bootstrap类加载器没有任何父类加载器,如果你调用String.class.getClassLoader(),会返回null,任何基于此的代码会抛出NUllPointerException异常。Bootstrap加载器被称为初始类加载器。
002.而Extension将加载类的请求先委托给它的父加载器,也就是Bootstrap,如果没有成功加载的话,再从jre/lib/ext目录下或者java.ext.dirs系统属性定义的目录下加载类。Extension加载器由sun.misc.Launcher$ExtClassLoader实现。
003.第三种默认的加载器就是System类加载器(又叫作Application类加载器)了。它负责从classpath环境变量中加载某些应用相关的类,classpath环境变量通常由-classpath或-cp命令行选项来定义,或者是JAR中的Manifest的classpath属性。Application类加载器是Extension类加载器的子加载器。通过sun.misc.Launcher$AppClassLoader实现。
类加载器的工作原理基于三个机制:委托、可见性和单一性
委托机制
当一个类加载和初始化的时候,类仅在有需要加载的时候被加载。假设你有一个应用需要的类叫作Abc.class,首先加载这个类的请求由Application类加载器委托给它的父类加载器Extension类加载器,然后再委托给Bootstrap类加载器。Bootstrap类加载器会先看看rt.jar中有没有这个类,因为并没有这个类,所以这个请求由回到Extension类加载器,它会查看jre/lib/ext目录下有没有这个类,如果这个类被Extension类加载器找到了,那么它将被加载,而Application类加载器不会加载这个类;而如果这个类没有被Extension类加载器找到,那么再由Application类加载器从classpath中寻找。记住classpath定义的是类文件的加载目录,而PATH是定义的是可执行程序如javac,java等的执行路径。
可见性机制
根据可见性机制,子类加载器可以看到父类加载器加载的类,而反之则不行。所以下面的例子中,当Abc.class已经被Application类加载器加载过了,然后如果想要使用Extension类加载器加载这个类,将会抛出java.lang.ClassNotFoundException异常。
单一性机制
根据这个机制,父加载器加载过的类不能被子加载器加载第二次。虽然重写违反委托和单一性机制的类加载器是可能的,但这样做并不可取。你写自己的类加载器的时候应该严格遵守这三条机制。
4)定义自已的ClassLoader
既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?
因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。
定义自已的类加载器分为两步:
1、继承java.lang.ClassLoader
2、重写父类的findClass方法
读者可能在这里有疑问,父类有那么多方法,为什么偏偏只重写findClass方法?
因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写loadClass搜索类的算法。
2、连接:包括验证、准备、解析
1)验证:保证被加载类的正确性
当类被加载后,就进入验证阶段。连接就是把已经读入到内存中的类的二进制数据合并到JVM运行时环境中去。连接的第一步是类的验证,其目的是保证被加载的类由正确的内部结构,并且与其他类协调一致。如果JVM检查到错误,那么就会抛出相应的Error对象。
疑问:由Java编译器生成的Java类的二进制数据肯定是正确的,为什么还要进行类的验证呢?
因为JVM不知道某个特定的class文件到底是如何创建的、从哪来的,这个class文件可能是由正常的Java编译器生成的,也有可能是恶意创建的(通过恶意class文件破坏JVM运行时环境),类的验证能提高程序的健壮性,确保程序被安全的执行。
类的验证主要包括以下内容:
类文件的结构检查:确保类文件遵从Java类文件的固定格式。
语义检查:确保类本身符合Java语言的语法规定(例如final类型的类没有子类,final类型的方法没有被覆写)。
字节码验证:确保字节码流可以被JVM安全的执行。字节码流代表Java方法(包括静态方法和实例方法),它是由被称作操作码的单元字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。字节码验证步骤会检查每个操作码是否合法,即是否有着合法的操作数。
二进制兼容的检查:确保相互引用的类之间协调一致。例如在A类的的a方法中调用B类的b方法。JVM在验证A类时,会检查在方法区是否存在B类的b方法,如果不存在(当A类和B类的版本不兼容,就会出现这种问题),就会抛出NoSuchMethodError错误。
2)准备:为类的静态变量分配内存,并将其初始化为默认值
public class Demo {
public static int a = 1;
public static long b;
static {
b = 2;
}
}
在准备阶段,将为int类型的静态变量a分配4个字节的内存空间并赋予默认值为0;为long类型的静态变量b分配8个字节的内存空间并赋予默认值0。
3)解析:把类中的符号引用转换成直接引用
在解析阶段,JVM会把二进制数据中的 符号引用替换为直接引用。例如在A类的a方法中调用B
类的b方法。
public class A {
B b = new B();
public void a() {
b.b();//这行代码在A类的二进制数据中表示为符号引用
}
}
在A类的二进制数据中,包含了一个对B类b()方法的符号引用,它由b()方法的全名和相关描述组成。在解析阶段,JVM将这个符号引用替换成为一个指针,该指针指向B类b()方法在方法区内的内存位置,这个指针就是直接引用。
3、初始化
在初始化阶段,JVM执行类初始化语句,为类的静态变量赋予初始值。在程序中,静态变量的初始化有两种途径:一是在静态变量的声明处进行初始化;二是在静态代码块中进行初始化。
如下代码,a和b都被显式的初始化,而c没有没显式的初始化,它将报纸默认值0。
public class A {
private static int a = 1;//在变量声明处初始化
public static long b;
public static long c;
static {
b = 1;//在静态代码块中初始化
}
}