目录
一、类加载器
还记得类加载机制吗?类加载机制的各阶段是加载、连接(验证、准备、解析)、初始化、使用、卸载。可参考上篇文章:JVM学习(一):Java类的加载机制 里有详细说明。
1. 什么是类加载器?
把类加载阶段中的"通过一个类的全限定名来获取描述此类的二进制字节流"
这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器
”。
2. 类与类加载器
类加载器虽然只用于实现类的加载动作,但它在Java程序中祈祷的作用却远远不限于类加载阶段。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立啊Java虚拟机中的唯一性。每一个类加载器都拥有一个独立的类名称空间。
比较两个类是否“相等“,只有在这两个类是由同一个类加载器加载的前提下才由意义;否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。
(这里指的“相等”,包括代表类的Class对象的equals()
方法、isAssignableFrom()
方法、isInstance()
方法的返回结果,也包括关键字instanceof
做对象所属关系判定情况。)
- 代码演示:
public class ClassLoaderTest { public static void main(String[] args) throws Exception { ClassLoader loader = new ClassLoader() { @Override public Class> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.lastIndexOf("." + 1) + ".class"; InputStream inputStream = this.getClass().getResourceAsStream(fileName); if (inputStream == null) { return super.loadClass(name); } byte[] bytes = new byte[inputStream.available()]; inputStream.read(bytes); return defineClass(name, bytes, 0, bytes.length); } catch (IOException e) { e.printStackTrace(); throw new ClassNotFoundException(name); } } }; Object obj = loader.loadClass("com.jx.Test1").newInstance(); System.out.println(obj.getClass()); // 打印类名称 System.out.println(obj instanceof com.jx.Test1); // 打印 比较obj对象是否是com.jx.Test1类 } }
- 运行结果:
class com.jx.Test1 false
从示例代码Object obj = loader.loadClass("com.jx.Test1").newInstance(); System.out.println(obj.getClass());
打印结果是com.jx.Test1
,说明通过自定义的类加载器 加载并实例的对象确实是Tes1的类;
但代码System.out.println(obj instanceof com.jx.Test1);
运行输出的结果是false,这是因为在JVM虚拟机中存在另外一个类加载器加载了。
虽然都是来自同一个Class文件,但因为是两个独立的类加载器加载出来的类,在做对象所属类型检测时结果是false。
二、类加载器分类
-
类加载器可以分为:
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 应用程序类加载器(Application ClassLoader)
- 自定义类加载器(USer ClassLoader)
他们的关系是:
自定义类加载器的父类是应用程序类加载器;
应用程序类加载器的父类是扩展类加载器;
启动类加载器严格意义上不是扩展类加载器的父类,抽象维度可以理解为父类。
1. 启动类加载器(Bootstrap ClassLoader)
启动类加载器(Bootstrap ClassLoader) 是最顶层的类加载器,主要加载核心类库。
- 加载路径
\jdk\jre\lib
下的rt.jar、resource.jar、charsets.jar和class等。 - 启动类架子啊其是无法被Java程序直接引用的。
Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层是由C++编写嵌入到JVM内核中;
当JVM启动后 Bootstrap ClassLoader也随着启动,赋值加载完核心类库后,并构造Extension ClassLoader和Application ClassLoader。
如图:
另外,可以通过启动JVM时指定-Xbootclasspath
路径来改变Bootstrap ClassLoader的加载目录。
2. 扩展类加载器(Extension ClassLoader)
扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Luancher&ExtClassLoader
实现。
- 负责加载
\jre\lib\ext
目录下的jar包和class文件. - 或者由
java.ext.dirs
系统变量指定路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
如图:
3. 应用程序类加载器(Application ClassLoader)
应用程序类加载器(Application ClassLoader):是由sun.misc.Launcher&ApplicationClassLoaer
实现。
- Application ClassLoader是负责加载
用户类路径上
所指定的类库,开发者可以直接使用这个类加载器,如果应用程序没有自定义过自己的类加载器,一般情况下就是程序默认的类加载器。
4. 自定义类加载器(User ClassLoader)
**自定义类加载器(User ClassLoader)
**:一般是继承ClassLoader,重写findClass方法。
因为JVM自带的ClassLoader只会从本地文件系统加载标准的Java class文件,因此编写自定义类加载器可以做到:
-
- 在执行非自信代码之前,自动验证数字签名。
-
- 动态地创建符合用户特定需要的定制化构建类。
-
- 从特定的场所取得Java class,例如数据库和网络中。
5. 类加载器体系结构(双亲委派模型)
关于类加载器的加载过程:
-
- 称为缓存查找环节。第一步是先检查类加载器中是否已经缓存加载了对应的类。 其中又分为:
- ① 若存在自定义类加载器,则先检查自身缓存中是否存在;如果存在则取到。
- ② 如果自定义缓存不存在,委托父类查找,也就是应用程序类加载器。
应用程序类加载器同样也先检查缓存中是否存在,如果存在则取到。 - ③ 如果应用缓存不存在,则委托它的父类,既是扩展类加载器。
扩展类加载器同样也会先检查缓存中是否存在,如果存在则取到。 - ④ 如果扩展类加载器缓存也不存在,则调用启动类加载器查找。
启动类加载器也是先检查是否已经加载,如果加载,则取到。如果未加载,则进入加载环节。
-
- 加载环节。第二步,在所有类加载器通过缓存都找不到时,则进入类加载环节。类加载环节可分为:
- ① 启动类加载器。启动类加载器在缓存找不到后,会根据它的路径范围
jre\lib\rt.jar
查找加载对应类。如果成功加载,则返回;如果不成功,则进入②。 - ② 扩展类加载器。扩展类加载器在收到启动类加载器未成功的情况下,会根据它的路径访问
jre\lib\ext\*.jar
查找加载对应类。如果成功加载,则返回;如果不成功,则进入③。 - ③ 应用程序类加载器。应用程序类加载器收到扩展类加载器不成功的情况下,会根据它的路劲访问
ClassPath
查找加载对应的类。如果成功加载,则返回;如果不成功,则进入④。 - ④ 自定义类加载器。如果应用程序类加载器在收到应用程序类加载器不成功的情况下,会根据它自定义的路径访问查找加载对应的类。如果成功加载,则返回;如果不从,则抛出ClassNotFoundExcepiton异常。
上面的流程又可以称为是双亲委派模型。
双亲委派模型
6. 类的加载方式
类的加载方式有三种:
-
-
命令行启动应用的时候由JVM初始化加载。
用一张图即可说明。请见下图:
-
-
- 通过
Class.forName()
方法动态加载。
- ① 我们先看测试示例代码:
public class TestClassLoader { public static void main(String[] args) throws ClassNotFoundException { Class.forName("com.jx.Test1");//直接通过Class.forName()来加载类 } }
- ② 接着跟进去查看Class.for()方法的实现。
public static Class> forName(String className) throws ClassNotFoundException { Class> caller = Reflection.getCallerClass(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); // 这里会调用ClassLoader.getClassLoader()方法获得该类的类加载器对象 }
- ③ 再跟进forName0()方法,是一个native方法。
private static native Class> forName0(String name, boolean initialize, ClassLoader loader, Class> caller) throws ClassNotFoundException;
- ④ 上面的native forName0()方法会调用类加载器的
ClassLoader.loadclass()
方法。
通过上面调用栈会发现
Class.forName()
方法本质上最后会调用ClassLoader.loadClass()
方法。 - 通过
-
- 通过
ClassLoader.loadClass()
方法动态加载。
直接上ClassLoader.loadClass()
方法代码,代码的注释已经说明了很清楚了。
public Class> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class> loadClass(String name, boolean resolve) //resolve字段表示是否进行【连接】阶段处理 throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded // 首先,判断该类是否已经加载过了。 Class> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { //如果父类存在 // 如果未加载过,则委派给父类进行加载。 c = parent.loadClass(name, false); } else { // 如果父类不存在,则交给BootstrapClassLoader来加载。 什么时候父类不存在呢?其实就是ExtClassLoader不存在父类的情况。 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader // 如果父类通过缓存+加载都无法找到,并抛出ClassNotFoundException异常时,则捕获异常但不处理。 } if (c == null) { // If still not found, then invoke findClass in order // to find the class. long t1 = System.nanoTime(); // 如果委托的父类们都无法找到该类,则本加载器自己亲自动手去查找。 c = findClass(name); // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
代码中有几个关键调用需要注意:
- ①
Class> c = findLoadedClass(name)
通过缓存查找判断是否存在该类。
进一步查看该方法实现,又调用了native findLoadedClass0方法。protected final Class> findLoadedClass(String name) { if (!checkName(name)) return null; return findLoadedClass0(name); } private native final Class> findLoadedClass0(String name);
- ② 当parent != null时,
c = parent.loadClass(name, false);
。如果父类不为空,则委派给父类的loadClass()方法执行。
当 parent == null是,c = findBootstrapClassOrNull(name);
父类如果为空时,则委派给BootstrapClassLoader来查找。
这里就是双亲委派模型出现了。
- ③ 当在经过父类们缓存查找和加载后,仍然未找到该类,则本加载器会亲自进行查找
c = findClass(name);
。这个方法很关键。
通常情况下,我们自定义的用户类加载器通过继承ClassLoader抽象类后,重写protected Class> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
findClass()
方法是比较的靠谱的。
到这里已经把双亲委派模型讲解了,还顺带讲解了自定义类加载器。
- 通过
三、ClassLoader代码解读-双亲委派模型
通过上面的《通过ClassLoader.loadClass()
方法动态加载》已经将双亲委派模型已经详细讲解了。
部分补充请查看:
JVM学习(二)续1-ClassLoader代码解读-双亲委派模型
四、自定义类加载器详解
请参考另外一篇文章中有详细讲解。
JVM学习(二)续2-自定义类加载器详解