jvm类加载器
虚拟机把类加载阶段中通过一个类的全限定名来获取描述此类的二进制字节流这个动作放到虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的代码模块则被称为类加载器。
从java虚拟机角度来说分两种
- 启动类加载器:使用C++实现,是虚拟机一部分。
- 所有其他类加载器:使用java实现,继承抽象类 java.lang.ClassLoader ,独立于虚拟机外部。
从java开发人员角度来说分三种
- 启动类加载器(Bootstrap ClassLoader):负责加载 <JAVA_HOME>\lib或者被 -Xbootclasspath 参数指定的目录中,并且是被虚拟机识别的类库(按文件名识别 *.jar)。启动类加载器无法被java直接调用。
- 扩展类加载器(Extension ClassLoader):这个加载器由 sun.misc.Launcher$ExtClassLoader 实现,负责加载 <JAVA_HOME>\lib\ext 或者被 java.ext.dirs 系统变量所指定的目录中的所有类库,开发者可以直接使用扩展类加载器。
- 应用程序类加载器(Application ClassLoader):这个加载器由 sun.misc.Launcher$AppClassLoader ,这个类加载器是ClassLoader中getSystemClassLoader() 方法的返回值,所以也被称为系统类加载器。负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个加载器,若没有自定义类加载器,则使用系统默认的类加载器。
双亲委派模型
上图所展示的类加载器之间的层次关系,我们称之为类加载器的双亲委派模型。双亲委派模型要求除了顶层启动类加载器外,其余的类加载器都要有自己的父类加载器。这里类加载器之间的父子关系一般不用继承来实现,而是采用组合关系来复用加载器的代码。
双亲委派模型工作原理
如果一个类收到了类加载的请求,他首先不会自己去加载这个类,而是会把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的类加载请求都传送到了启动类加载器,只有当父类加载器无法处理这个类加载请求时,子加载器才会尝试自己加载。
破坏双亲委派模型
双亲委派并不是一个强制性约束模型,而是java推荐的类加载器实现方式,所以他也可以被“破坏”。
第一次破坏
jdk1.2之前,即双亲委派模型出现之前,用户通过继承java.lang.ClassLoader 重写loadClass()方法,然后通过调用类加载器的loadClassInternal()来调用自己的loadClass()方法。
jdk1.2之后,java.lang.ClassLoader 增加了一个protected方法 findClass() ,将类加载的实现逻辑写在里面,然后再loadClass()方法里判断如果父类加载失败则调用findClass().
第二次破坏
涉及到SPI(第三方接口,比如JNDI,JDBC等)代码的加载,由于基础类加载器无法加载第三方所需资源,java提供了线程上下文类加载器,通过这个类加载器去加载SPI代码。
线程上下文类加载器(Context ClassLoader):类Thread中的getContextCLassLoader()与setContextClassLoader(ClassLoader classloader) 分别用来获取和设置上下文类加载器。如果未设置则从父线程继承,如果全局都没有设置,那这个类加载器就是默认的应用程序类加载器。
第三次破坏
为了追求程序的动态性,比如代码热替换,模块热部署等。
OSGI模块化热部署:OSGI是java模块化标准,热部署就是通过自定义类加载器实现,每一个程序模块(Bundle)都有一个自己的类加载器,当需要替换一个Bundle时候,就把Bundle的类加载器一同替换,达到代码热替换。
总结
- 双亲委派机制好处:防止核心库API不被随意篡改、避免类重复加载
- 同一个jvm内,两个相同包名和类名的类对象可以共存,因为他们的类加载器不一样,所以判断两个类对象是否是同一个,除了看包名和类名是否相通之外,还需要看他们的类加载器是否是同一个。