类的加载是指将类的.class文件中二进制数据读入到内存中,然后将其放在运行时数据区的 方法区
内,然后在内存中创建爱你一个 java.lang.Class
对象
规范并没有说明Class对象应该存放在哪,HotSpot虚拟机将其放在方法区中,用来封装类在方法区内的数据结构
servlet技术
类加载器用来把类加载到Java虚拟机中,从JDK1.2版本开始,类的加载过程采用 双亲委托机制
,这种机制能保证Java平台的安全性.
从源码文档中翻译应该称为父类委托模式
类加载器并不需要等到某个类被 首次主动使用
时再加载它
程序首次主动
根加载器 没有父加载器
,主要负责虚拟机的核心类库,如 java.lang.*
等, java.lang.Object
是由根类加载器加载的,根类加载器的实现 依赖于底层操作系统
,属于虚拟机实现第一部分,它并没有继承java.lang.ClassLoader类. 启动类加载器是特定于平台的机器指令,它负责开启整个加载过程 启动类加载器还会负责加载JRE正常运行所需的基本组件.其中包括 java.util
, java.lang
包中的类
扩展类加载器的父加载器是 根加载器
,从 java.ext.dirs
系统属性指定的目录中加载类库,或者再 jre/lib/ext
子目录下加载类库,如果把用户创建的JAR文件放在这个目录下,会自动由 扩展类加载器
加载,扩展类加载器是纯Java类,是ClassLoader的子类
注意一点的是,拓展类加载器加载的是jar包内的class文件
系统类加载器
的父加载器为 扩展类加载器
,从环境变量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手动加载类,观察是否会触发类的初始化
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 复制代码
clazz.getClassLoader() Thread.currentThread().getContextLoader() ClassLoader.getSystemClassLoader() DriverManager.getClassLoader()
类加载器是负责加载 类
的对象,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语言规范所定义的二进制名称。 例如
步骤
loadClass loadClass super.defineClass(byte[])
源码示例
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; } } 复制代码
AppClassLoader
),所以如果默认的classPath存在字节码文件,则会由 AppClassLoader
正确加载类,如果classPath中没有,则会向下使用自定义的类加载器加载类 NameSpace
的原因,因为两个类加载器定义的名称是不一样的,如果改成相同的名称,则两个class对象一致 重写的是findClass方法,在调用时候,使用的是classLoader的 loadClass
方法,这个方法内部会调用 findClass
还有一个重点,如果将class字节码文件放在根目录,则会抛出 NoClassDefFoundError
异常,因为 binaryName
不符合规范.
实现自己的类加载器,最重要就是实现findClass,通过传入 binaryName
,将二进制名称加载成一个Class对象
在实现 findClass
后,需要通过defineClass方法,将二进制数据交给 defineClass
方法转换成一个Class实例, 在 defineClass
内部会做一些保护和检验工作.
通过 loadClass
方法加载类,会有如下默认加载顺序
findLoadedClass loadClass findClass
同步
的 同一个类(包名,类名一致)
, 命名空间发挥的作用
因为优先加载的是类库中的class,会忽略掉自定义的类
当类被加载,连接,初始化之后,它的生命周期就开始了.当代表类的Class对象不再被引用,即不可触及时,Class对象就会结束生命周期,类在元空间内的数据也会被卸载,从而结束类的生命周期.
一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期
由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载.
用户自定义的类加载器所加载的类是可以被卸载的
BootstrapClassLoader加载的路径
ExtClassLoader
AppClassLoader
三个路径和JDK版本,操作系统都有关系
如果将编译好的class字节码文件放到根加载器的加载路径上,可以成功由BootstrapClassLoader加载
即子加载器能访问父加载器加载的类,而父加载器不能访问子加载器加载的类.(类似于继承的概念)
ClassLoader.getSystemClassLoader
源码 返回用于委托的系统类加载器.是自定义类加载器的父加载器,通常情况下类会被系统类加载器加载. 该方法在程序运很早的时间就会被创建,并且会将系统类加载器设为调用线程的 上下文类加载器 ( context class loader
)
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操作
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问题.
即改变了父亲委托模型
使用步骤(获取 - 使用 - 还原)
ServiceLoader
是一个简单的服务提供者加载设施
加载基于JDK规范接口实现的具体实现类 实现类需要提供无参构造,用于反射构造出示例对象
服务提供者将配置文件放到资源目录的 META-INF/services
目录下,告诉JDK在此目录的文件内配置了需要加载的类,其中文件名称是需要加载的接口全限定名称,文件内容是一个或多个实现的类全限定名称.
在双亲委托模型下,类加载时由下至上的.但是对于 SPI
机制来说,有些接口是由Java核心库提供的,根据类加载的机制,JDK的rt包会被 BootstrapClassLoader
加载,而自定义的类会被 AppClassLoader
加载.这样传统的双亲委托模型就不能满足 SPI
的情况,就可以通过线程上下文加载器来实现对于接口实现类的加载.