Java类加载原理解析 by CSDN-书呆子Rico
双亲委派模型与线程上下文类加载器 by CSDN-书呆子Rico
类文件加载的过程 by CSDN-lifes_java
jvm加载类机制CLass Loading by CSDN-tangdong3415
类加载器 启动(Bootstrap)类加载器 负责将JDK目录(/lib/)下的核心类库(或虚拟机识别的类库加载到内存中) 由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用。 扩展(Extension)类加载器 由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现 负责将JDK扩展目录(/lib/ext/)下的扩展类库 系统(System)类加载器 由 Sun 的 AppClassLoader(sun.misc.Launcher$ExtClassLoader)实现 负责将用户的类库加载到内存中 双亲(parent)委派(delegation)机制 某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器 实质是,递归的调用ClassLoader.loadClass() ExtClassLoader、ExtClassLoader均间接的继承自ClassLoader 系统类加载器的父加载器是标准扩展类加载器 但是我们试图获取标准扩展类加载器的父类加载器时却得到了null 由于启动类加载器无法被Java程序直接引用 JVM默认直接使用 null 代表启动类加载器 JAVA程序动态扩展方式 1.反射获取Class对象时,指定JVM预声明的类加载器 2.使用用户自定义的类处理器加载类 JVM类加载机制(与Java.lang.ClassLoader.loadClass()的逻辑挂钩) 双亲委派制 默认 java.lang.Class forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException 这里的参数initialize,表示加载时是否需要初始化 像JDBC驱动程序使用前就需要被初始化(预处理其中静态的、非编译的内容) forName(String name,[true,ClassLoader.getCallerClassLoader()]) 默认加载时初始化 默认优先调用 主调类 的类加载器 java.lang.ClassLoader loadClass() //用于作为类加载的入口 该类加载器是否存在父级类加载器?委派父..加载:findClass() findClass(String 类全限定名) //通过类全限定名找到这个.class->读取成二进制字节流->调用defindClass() 自定义的类加载器可以通过覆写这个方法,读取磁盘中的字节码文件 final defineClass() //将类的二进制字节流转换到JVM的方法区->JVM堆中生类的java.lang.Class实例,并缓存起来 注意:是这个类的Class实例,用于封存这个类的属性的 注意不要将自定义类型的字节码放置到系统路径或者扩展路径中,否则会被系统类加载器或扩展类加载器抢先加载 将当前工程输出目录下的TestBean.class打包进test.jar剪贴到/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件) 系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。 将test.jar拷贝一份到 /lib下,运行测试代码那就是说,放置到 /lib目录下的 TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载 /lib目录下存在的陌生类。换句话说,虚拟机只加载 /lib目录下它可以识别的类。 由不同的类加载器加载的指定类还是相同的类型吗? 返回的类名确实是相同的,equals() 返回false JVM中使用 全限定类名 & 加载该类所使用的类加载器 来限定其命名空间 也就是说,此时,JVM中存在两个同类名的类,他们的内存地址是不同的 //摘自java.lang.ClassLoader.java protected ClassLoader() { SecurityManager security = System.getSecurityManager(); if (security != null) { security.checkCreateClassLoader(); } this.parent = getSystemClassLoader(); //由此可见,自定义的类加载器的父级类加载器为系统类加载器 initialized = true; } //摘自java.lang.ClassLoader.loadClass() -> 默认的 双亲委托机制 的逻辑如下 public Class> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 首先判断该类型是否已经被加载 Class c = findLoadedClass(name); if (c == null) { //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载 try { if (parent != null) { //如果存在父类加载器,就委派给父类加载器加载 c = parent.loadClass(name, false); } else { // 递归终止条件 // 由于启动类加载器无法被Java程序直接引用,因此默认用 null 替代 // parent == null就意味着由启动类加载器尝试加载该类, // 即通过调用 native方法 findBootstrapClass0(String name)加载 c = findBootstrapClass0(name); } } catch (ClassNotFoundException e) { // 如果父类加载器不能完成加载请求时,再调用自身的findClass方法进行类加载,若加载成功,findClass方法返回的是defineClass方法的返回值 // 注意,若自身也加载不了,会产生ClassNotFoundException异常并向上抛出 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } 建议不要去覆写loadClass()的逻辑,很可能导致很多异常,有可能连java.lang.Object都找不到 ------- 补充 JAVA_HOME/lib下面的核心类库或-Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中。 JAVA_HOME /lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中 用户类路径(java -classpath或-Djava.class.path变量所指的目录 SPI Service Provider Interface JDK针对第三方实现类的一种面对服务的、IoC的解决方案 相对于面向接口,更加的可插拔 举个栗子: JDBC第三方实现类库 按照约定的,将需要注册到JDK(DriveManager)的第三方驱动类的实现类的全限定名 存放在jar的meta/services/...下(这个文件,一般叫java.sql.Driver) 那么JDK则提供了一个发现第三方实现类的工具类java.util.ServiceLoader 也就说JDK通过这个SPI机制来将JAVA_HOME路径外的第三方实现类作为classpath,但这将带来另外的问题 启动类加载器只能加载JAVA_HOME中JDK的核心类库,并不能加载SPI的机制引入的第三方类库 故引入了线程上下文的类加载器 线程的上下文类加载器 并不是特定一类类记载器 其实就是,当前线程所在的上下文的类所用的类加载器 java.lang.Thread.get/setContextClassLoader() 这时候,类的加载就不需要由双亲向下委托xxx类加载器来加载, 这时候,类的加载就突破了JDK默认推荐的双亲委托机制,而是依赖于上下文使用的类加载器 举个栗子,常见有:后来JDK的JDBC 4.0的注册第三方驱动不需要显式的使用Class.forName 最后的最后,在ServiceLoader中发现java.lang.Thread.getContextClassLoader()获取了类加载器 第三方实现类按照约定将实现类的驱动类的全限定名存放在指定的目录中 这也应验了SPI机制自动的从线程中获取类加载器,而不像默认的双亲委托机制那样向下的授权一个匹配的类加载器 这使得SPI机制将主动权还给了第三方实现类,是IoC的 Tomcat Tomcat的上下文&这些上下文的类加载器 common:类库可被Tomcat和所有的Web应用程序共同使用; =>CommonClassLoader server:类库可被Tomcat使用,但对所有的Web应用程序都不可见; =>CatalinaClassLoader shared:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见; =>WebAppClassLoader /WebApp/WEB-INF:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见 =>JasperClassLoader WebAppClassLoader、JasperClassLoader存在多个实例(多个Web服务、多个Jsp) 各个类加载器之间可以隔离 热重载Hot Swap 看起来,好像Tomcat遵循着JDK默认的双亲委托机制来使用类加载器 但是,这又将导致新的问题,Tomcat作为Java服务的容器,如果Java Web(像Spring一样)的上下文是在Common、Service中加载 那将无法访问到Web App加载的类 Spring类加载 完美的契合SPI机制,访问Spring的上下文将决定使用那个类加载器 org.springframework.web.context.ContextLoader public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { try { // 创建WebApplicationContext if (this.context == null) { this.context = createWebApplicationContext(servletContext); } // 将其保存到该webapp的servletContext中 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); // 获取线程上下文类加载器,默认为WebAppClassLoader ClassLoader ccl = Thread.currentThread().getContextClassLoader(); // 如果spring的jar包放在每个webapp自己的目录中 // 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader if (ccl == ContextLoader.class.getClassLoader()) { currentContext = this.context; } else if (ccl != null) { // 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来 // 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出 currentContextPerThread.put(ccl, this.context); } return this.context; } catch (RuntimeException ex) { logger.error("Context initialization failed", ex); throw ex; } catch (Error err) { logger.error("Context initialization failed", err); throw err; } }



