2019独角兽企业重金招聘Python工程师标准>>>
JAVA为我们提供了两种动态加载机制。
第一种是隐式机制。其实new一个对象和调用类的静态方法时,就是隐式机制在工作。
第二种是显示机制。显示的机制又有两种策略
第一种是用public static Class<?> forName(String className)。public static Class<?> forName(String name, boolean initialize, ClassLoader loader),第二种是用java.lang.ClassLoader的loadClass())。
Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载。
其中类加载过程:
1、寻找 jre 目录,寻找jvm.dll,并初始化JVM;
2、产生一个Bootstrap Loader(启动类加载器,用C++实现),在java虚拟机启动的时候会利用这个类加载器来加载 JDK安装目录下的 /JRE/LIB/rt.jar等文件。 通过System.getProperty("sun.boot.class.path")可以查询。
3、Bootstrap Loader自动加载Extended Loader(标准扩展类加载器 ,用java实现),并将其父Loader设为Bootstrap Loader。这个ClassLoader是用来加载java的扩展API的,加载JDK安装目录下的/JRE/LIB/ext目录中的类。可以通过System.getProperty("java.ext.dirs")进行查询。
也可以指定搜索路径: java -Djava.ext.dirs=d:/projects/testproj/classes HelloWorld
4、Bootstrap Loader自动加载AppClass Loader(系统类加载器 用java实现),并将其父Loader设为Extended Loader。这个ClassLoader是用来加载用户机器上CLASSPATH设置目录中的Class的。通过System.getProperty("java.class.path")可以查询。也可以覆盖环境变量: java -cp ./lavasoft/classes HelloWorld
5、最后由AppClass Loader加载HelloWorld类。
以上就是类加载的最一般的过程。
ExtClassLoader和AppClassLoader在JVM启动后,会在JVM中保存一份,并且在程序运行中无法改变其搜索路径。如果想在运行时从其他搜索路径加载类,就要产生新的类加载器。
在JAVA中,一个类用其完全匹配类名(fully qualified class name)作为标识,这里指的完全匹配类名是包名和类名。不过在JVM中一个类是用其全名再附加上一个加载类ClassLoader的实例作为唯一标识。
同一个ClassLoader加载的类文件,只有一个Class实例。
但是,如果同一个类文件被不同的ClassLoader载入,则会有两份不同的ClassLoader实例(前提是着两个类加载器不能用相同的父类加载器)
双亲委托模式:在任何一个自定义ClassLoader加载一个类之前,它都会先委托它的父亲ClassLoader进行加载,只有当父亲ClassLoader无法加载成功后,才会由自己加载。
《特例是线程上下文类加载器,使用线程上下文类加载器, 可以在执行线程中, 抛弃双亲委派加载链模式, 使用线程上下文里的类加载器加载类.
典型的例子有, 通过线程上下文来加载第三方库jndi实现, 而不依赖于双亲委派. 大部分java app服务器(jboss, tomcat..)也是采用contextClassLoader来处理web服务。
以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺 序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。
这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。 》
在JVM加载类的时候,需要经过三个步骤,装载、连接、初始化。装载就是找到相应的class文件,读入JVM,初始化就不用说了,最主要就说说连接。
forName加载的时候就会将Class进行解释和初始化。forName也有另外一个版本的方法,可以设置是否初始化以及设置ClassLoader,在此就不多讲了。 loadClass加载类实际上就是加载的时候并不对该类进行解释,因此也不会初始化该类。
加载过程中会先检查类是否被已加载,检查顺序是自底向上,而加载的顺序是自顶向下,也就是由上层来逐层尝试加载此类。
卸载重载:一个已经加载的类是无法被更新的,如果你试图用同一个ClassLoader再次加载同一个类,就会得到异常(java.lang.LinkageError: duplicate classdefinition),我们只能够重新创建一个新的ClassLoader实例来再次加载新类。至于原来已经加载的类,开发人员不必去管它,因为它可能还有实例正在被使用,只要相关的实例都被内存回收了,那么JVM就会在适当的时候把不会再使用的类卸载。
真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。
《注意事项:如果A是由Bootstrap Loader所载入,这个时候,要加入B,先交给parent进行查询,这时parent为null,交给自己查询,自己又没有,就报错。》
线程上下文类加载器
线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread 中的方法getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl) 方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在javax.xml.parsers 包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces 所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory 类中的newInstance() 方法用来生成一个新的 DocumentBuilderFactory 的实例。这里的实例的真正的类是继承自javax.xml.parsers.DocumentBuilderFactory , 由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl 。 而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到.