java类加载机制、类加载器、自定义类加载器的案例
作者:康斌825
类加载机制
java类从被加载到JVM到卸载出JVM,整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(using)、和卸载(Unloading)七个阶段。
其中验证、准备和解析三个部分统称为连接(Linking)。
1、加载
加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。
类的加载通过JVM提供的类加载器完成,类加载器是程序运行的基础。程序在启动的时候,并不会一次性加载程序所要用到的所有class文件,而是根据需要,通过java的类加载器机制(classLoader)来动态加载某个class文件到内存中。
jvm在运行时会产生三个classLoader:
启动类加载器(BootStrap ClassLoader):是java类加载层次中最顶层的类加载器,负责加载jdk中的核心类库。由C++实现,不是classLoader的子类。
扩展类加载器(Extension ClassLoader):负责加载java的扩展类库,比如lib/ext或者java.ext.dirs系统属性指定的目录中的jar包。父类加载器为null。
系统类加载器(App ClassLoader):负责加载来自java命令的-classpath选项、java.class.path系统属性所指定的jar包和类路径。程序可以通过classLoader的静态方法getSystemClassLoader(),来获取系统类加载器。由java语言实现,父类加载器为ExtClassLoader。
除了java默认提供的这三个classLoader之外,用户可以根据需要定义自己的classLoader,这些自定义的classLoader都必须继承自java.lang.ClassLoader类。
通过使用不同的类加载器,可以从不同来源加载类的二进制数据。通常有如下几种情况:
从本地文件系统加载class文件,这是绝大部分实例程序的类加载方式。从jar包加载class类,这种方式也很常见。通过网络加载class类把一个java源文件动态编译,并执行加载,比如jsp。
2、连接
当类被加载之后,系统为之生成一个对应的class对象,接着进入连接阶段(验证-准备-解析),连接阶段负责把类的二进制数据合并到jre中。
验证:用于检测被加载的类是否有正确的内部结构,并和其他类协调一致。
包括四种验证:文件格式验证、元数据验证、字节验证和符号引用验证。准备:负责为类变量分配内存,并设置默认初始值。
解析:将类的二进制数据中的变量进行符号引用替换成直接引用。
3、初始化
在初始化阶段,主要为类的静态变量赋予正确的初始值。其实就是执行类构造器<clinit>()方法的过程。
在java类中对类变量指定初始值有两种方式:a.声明类变量时指定初始值;b.使用静态初始化块为类变量指定初始值。
jvm初始化一个类包含如下步骤:
加载并连接该类先初始化其直接父类依次执行初始化语句当执行第2步时,系统对直接父类的初始化也遵循1~3,以此类推。
当一个类被主动引用后会触发初始化过程:
遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
生成这4条指令最常见的Java代码场景是:使用new关键字实例化对象时、读取或者设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)时、以及调用一个类的静态方法的时候。
使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要触发父类的初始化。当虚拟机启动时,用户需要指定一个执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
当使用jdk7+的动态语言支持时,如果java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发器 初始化。
当一个类如果是被动引用的话,不会触发初始化过程:
通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义该字段的类才会被初始化,因此当我们通过子类来引用父类中定义的静态字段时,只会触发父类的初始化,而不会触发子类的初始化。
通过数组定义来引用类,不会触发此类的初始化。
常量在编译阶段会存入调用类的常量池中,本质上没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
4、使用
(略)
5、卸载
如果出现下面的情况,类就会被卸载:
该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。加载该类的ClassLoader已经被回收。
该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
类加载器
类加载器负责加载所有的类。其为所有被载入内存中的类生成一个java.lang.Class实例对象。一旦一个类被加载如JVM中,同一个类就不会被再次载入了。
正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
在Java中,一个类用其全限定类名(包括包名和类名)作为标识;但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
例如,如果在pg的包中有一个名为Person的类,被类加载器ClassLoader的实例kl负责加载,则该Person类对应的Class对象在JVM中表示为(Person.pg.kl)。
这意味着两个类加载器加载的同名类:(Person.pg.kl)和(Person.pg.kl2)是不同的、它们所加载的类也是完全不同、互不兼容的。
前面我们已经介绍了java中的几种类加载器,下面我们用一张图展示他们的层次关系:
类加载步骤
类加载器加载class大致需要如下8个步骤:
检测此Class是否载入过,即在缓冲区中是否有此Class,如果有直接进入第8步,否则进入第2步。
如果没有父类加载器,则要么Parent是根类加载器,要么本身就是根类加载器,则跳到第4步,如果父类加载器存在,则进入第3步。
请求使用父类加载器去载入目标类,如果载入成功则跳至第8步,否则接着执行第5步。
请求使用根类加载器去载入目标类,如果载入成功则跳至第8步,否则跳至第7步。
当前类加载器尝试寻找Class文件,如果找到则执行第6步,如果找不到则执行第7步。
从文件中载入Class,成功后跳至第8步。
抛出ClassNotFountException异常。返回对应的java.lang.Class对象。
类加载机制
全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖和引用其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。
双亲委派:先让父类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父加载器,依次递归,如果父加载器可以完成类加载任务,就成功返回;只有父加载器无法完成此加载任务时,才自己去加载。
缓存机制:保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜寻该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓冲区中。
这就是为很么修改了Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
自定义的类加载器
jvm除跟类加载器之外的所有类加载器都是ClassLoader子类的实例,开发者可以通过拓展ClassLoader的子类,并重写该ClassLoader所包含的方法实现自定义的类加载器。
ClassLoader有如下两个关键方法:
loadClass(String name,boolean resolve):该方法为ClassLoader的入口点,根据指定名称来加载类,系统就是调用ClassLoader的该方法来获取指定类的class对象。
findClass(String name):根据指定名称来查找类如果需要实现自定义的ClassLoader,则可以通过重写以上两个方法来实现,通常推荐重写findClass()方法而不是loadClass()方法。
classLoader()方法的执行步骤:
1)findLoadedClass():来检查是否加载类,如果加载直接返回;
2)父类加载器上调用loadClass()方法。如果父类加载器为null,则使用跟类加载器加载;
3)调用findClass(String)方法查找类。从这边可以看出,重写findClass()方法可以避免覆盖默认类加载器的父类委托,缓冲机制两种策略;如果重写loadClass()方法,则实现逻辑更为复杂。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。如有错误或未考虑完全的地方,望不吝赐教。