1.类加载概述
Java类加载器:是Java运行时环境的一部分,负责动态加载Java类到Java虚拟机的内存中
虚拟机类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的java类型
2.类加载过程
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载7个阶段。其中验证、准备、解析三个部分统称为连接
第一步:加载
“加载”是虚拟机类加载的第一个阶段,在这个阶段中,虚拟机要完成3件事情:
1.通过权限定名,获取类的二进制字节流
2.将字节流的静态数据结构转化为方法区的运行时数据结构
3.在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区中这个类数据结构的访问入口
第二步:验证
“验证”是连接阶段的第一步,这一阶段是为了确保 Class 文件内的字节流格式符合Java虚拟机要求,不会危害虚拟机安全
验证总体上分为4个阶段: 文件格式验证、元数据验证、字节码验证、符号引用验证
第三步:准备
“准备”阶段是正式为类变量(仅仅是类变量,即 static 修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行
第四步:解析
“解析“阶段是虚拟机将常量池内的符号引用替换为直接引用的过程
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能简介定位到的目标的句柄。直接引用是和虚拟机实现的内存布局有关的。如果有了直接引用,那引用的目标必定已经在内存中存在了
第五步:初始化
类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正的开始执行类中定义的java程序代码了(或者说是字节码)
参考文章:
1.https://www.cnblogs.com/htyj/p/11673053.html
2.https://www.cnblogs.com/linghu-java/p/8551360.html
3.https://www.cnblogs.com/fsmly/p/10394972.html
4.https://www.cnblogs.com/cheng21553516/p/11261647.html
5.https://www.cnblogs.com/luohanguo/p/9469851.html
3.类加载器
类的加载是由类加载器完成的
类加载器有两种:
(1)Java虚拟机自带的类加载器,分别为启动类加载器,扩展类加载器和系统类加载器
(2)用户自定义的类加载器,是Java.lang.ClassLoader的子例实例
1.启动类加载器(Bootstrap)
启动类加载器是最底层的类加载器,是虚拟机的一部分,它是由C++语言实现的,且没有父加载器,也没有继承Java.lang.ClassLoader类。
它主要负责加载由系统属性“sun.boot.path”指定的路径下的核心类库(即<JAVA_HOME>\jre\lib),出于安全考虑,启动类加载器只加载java,javax,sun开头的类
2.扩展类加载器(Extension)
扩展类加载器是指由原Sun公司实现的sun.misc.Launcher$ExtClassLoader类(每个版本的JDK不一样),它是由Java语言编写,父加载器是启动类加载器。
负责加载<JAVA_HOME>\jre\lib\ext目录下的类库或者系统变量"java.ext.dirs"指定的目录下的类库
3.系统类加载器(System)
系统类加载器也称之为应用类加载器,是纯Java类,是原SUN公司实现的sun.misc.Launcher$AppClassLoader类(每个版本的JDK不一样)
它的父类加载器是扩展类加载器
负责从classpath环境变量或者系统属性java.class.path所指定的目录中加载类
它是用户自定义的类加载器的默认父加载类
一般情况下,改类加载器是程序中默认的类加载器,可以通过ClassLoader.getSystemClassLoader()方法直接获取
4.类加载器小结
在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的,同时我们还可以自定义类加载器,需要注意的是,Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它一种任务委派模式
4.类加载器的双亲委派机制
双亲委派机制工作原理:如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载
即每个儿子都很懒,每次有活就丢给父亲去干,直到父亲说这件事我也干不了时,儿子自己才想办法去完成(坑爹模式)
双亲委派机制优势:
(1)可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次
(2)考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Object,而直接返回已加载过的Object.class,这样便可以防止核心API库被随意篡改
5.ClassLoader
所有的类加载器(除启动类加载器)都必须继承Java.lang.ClassLoader。它是一个抽象类,主要的方法如下:
1.loadClass()方法
在ClassLoader的源码中,有一个方法loadClass(String name,boolean resolve),这里就是双亲委托模式的代码实现
从源代码中可以观察到它的执行顺序
注意:只有父类加载器加载不到类时,才会调用findClass方法进行类的查找,所以,在定义自己的类加载器时,不要覆盖掉该方法,而应该覆盖掉findClass方法
2.findClass()方法
在自定义类加载器时,一般需要覆盖这个方法,且ClassLoader中给出了一个默认的错误实现
3.defineClass()方法
protected final class<?> defineclass(String name,byte[] b,int off,int len) throws classFormatError{}
用来将byte字节解析成虚拟机能够识别的Class对象
defineClass()方法通常与findClass()方法一起使用
在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法获取要加载类的字节码,然后调用defineClass()方法生成Class对象
4.resolveClass()方法
连接指定的类。类加载器可以使用此方法来连接类
6.URLClassLoader
在Java.net包中,JDK提供了一个更加易用的类加载器URLClassLoader,它扩展了ClassLoader,能够从本地或者网络上指定的位置加载类
我们可以使用该类作为自定义的类加载器使用
构造方法:
(1)public URLClassLoader(URL[] urls):指定要加载的类所在的URL地址,父类加载器默认为系统类加载器
(2)public URLClassLoader(URL[] urls,ClassLoader parent):指定要加载的类所在的URL地址,并指定父类加载器
案例1:加载磁盘上的类
案例2:加载网络上的类
7.自定义加载器
实现自定义类加载器的步骤:
1.继承ClassLoader
2.重写findClass()方法
案例1:自定义文件类加载器
案例2:自定义网络类加载器
案例3:热部署类加载器
当我们调用loadClass()方法加载类时,会采用双亲委派模式,即如果类已经被加载,就从缓存中获取,不会重新加载
如果同一个class被同一个类加载器多次加载,则会报错。因此,我们要实现热部署让同一个class文件被不同的类加载器重复加载即可
但是不能调用loadClass方法,而应该调用findClass()方法,避开双亲委托模式,从而实现同一个类被多次加载,实现热部署
案例4:线程上下文类加载器
在Java中存在很多的服务提供者接口SPI,是Java提供的一套用来被第三方实现或者扩展的API,这些接口一般由第三方提供实现,常见的SPI由JDBC,JNDI等
这些SPI的接口(比如JDBC中的Java.sql.Driver)属于核心类库,一般存在rt.jar包中,由启动类加载器加载
而第三方实现的代码一般作为依赖jar包存在classpath路径下,由于SPI接口中的代码需要加载具体的第三方实现类并调用其相关方法,SPI的接口类是由启动类加载器加载的,启动类加载器无法直接加载位于classpath下的具体实现类
由于双亲委派模式的存在,启动类加载器也无法反向委托AppClassLoader加载SPI的具体实现类。在这种情况下,Java提供了线程上下文类加载器用于解决以上问题
线程上下文类加载器可以通过Java.lang.Thread的getContextClassLoader()来获取,或者通过setContextClassLoader(ClassLoader cl)来设置线程的上下文类加载器。
如果没有手动设置上下文类加载器,线程将继承其父线程的上下文类加载器,初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的代码可以通过此类加载器来加载类或资源
显然这种加载类的方式破坏了双亲委派模型,但它使得Java类加载器变得更加灵活
8.类的显式与隐式加载
类的加载方式:是指虚拟机将class文件加载到内存的方式
显式加载:是指在Java代码中通过调用ClassLoader加载class对象
比如Class.forName(String name);或者是this.getClass().getClassLoader().loadClass()加载类
隐式加载:指不需要在Java代码中明确调用加载的代码,而是通过虚拟机自动加载到内存中
比如在加载某个class时,该class引用了另外一个类的对象,那么这个对象的字节码文件就会被虚拟机自动加载到内存中