Java类加载器是jvm运行时环境的一个部件,负责将类动态的加载到虚拟机的内存中,类通常是
按需加载
也就是懒加载,由于有了类加载器,Java运行时不需要知道文件与文件系统。
类的生命周期
在Jvm中,一个类的生命周期一共包含: 加载、验证、解析、准备、初始化、使用、销毁这几部分,这个过程可以概括为:加载、解析、初始化和使用这四个阶段。
-
加载:加载主要说的就是将jvm要用到的一些基本资源,以及我们自己要使用的类,通过
类加载器
,加载到jvm虚拟机中,其中包 括 jar,class文件等等,甚至可以通过我们自己实现的自定义类加载器去通过各种方式加载这两种资源,比如网络,数据库等等 -
解析:解析里面做的工作包括
- 验证:先验证这个字节码文件是不是java的可使用的文件,比如是否包含
magic
属性(二进制的字节码文件可以看到开头有cafebabe字样)等等信息进行判断是否符合jvm的要求,避免恶意代码危害到jvm的安全 - 解析:将类中static修饰的类变量进行内存分配,并且设置初始值。
类变量和类一样,在堆内存中进行初始化
- 准备:将常量池中的所有符号引用,替换为直接引用。
符号引用就是字面量,直接引用就是对应所有对象或者值的地址引用,个人理解符号引用应该就是给人看的,虚拟机看不懂
- 验证:先验证这个字节码文件是不是java的可使用的文件,比如是否包含
-
初始化:初始化就是真正的开始要执行Java代码了,初始化其实就是运行类构造器,并不是在jvm启动的时候就将所有的类进行了初始化,初始化时机有很多,其实就是不同的阶段,调用一个类的构造函数的过程
-
使用:这个简单过一下,其实就是可以开始调用了
-
销毁:当使用完毕,没有引用的时候,会被垃圾回收进行清理
初始化时机
- jvm启动的时候,main方法,就是唯一一个主动会调用的初始化静态方法
- new 一个对象的时候,会触发类的初始化构造函数调用
- 当运行过程中,遇到使用静态方法调用了某个类方法,如果这个类还没被初始化,会调用这个类的静态构造函数初始化方法
- 同上,静态字段也是一样
- 如果接口是被default修饰,直接或者间接实现这个接口的类,初始化的时候,会触发该类的初始化
- 子类的初始化会触发父类的初始化,会先调用父类的构造函数初始化
- 反射调用的类,会被初始化,要么new,要么必须static,都需要初始化
- MethodHandle 实例初次调用的时候,会初始化指定的方法所在的类(这个没有细看过)
不会被初始化的情况
-
子类引用父类的静态变量,只会触发父类的初始化,子类不会进行初始化
-
定义数组(
static
)对象,不会触发该类的初始化 -
在类的准备阶段,会将类的常量存入常量池,但是常量因为没有像static一样,被类进行调用,所以不会触发类的初始化
-
通过类名获取类的Class对象,不会触发类的初始化
-
通过Class.forName加载类的时候,可以通过initialize参数,告诉Jvm是否初始化这个类,默认为true,false是不初始化,这个可以自己去定义静态构造函数,输出打印进行测试
-
Classloader 默认的loadClass方法,也不会触发类的初始化,只做加载
类加载器的分类
类加载器一共分为三类,启动类加载器,扩展类加载器,应用类加载器
具体每个类加载加载了哪些内容,可以通过代码直接获取,细节的话可以自行去google或者百度一下
启动类加载器(BootstrapClassLoader)
启动类加载器是由C++定义实现,在jvm启动过程中,加载了jdk基本操作的jar包,我们最常用的rt.jar包就是通过这个加载到Jvm中的
扩展类加载器(ExtClassLoader)
扩展类加载器,主要是加载一些引用的jar包,默认jdk会从jdk工作目录的
jre/lib/ext/
这个文件夹中加载所有的扩展jar包,我们有使用一些第三方jar文件可以通过将jar放到这个目录下进行加载,当然也有很多其他方式,比如设置参数指定,甚至手动加载扩展类自定义加载的几种方式
1、放到 JDK 的 lib/ext 下,或者-Djava.ext.dirs
2、 java –cp/classpath 或者 class 文件放到当前路径
3、自定义 ClassLoader 加载
4、拿到当前执行类的 ClassLoader,反射调用 addUrl 方法添加 Jar 或路径(JDK9 无效)。
应用类加载器(AppClassLoader)
这个是我们最长接触的一个类加载器,这个加载器的作用就是用来加载我们自己写的类,一般我们会通过获取当前类的类加载器,比如
*.class.getClassLoader()
,拿到的这个类型其实就是应用类加载器,可以应用类加载器的父类是扩展类加载器,可以通过getParent
方式获取,扩展类加载的父类是启动类加载器,但是启动类加载器不是Java定义的,所以我们拿不到自定义类加载器
当然我们也可以自定义一个自己的加载器,可以去扩展类加载的方式,java提供的方式是让我们继承
ClassLoader
这个抽象类去实现,可以扩展他的loadClass
方法,去实现自己的类加载方式,加载之后,可以通过它提供的方法,将类加载到虚拟机中。自定义的类加载器虽然他们都有同一个父类是AppClassLoader,但是他们是完全隔离开的,两个不同的容器
类加载器的特性
双亲委派
比如rt中的java.lang.Object这个类,如果我们用多个类加载器去加载的时候,会加载多个相同的类,双亲委派机制,可以避免这个问题,只会由启动类加载器,只加载一次。保证全局唯一
-
先检查需要加载的类是否已经被加载过
-
如果父类存在,就让自己的父类去加载
-
如果父类加载失败,就交给应用类加载器加载(AppClassLoader)
-
如果应用类加载器加载失败,那么自己捕获异常,自己来加载
spi机制对双亲委派的破坏
spi简单说的话,就是一个插件加载机制,比如jdbc驱动,rt包中定义了一个Driver接口,用于插件包扩展数据库驱动的实现,但是启动类加载器加载这个Driver接口的时候,根本不知道自己的接口被哪些插件实现了,无法去加载这些插件包,所以在自己的
DriverManager.getConnection
方法被调用的时候,只能通过线程去拿当前线程的classloader去通过应用类加载器去加载对应的驱动Class.forName("com.mysql.jdbc.Driver"); Connection connection = DriverManager.getConnection(""); 复制代码
ps: 反射中Class.forName和ClassLoader的区别
Class.forName是会加载并且初始化类
ClassLoader只会加载,不会初始化
总结
文章主要介绍了类加载的过程以及类加载器的作用还有特性,我们通过对类加载器了解的更深刻,可以了解我们整个jvm启动到运行加载类的过程,可以定义我们自己的加载器,自己的加载方式,甚至我们可以对整个代码模块做拆分,用自己的类加载器做隔离等等