类加载器
1.类加载器概述
类加载就是将磁盘上的class文件加载到内存中。虚拟机设计团队把类加载阶段的"通过一个类的全限定名获取描述此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为"类加载器"。类加载器是JVM执行类加载机制的前提。
ClassLoader的作用
1 2 3
| # ClassLoader的作用 ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的。 ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个于目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等操作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于他是否可以运行,则由Execution Engine决定。
|
1 2 3 4 5
| # Class、ClassLoader和实例对象的关系 在下图中: 类模板数据User Class 和ClassLoader之间是相互引用的关系。 在ClassLoader中,记录了加载类的集合,有那些类是由这个ClassLoader加载的。 我们可以通过Class创建实例对象
|

类加载的命名空间
1 2 3 4 5 6
| # 何为类的唯一性 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。
每一个类加载器,都会拥有一个独立的类名称空间: 比较两个类是否相等,只有在这两个类是由同一类加载器加载的前提下才有意义。 否则即使这两个类源自同一Class文件,被同一个虚拟机加载,只要加载他们类加载器不同,那这两个类就必定不相等。
|
1 2 3 4
| # 命名空间 - 每一个类加载器都有自己命名空间,命名空间由该加载器及所有的父加载器锁加载的类组成。 - 在同一命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类,只加载一次类。 - 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类,可以加载多次。
|
类加载机制的特征
双亲委派机制
可见性
1
| 子类加载器可以访问父加载器加载的类型,但是反过来是不允许的。
|
单一性
1 2
| 由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。 但是,类加载器"邻居"间,同一类型仍然可以被加载多次,因为互相并不可见。
|
2.类加载分类
JVM支持两种类型的类加载器,分别是启动类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上讲,自定义类加载器一般指的是程序中由开发人员自定义的一类加载器,但是Java虚拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载结构主要是如下情况:

- 除了顶层的启动类加载器,其余的类加载应当有自己的”父类“加载器。
- 不同类加载看似是继承关系,
实际上是包含关系。在下层加载器中,包含上层加载器的引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class ClassLoader{ ClassLoader parent; public ClassLoader(ClassLoader parent){ this.parent = parent; } }
class ParentClassLoader extends ClassLoader{ public ParentClassLoader(ClassLoader parent){ super(parent); } }
class ChildClassLoader extends ClassLoader{ public ChildClassLoader(ClassLoader parent){ super(parent); } }
|
启动类加载器
这个类加载使用C/C++语言实现,嵌套在JVM内部。
用来加载Java核心库(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。
并不继承自java.lang.ClassLoader,没有所谓的父加载器
出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
启动类加载器还用于去加载扩展类加载器和应用程序类加载器,并指定为他们的父类加载器。
我们去加载这个一个类,去看一下它的ClassLoader
1 2 3 4
| public class Test { public static void main(String[] args) { } }
|

加载ClassLoader

加载自定义类加载器

1 2 3 4 5 6
| public class Test { public static void main(String[] args) { ClassLoader classLoader = Provider.class.getClassLoader(); System.out.println(classLoader); } }
|
扩展类加载器
1 2 3 4 5 6 7
| public class Test { public static void main(String[] args) { ClassLoader classLoader = SunEC.class.getClassLoader(); System.out.println(classLoader); System.out.println(classLoader.getParent()); } }
|

系统类加载器
Java语言编写,由sun.misc.Launcher$AppClassLoader实现
继承于ClassLoader类
父类加载器为启动类加载器
它负责加载环境变量classpath或系统属性java.class.path指定路径下的类库
应用程序中的类加载器默认是系统类加载器。
它是用户自定义类加载器的默认父加载器。
通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器。
1 2 3 4 5 6 7 8 9 10 11
| public class Test { public static void main(String[] args) { ClassLoader classLoader = Test.class.getClassLoader(); System.out.println(classLoader); ClassLoader loader = classLoader; while(loader != null){ System.out.println(loader.getParent()); loader = loader.getParent(); } } }
|

用户自定义加载器

3. ClassLoader源码分析

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| public abstract class ClassLoader {
private static native void registerNatives(); static { registerNatives(); }
private final ClassLoader parent;
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ } protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
protected final Class<?> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { protectionDomain = preDefineClass(name, protectionDomain); String source = defineClassSourceLocation(protectionDomain); Class<?> c = defineClass1(name, b, off, len, protectionDomain, source); postDefineClass(c, protectionDomain); return c; }
@CallerSensitive public final ClassLoader getParent() { if (parent == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(parent, Reflection.getCallerClass()); } return parent; } }
|
4. 双亲委派模型
类加载器用来把来加载到Java虚拟机中,从jdk1.2开始,类加载过程采用双亲委派机制,这种机制能更好地保证Java平台的安全。
什么是双亲委派模型
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个类请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。
实质上:规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。


双亲委派机制的优势和劣势
优势:
源码解释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { }
if (c == null) { long t1 = System.nanoTime(); c = findClass(name);
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } }
|
1 2 3 4 5
| //双亲委派模型就隐藏在第2步和第3步上 1. 先在当前加载器的缓存中查找有无目标类,如果有,直接返回。 2. 判断当前加载器的父加载器是否为空,如果不为空,调用parent.loadClass(name,false) 3. 如果为空,调用引导类加载器加载 4. 如果还没有加载成功,调用findClass(name)接口进行加载,最终会调用defineClass加载目标类
|
1 2 3 4 5 6
| # 问题 如果在自定义的类加载中重写java.lang.ClassLoader.loadClass(String)或java.lang.ClassLoader.loadClass(String,boolean)方法, 抹去其中的双亲委派机制,仅保留上面的第一步和第四步,是不是就能够加载类库了?
不行,因为JDK还为核心类库提供了一层保护机制。不管是自定义的类加载器,还是系统类加载器,最终都必须调用java.lang.ClassLoader.defineClass(String,byte[],int,int,ProtectDomain)方法, 而该方法会执行preDefineClass()接口,该接口中提供了对JDK核心类库的保护。
|
劣势
- 顶层的ClassLoader无法访问到底层的ClassLoader所加载的类
总结
Java虚拟机规范并没有明确要求类加载的加载机制一定要使用双亲委派机制
,只是建议采用这种方式而已。
在Tomcat中,类加载器所采用的加载机制和传统的双亲委派机制有一定的区别,当缺省的类加载器接受到一个类的加载任务时,首先会由它自行加载
,当他加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。