类加载器介绍:
类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。
每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。
java.lang.ClassLoader 类介绍:
基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,
然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class 类的一个实例。
除此之外,ClassLoader 还负责加载 Java 应用所需的资源,如图像文件和配置文件等.
ClassLoader 中与加载类相关的方法:
getParent() | 返回该类加载器的父类加载器。 |
loadClass(String name) | 加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findClass(String name) | 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例。 |
findLoadedClass(String name) | 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。 |
defineClass(String name, byte[] b, int off, int len) | 把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。 |
resolveClass(Class<?> c) | 链接指定的 Java 类。 |
Java 中的类加载器大致可以分成两类:
一类是系统提供的
一类则是由 Java 应用开发人员编写的
系统提供的类加载器主要有下面三个:
1, 引导类加载器(bootstrap classloader)
负责加载核心的Java库,比如位于<JAVA_HOME>/jre/lib 目录下的vm.jar,core.jar。
这个类加载器,是JVM核心部分,是用native代码写成的。
并不继承自 java.lang.ClassLoader。
2, 扩展类加载器(extensions classloader)
负责加载扩展路径下的代码,一般位于<JAVA_HOME>/jre/lib/ext 或者通过java.ext.dirs 这个系统属性指定的路径下的代码。
这个类加载器是由sun.misc.Launcher$ExtClassLoader 实现的。
3, 应用程序类加载器(application classloader)
负责加载java.class.path(映射系统参数 CLASSPATH的值) 路径下面的代码,这个类加载器是由 sun.misc.Launcher$AppClassLoader 实现的。
可以通过 ClassLoader.getSystemClassLoader() 来获取它
java.ext.dirs属性指的是系统属性下的一个key,可以通过System.getProperties()方法获得。
java.class.path属性指的是系统属性下的一个key,可以通过System.getProperties()方法获得。
类加载器之间的关系:父子
引导类加载器
|
扩展类加载器
|
应用程序类加载器
自定义的类加载器的父亲是应用程序类加载器。
类的加载模式:父委托模式
类加载器在加载自己的类之前,先委托加载父类。父类加载器可以是客户化的类加载器或者引导类加载器。
但是有一点很重要,类加载器只能委托自己的父类加载器,而不能是子类加载器(只能向上不能向下)
如果应用程序类加载器需要加载一个类,它首先委托扩展类加载器,扩展类加载器再委托引导类加载器。
如果父类加载器不能加载类,子类加载器就回在自己的库中查找这个类。基于这个特性,类加载器只负责它的祖先无法加载的类。
重点注意:当一个类已经被类加载器加载后,这个类需要的任何其他的新类都必须用同一个类加载器加载他们(或者遵循父委托模式,由父类加载器加载)。
例如:WhichClassLoader2 引用 WhichClassLoader3,当类加载器加载WhichClassLoader2时,由于WhichClassLoader3被引用了,
该类加载器会试着加载WhichClassLoader3。
注意:开发者通常会使用如下语法通过类加载器机制加载属性文件:
Properties p = new Properties();
p.load(MyClass.class.getClassLoader().getResourceAsStream("myApp.properties"));
这个意思是:如果MyClass 由扩展类加载器加载,而 myApp.properties 文件只能应用程序类加载器加载,
否则装入属性文件就会失败。
每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader() 方法就可以获取到此引用。
例如:targetObject.class.getClassLoader();
Java 虚拟机是如何判定两个 Java 类是相同的:
一个类的全名和一个加载类ClassLoader的实例作为唯一标识,
也就是:Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。
只有两者都相同的情况,才认为两个类是相同的。
不同两个类的实例之间是不能相互赋值的。
父委托模式是为了保证 Java 核心库的类型安全,所有 Java 应用都至少需要引用 java.lang.Object 类,
也就是说在运行的时候,java.lang.Object 这个类需要被加载到 Java 虚拟机中。
如果这个加载过程由 Java 应用自己的类加载器来完成的话,很可能就存在多个版本的 java.lang.Object 类,而且这些类之间是不兼容的。
类加载器在成功加载某个类之后,会把得到的 java.lang.Class 类的实例缓存起来。
下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。
也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass 方法不会被重复调用。
线程上下文类加载器:
类 java.lang.Thread 中的方法 getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 用来获取和设置线程的上下文类加载器。
如果没有通过 setContextClassLoader(ClassLoader cl) 方法进行设置的话,线程将继承其父线程的上下文类加载器。
Java 应用运行的初始线程的上下文类加载器是应用程序类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
线程上下文类加载器是绑定于这个线程的,默认继承应用程序类加载器,我们可以进行修改,这个有十分重要的作用:
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。
常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等,这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers 包中。
这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到。
而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。
引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。
线程上下文类加载器正好解决了这个问题,Java 应用的线程的上下文类加载器默认就是应用程序类加载器。
在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。
Class.forName:
Class.forName 是一个静态方法,同样可以用来加载类。该方法有两种形式:
Class.forName(String name, boolean initialize, ClassLoader loader)
Class.forName(String className)
第一种形式的参数 name 表示的是类的全名;initialize 表示是否初始化类;loader 表示加载时使用的类加载器。
第二种形式则相当于设置了参数 initialize 的值为 true,loader 的值为当前类的类加载器。
Class.forName 的一个很常见的用法是在加载数据库驱动的时候。
如 Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance() 用来加载 Apache Derby 数据库的驱动。
开发自己的类加载器:
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } }
一般来说,自己开发的类加载器只需要覆写 findClass(String name) 方法即可。
java.lang.ClassLoader 类的方法 loadClass() 封装了前面提到的父委托模式的实现。
该方法会首先调用 findLoadedClass() 方法来检查该类是否已经被加载过;
如果没有加载过的话,会调用父类加载器的 loadClass() 方法来尝试加载该类;
如果父类加载器无法加载该类的话,就调用 findClass() 方法来查找该类。
因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,
最好不要覆写 loadClass() 方法,而是覆写 findClass() 方法。
类加载器与 Web 容器:
对于运行在 Java EE™ 容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。
以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。
该类加载器也使用父委托模式,所不同的是它是首先尝试去加载某个类,
如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。
这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。
这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
我们在加载类时通常会遇到两种异常:
方法 loadClass() 抛出的是 java.lang.ClassNotFoundException 异常:没有找到这个类会抛出这个异常
方法 defineClass() 抛出的是 java.lang.NoClassDefFoundError 异常:通常是加载SPI类时,由于SPI的实现类没有定义,导致加载该类的定义失败。