JVM类加载器
1、概念
类加载阶段,通过一个类的全限定名来获取描述该类的二进制流文件,实现这个动作的代码就是类加载器。
2、类与类加载器
对于任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性。每个类加载器都有独立的类名称空间。通俗的说如果要比较两个类是否相同,必须在同一类加载器的前提下,不然没有意义。
3、双亲委派模型
基于JDK8及之前的版本,三个系统提供的类加载器:
- 启动类加载器(Bootstrap Class Loader):负载将存放在<JAVA_HOME>\lib目录下,或者-Xbootstrapath参数所指定路径中存放,而且是虚拟机可以识别的类库加载到虚拟机的内存中。
- 扩展类加载器(Extension Class Loader):负责加载<JAVA_HOME>\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库
- 应用程序类加载器(Application Class Loader): 负责加载用户类路径(classpath)上所有的类库
图7-2中展示的各个类加载器的层次关系,被称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都要有其父类加载器。这里类加载器之间的父子关系不是通过继承来实现,通常使用组合关系来复用父类加载器的代码。
3.1、工作过程
如果一个类加载器收到了类加载的请求,首先它不会自己加载该类,而是请求父类加载器去完成。每个层次的类加载器都是如此,那么所有的请求最终都会传送到顶层加载器。只有当父类加载器反应无法完成这个加载请求,子类加载器才会尝试自己去完成加载。
3.2、好处
一个显而易见的好处就是类随着类加载器一起具备了一种带有优先级的层级关系。比如一个java.lang.Object类,无论哪个类要加载它,最终都会委派给顶层的类加载器,因此Object在各个类加载器中都是同一个类。反之如果没有使用双亲委派模型,用户自己定义一个java.lang.Object类,那么系统中会出现多个不同的Object类,应用程序会变得一片混乱。
3.3、代码实现
/** * Loads the class with the specified <a href="#name">binary name</a>. The * default implementation of this method searches for classes in the * following order: * * <ol> * * <li><p> Invoke {@link #findLoadedClass(String)} to check if the class * has already been loaded. </p></li> * * <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method * on the parent class loader. If the parent is <tt>null</tt> the class * loader built-in to the virtual machine is used, instead. </p></li> * * <li><p> Invoke the {@link #findClass(String)} method to find the * class. </p></li> * * </ol> * * <p> If the class was found using the above steps, and the * <tt>resolve</tt> flag is true, this method will then invoke the {@link * #resolveClass(Class)} method on the resulting <tt>Class</tt> object. * * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link * #findClass(String)}, rather than this method. </p> * * <p> Unless overridden, this method synchronizes on the result of * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method * during the entire class loading process. * * @param name * The <a href="#name">binary name</a> of the class * * @param resolve * If <tt>true</tt> then resolve the class * * @return The resulting <tt>Class</tt> object * * @throws ClassNotFoundException * If the class could not be found */ protected Class<?> loadClass(String name, boolean 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 { c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } 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; } }
整体思路:先检查请求加载的类型是否已经加载过,若没有则调用父类加载器的loadClass方法,所父类加载器为空,则使用默认的启动类加载器加载。假如父类加载器加载失败,抛出ClassNotFoundException,调用自己的findClass()方法进行加载。
3.4、破坏双亲委派模型
典型的两种方法: 1、使用线程上下文类加载器 2、自定义类加载器,重写loadClass方法
- 线程上下文类加载器:一种典型的例子就是JNDI,JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序classpath下的spi代码。线程上下文类加载器就是用来加载这些代码。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型的一般性原则,但也是无可奈何的事情
- 重写loadClass方法:双亲委派模型主要就是通过该方法实现,这个方法可以指定类通过什么类加载器来加载,我们重写这个方法可以自定义使用什么类加载器就等于破坏了双亲委派模型。