类的加载
类的加载是指将类的.class文件中二进制数据读入到内存中,然后将其放在运行时数据区的方法区
内,然后在内存中创建爱你一个java.lang.Class
对象
规范并没有说明Class对象应该存放在哪,HotSpot虚拟机将其放在方法区中,用来封装类在方法区内的数据结构
加载.class文件的方式
- 从本地系统中直接加载
- 从网络下载.calss文件
- 从zip,jar等归档文件中加载
- 从专有数据库中提取
- 将Java源文件动态编译为.class文件
servlet技术
类加载器
类加载器用来把类加载到Java虚拟机中,从JDK1.2版本开始,类的加载过程采用双亲委托机制
,这种机制能保证Java平台的安全性.
从源码文档中翻译应该称为父类委托模式
类加载器并不需要等到某个类被首次主动使用
时再加载它
- JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了.class文件缺失或者存在错误,类加载器必须在
程序首次主动
使用该类时才报告错误(LinkageError) - 如果一个类一直没有被程序主动使用,那么累加载器就不会报告错误
JVM中的类加载器
根加载器(Bootstrap),
根加载器没有父加载器
,主要负责虚拟机的核心类库,如java.lang.*
等,java.lang.Object
是由根类加载器加载的,根类加载器的实现依赖于底层操作系统
,属于虚拟机实现第一部分,它并没有继承java.lang.ClassLoader类.
启动类加载器是特定于平台的机器指令,它负责开启整个加载过程
启动类加载器还会负责加载JRE正常运行所需的基本组件.其中包括java.util
,java.lang
包中的类
扩展类加载器(Extension)
扩展类加载器的父加载器是根加载器
,从java.ext.dirs
系统属性指定的目录中加载类库,或者再jre\lib\ext
子目录下加载类库,如果把用户创建的JAR文件放在这个目录下,会自动由扩展类加载器
加载,扩展类加载器是纯Java类,是ClassLoader的子类
注意一点的是,拓展类加载器加载的是jar包内的class文件
系统(应用)类加载器(System/Application)
系统类加载器
的父加载器为扩展类加载器
,从环境变量classpath或者系统属性java.class.path
所制定的目录加载类,它是用户自定义的类加载器的默认父加载器,系统类加载器是纯Java类,是ClassLoader的子类
用户自定义的类加载器
除了虚拟机自带的加载器外,用户可以定制自己的类加载器.Java提供了抽象类ClassLoader.所有用户自定义的加载器都应该继承ClassLoader
AppClassLoader和ExtClassLoader都是Java类,所以需要类加载器进行加载,而这两个类的类加载器就是bootstrapClassLoader
可以通过修改
System.getProperty(java.system.class.loader)对默认的SystemClassLoader进行修改
父亲委托机制
在父亲委托机制中,各个加载器按照父子关系形成树形结构,除了根加载器之外,其余的类加载器有且只有一个父加载器.
父亲委托机制简单描述,就是一个类加载器要加载一个类,并不是由自身进行直接加载,而是通过向上寻找父加载器,直到没有父加载器的类加载器,然后再从上至下尝试加载,直至找到一个可以正确加载的类加载器,一般情况下,系统类加载器就能加载普通的类.
并不是所有的类加载器都必须遵守双亲委托的机制,具体实现可以根据需要进行改造
代码示例,查看类的加载器
public class Test08 {
public static void main(String[] args) {
try {
Class<?> clzz = Class.forName("java.lang.String");
//如果返回null,证明是由BootStrap加载器进行加载的
System.out.println(clzz.getClassLoader());
Class<?> customClass = Class.forName("com.r09er.jvm.classloader.Custom");
System.out.println(customClass.getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
class Custom{
}
输出
null
sun.misc.Launcher$AppClassLoader@18b4aac2
String的类加载器为null,证明String是由Bootstrap类加载器加载,因为根加载器是由C++实现.所以会返回null
.
Custom的类加载器是Launcher$AppClassLoader,这个类是不开源的.但是是默认的系统(应用)类加载器
.
classLoader和初始化的时机
通过ClassLoader手动加载类,观察是否会触发类的初始化
public class Test12 {
public static void main(String[] args) throws Exception {
ClassLoader loader = ClassLoader.getSystemClassLoader();
Class<?> aClass = loader.loadClass("com.r09er.jvm.classloader.TestClassLoader");
System.out.println(aClass);
System.out.println("-------");
aClass = Class.forName("com.r09er.jvm.classloader.TestClassLoader");
System.out.println(aClass);
}
}
class TestClassLoader{
static {
System.out.println("Test classloader");
}
}
输出
class com.r09er.jvm.classloader.TestClassLoader
-------
Test classloader
class com.r09er.jvm.classloader.TestClassLoader
结论
明显可以看出,classLoader.load方法加载类,类并不会初始化,说明不是对类的主动使用,调用了Class.ForName
才进行初始化
不同的类加载器与加载动作分析
打印类加载器,由于根加载器由C++编写,所以就会返回null
public static void main(String[] args) {
ClassLoader loader = ClassLoader.getSystemClassLoader();
System.out.println(loader);
//向上遍历父classLoader
while (null != loader) {
loader = loader.getParent();
System.out.println(loader);
}
}
输出结果
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@610455d6
null
获取ClassLoader的途径
- 通过类对象获取ClassLoader,
clazz.getClassLoader()
- 通过线程获取上限文ClassLoader,
Thread.currentThread().getContextLoader()
- 获得系统(应用)ClassLoader,
ClassLoader.getSystemClassLoader()
- 获得调用者的ClassLoader,
DriverManager.getClassLoader()
ClassLoader源码分析
JavaDoc描述
类加载器是负责加载类
的对象,classLoader是抽象类.赋予类一个二进制名称,一个类加载器应当尝试定位
或生成
数据,这些数据构成类的定义.一种典型的策略是将二进制名称转换为文件名,然后从文件系统中读取该名称的字节码文件
。
每一个类
对象都包含定义该类
的classLoader引用(reference)
数组
对应的class对象并不是由类加载器创建的,而是由java虚拟机在需要时自动创建的.对于一个数组的类加载器,与这个数组元素的类加载器一致.如果数组是原生类型
,那这个数组将没有classLoader
String[],则这个数组的类加载器是String的类加载器,使用的是Bootstrap类加载器
int[] ,这种基本类型的数组,是没有类加载器的.
应用
实现classLoader的目的是为了拓展JVM动态加载类
ClassLoader使用了委托模型去寻找类的资源.ClassLoader的每一个实例都有会一个关联的父ClassLoader,当需要寻找一个类的资源时,ClassLoader实例就会委托给父ClassLoader.虚拟机内建的ClassLoader称为BootstrapClassLoader
,BootstrapClassLoader
本身是没有父ClassLoader的,但是可以作为其他ClassLoader的父加载器
支持并发加载的类加载器称为并行类加载器,这种类加载器要求在类初始化期间通过ClassLoader.registerAsParallelCapable
将自身注册上.默认情况下就是并行的,而子类需要需要并行,则需要调用该方法
在委托机制并不是严格层次化的环境下,classLoader需要并行处理,否则类在加载过程中会导致死锁,因为类加载过程中是持有锁的
通常情况下,JVM会从本地的文件系统中加载类,这种加载与平台相关.例如在UNIX系统中,jvm会从环境变量中CLASSPATH定义的目录中加载类.
然而有些类并不是文件,例如网络,或者由应用构建出来(动态代理),这种情况下,defineClass
方法会将字节数组转换为Class实例,可以通过Class.newInstance
创建类真正的对象
由类加载器创建的对象的构造方法和方法,可能会引用其他的类,所以JVM会调用loadClass
方法加载其他引用的类
二进制名称BinaryNames
,作为ClassLoader中方法的String参数提供的任何类名称,都必须是Java语言规范所定义的二进制名称。
例如
- "java.lang.String",全限定类名
- "javax.swing.JSpinner$DefaultEditor",内部类
- "java.security.KeyStoreFileBuilder$1",匿名内部类
- "java.net.URLClassLoader1"
自定义类加载器
步骤
- 1.继承CLassLoader
- 2.重写
loadClass
方法 - 3.在
loadClass
方法中实现加载class字节码的方法,返回byte[] - 4.调用
super.defineClass(byte[])
方法将Class对象返回给loadClass方法
源码示例
public class Test16 extends ClassLoader {
private String classLoaderName;
private String path;
private final String fileExtension = ".class";
public Test16(String classLoaderName) {
//将systemClassLoader作为当前加载器的父加载器
super();
this.classLoaderName = classLoaderName;
}
public Test16(ClassLoader parent, String classLoaderName) {
//将自定义的ClassLoader作为当前加载器的父加载器
super(parent);
this.classLoaderName = classLoaderName;
}
public void setPath(String path) {
this.path = path;
}
public static void main(String[] args) throws Exception {
Test16 loader1 = new Test16("loader1");
//设置绝对路径,加载工程根目录下的com.r09er.jvm.classloader.Test01.class
loader1.setPath("/Users/cicinnus/Documents/sources/jvm-learning/");
Class<?> aClass = loader1.loadClass("com.r09er.jvm.classloader.Test01");
//打印加载的类
System.out.println("loader1 load class" + aClass.hashCode());
Object instance = aClass.newInstance();
System.out.println("instance1: " + instance);
Test16 loader2 = new Test16("loader2");
//设置绝对路径,加载工程根目录下的Test01.class
loader2.setPath("/Users/cicinnus/Documents/sources/jvm-learning/");
Class<?> aClass2 = loader2.loadClass("com.r09er.jvm.classloader.Test01");
System.out.println("loader2 load class" + aClass2.hashCode());
Object instance2 = aClass2.newInstance();
System.out.println("instance2 : " + instance2);
//todo ****
//1.重新编译工程,确保默认的classPath目录下有Test01.class的字节码文件,然后运行main方法,观察输出
//2.删除默认classpath目录下的Test01.class,运行main方法,观察输出
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println("invoke findClass");
System.out.println("class loader name : " + this.classLoaderName);
byte[] bytes = this.loadClassData(name);
return super.defineClass(name, bytes, 0, bytes.length);
}
private byte[] loadClassData(String binaryName) {
byte[] data = null;
binaryName = binaryName.replace(".", "/");
try (
InputStream ins = new FileInputStream(new File(this.path + binaryName + this.fileExtension));
ByteArrayOutputStream baos = new ByteArrayOutputStream();
) {
int ch;
while (-1 != (ch = ins.read())) {
baos.write(ch);
}
data = baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
}
执行两次main方法后,会发现类加载器真正生效的逻辑,因为默认的父加载器其实是系统加载器(AppClassLoader
),所以如果默认的classPath存在字节码文件,则会由AppClassLoader
正确加载类,如果classPath中没有,则会向下使用自定义的类加载器加载类
如果构造函数传入两个不一样的ClassLoaderName,会发现两个class对象并不一致,是由于命名空间NameSpace
的原因,因为两个类加载器定义的名称是不一样的,如果改成相同的名称,则两个class对象一致
重写的是findClass方法,在调用时候,使用的是classLoader的
loadClass
方法,这个方法内部会调用findClass
自定义类加载器加载类流程图还有一个重点,如果将class字节码文件放在根目录,则会抛出
NoClassDefFoundError
异常,因为binaryName
不符合规范.
类加载器重要方法详解
findClass
实现自己的类加载器,最重要就是实现findClass,通过传入binaryName
,将二进制名称加载成一个Class对象
defineClass
在实现findClass
后,需要通过defineClass方法,将二进制数据交给defineClass
方法转换成一个Class实例,
在defineClass
内部会做一些保护和检验工作.
双亲委派机制解析
通过loadClass
方法加载类,会有如下默认加载顺序
- 1.调用
findLoadedClass
方法检查class是否被加载 - 2.调用父加载器的
loadClass
方法,如果父加载器为null,则会调用JVM内建的类加载器. - 3.调用
findClass
方法找到类
在默认的loadClass方法中,类加载是同步
的
双亲委派机制优点
- 1.可以确保Java核心类库的类型安全,如果这个加载过程由Java应用自己的类加载器完成,很可能会在JVM中存在多个版本的
同一个类(包名,类名一致)
,
命名空间发挥的作用
- 2.可以确保Java核心类库提供的类不会被自定义的类替代
因为优先加载的是类库中的class,会忽略掉自定义的类
- 3.不同的类加载器可以为相同名称(binaryName)的类创建额外的命名空间,相同名称的类可以并存在JVM中.
类的卸载
当类被加载,连接,初始化之后,它的生命周期就开始了.当代表类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在元空间内的数据也会被卸载,从而结束类的生命周期.
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期
由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载.
用户自定义的类加载器所加载的类是可以被卸载的
类加载器加载的类路径
BootstrapClassLoader加载的路径
- System.getProperty("sun.boot.class.path")
ExtClassLoader
- System.getProperty("java.ext.dirs")
AppClassLoader
- System.getProperty("java.class.path")
三个路径和JDK版本,操作系统都有关系
如果将编译好的class字节码文件放到根加载器的加载路径上,可以成功由BootstrapClassLoader加载
类加载器命名空间
- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成
即子加载器能访问父加载器加载的类,而父加载器不能访问子加载器加载的类.(类似于继承的概念)
- 在同一个命名空间中,不会出现类的完整名字相同的两个类
一个Java类是由该类的全限定名称+用于加载该类的定义类加载器(defining loader)共同决定.
ClassLoader.getSystemClassLoader
源码
返回用于委托的系统类加载器.是自定义类加载器的父加载器,通常情况下类会被系统类加载器加载.
该方法在程序运很早的时间就会被创建,并且会将系统类加载器设为调用线程的上下文类加载器(context class loader
)
Launcher构造主要逻辑
1.初始化ExtClassLoader
2.初始化AppClassLoader,将初始化好的ExtClassLoader设置为AppClassLoader的父加载器
3.将AppClassLoader设置为当前线程的上下文类加载器
SystemClassLoaderAction
逻辑
1.判断System.getProperty("java.system.class.loader")
是否有设置系统类加载器
2.如果为空,直接返回AppClassLoader
3.如果不为空,通过反射创建classLoader,其中必须提供一个函数签名为ClassLoader
的构造
4.将反射创建的自定义类加载器设置为上限为加载器.
5.返回创建好的类加载器
Class.ForName(name,initialize,classloader)
解析
-
name
,需要构造的类全限定名称(binaryName)
不能用于原生类型或者void类型
如果表示的是数组,则会加载数组中的元素class对象,但是不进行初始化
-
initialize
,类是否需要初始化 -
classloader
,加载此类的类加载器
线程上下文加载器(ContextClassLoader
)实现与分析
CurrentClassLoader(当前类加载器)
- 每一个类都会尝试使用自己的
ClassLoader
去加载当前类引用的其他类
如果ClassA引用了ClassY,那么ClassA的类加载器会去加载ClassY,前提是ClassY未被加载
线程类加载器从JDK1.2开始引入,Thread类中的getContextClassLoader
和setContextClassLoader
分别用来获取和设置上下文加载器.如果没有手动进行设置,那么线程会继承其父线程的上下文加载器.
java应用运行时的初始线程的上下文类加载器是系统类加载器(AppClassLoader),在线程中运行的类可以通过这个类加载器加载类与资源
由JDBC引出的问题
回顾一下JDBC操作
Class.forName("com.mysql.driver.Driver");
Connection conn = Driver.connect();
Statement stae = conn.getStatement();
Driver
,Connection
,Statement
都是由JDK提供的标准,而实现是由具体的DB厂商提供.
根据类加载的机制,JDK的rt包会被BootstrapClassLoader
加载,而自定义的类会被AppClassLoader
加载,同时因为命名空间
的原因,父加载器是无法访问子加载器加载的类的.所以父加载器会导致这个问题.
上下文加载器就是为了解决这种问题所存在的
父ClassLaoder可以使用当前线程Thread.currentThread().getContextClassLoader()
加载的类,
这就改变了父ClassLoader不能使用子ClassLoader或是其他没有直接父子关系的ClassLoader无法访问对方加载的class问题.
即改变了父亲委托模型
线程上下文加载器一般使用
使用步骤(获取 - 使用 - 还原)
- Thread.currentThread().getContextClassLoader()
- Thread.currentThread().setContextClassLoader(targetClassLoader)
doSomentthing();
3.Thread.currentThread().setContextClassLoader(originClassLoader);
ContextClassLoader的作用就是破坏Java的类加载委托机制
ServiceLoader
ServiceLoader
是一个简单的服务提供者加载设施
加载基于JDK规范接口实现的具体实现类
实现类需要提供无参构造,用于反射构造出示例对象
服务提供者将配置文件放到资源目录的META-INF/services
目录下,告诉JDK在此目录的文件内配置了需要加载的类,其中文件名称是需要加载的接口全限定名称,文件内容是一个或多个实现的类全限定名称.
总结
在双亲委托模型下,类加载时由下至上的.但是对于SPI
机制来说,有些接口是由Java核心库提供的,根据类加载的机制,JDK的rt包会被BootstrapClassLoader
加载,而自定义的类会被AppClassLoader
加载.这样传统的双亲委托模型就不能满足SPI
的情况,就可以通过线程上下文加载器来实现对于接口实现类的加载.
网友评论