类加载器
1、类加载器概述
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。
作用:加载Java类到JVM中.(唯一标识:全类名+类加载器)
一旦一个类被加入JVM中,同一个类就不会被再次加入了。
正如一个对象有一个唯一的标识一样,一个载入JVM的类也有一个唯一的标识。
在Java中,一个类用其全限定类名(包括包名和类名)作为标识;
但在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。
在JVM中两个同名的Person类是完全不同的,之间也互不兼容,因为类加载器不同。
public class Person {
private Person instance;
public void setPerson(Object instance) {
this.instance = (Person) instance;
}
}
编写一个测试方法,用于测试通过两个不同类加载器加载Person类得到不同的Class实例。
@Test
public void ClassIdentityTest() throws Exception{
String classDataRootPath = "WebRoot\\WEB-INF\\classes";
FileSystemClassLoader fscl1 = new FileSystemClassLoader(classDataRootPath);
FileSystemClassLoader fscl2 = new FileSystemClassLoader(classDataRootPath);
String className = "app.java.classloader.Person";
try {
Class<?> class1 = fscl1.findClass(className);
Object obj1 = class1.newInstance();
Class<?> class2 = fscl2.findClass(className);
Object obj2 = class2.newInstance();
Method setSampleMethod = class1.getMethod("setPerson", java.lang.Object.class);
setSampleMethod.invoke(obj1, obj2);
} catch (Exception e) {
e.printStackTrace();
}
}
上述代码运行后,报如下错误,表示虽然两个对象的类名称相同,但是由于通过不同类加载器得到Class实例,JVM不会认为是相同的。
2、类加载器分类
当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构。
BootstrapClassLoader:根类加载器。
作用:负责加载Java的核心类.
* 读取哪些内容:
* %JAVA_HOME%/jre/lib目录下的所有Jar包.
* %JAVA_HOME%/jre/classes目录:上述所有Jar编译后的class文件路径.
* 如果当前工程为Web工程,读取有关JavaEE的所有Jar包.
@Test
public void BootstrapTest(){
// 获取根类加载器所加载的全部URL数组
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
// 遍历输出根类加载器加载的全部URL
for (int i = 0; i < urls.length; i++) {
System.out.println(urls[i].toExternalForm());
}
}
ExtensionClassLoader:扩展类加载器。
作用:负责加载JRE的扩展目录.
* 读取哪些内容:
* %JAVA_HOME%/jre/lib/ext目录下的所有Jar包.
* JRE的扩展目录的Jar包的作用:
* 例如:工具Jar包,自定义工具Jar包(很少用).
ExtensionClassLoader被称为扩展类加载器,它负责加载JRE的扩展目录(%JAVA_HOME%/jre/lib/ext或由java.ext.dirs系统属性指定的目录)中的JAR包的类。通过这种方式可以为Java扩展核心类以外的新功能,只要把自己开发的类打包成JAR文件,然后放入%JAVA_HOME%/jre/lib/ext路径即可。
@Test
public void ExtensionTest() throws Exception{
// 位于jre/lib/ext/dnsns.jar
DNSNameService dnsNameService = new DNSNameService();
System.out.println(dnsNameService.getClass().getClassLoader());
}
SystemClassLoader:系统类加载器。
作用:负责加载工程的Class文件.
@Test
public void SystemTest() throws Exception{
// 获取当前类的实例对象
ClassLoaderTest classLoaderTest = new ClassLoaderTest();
System.out.println(classLoaderTest.getClass().getClassLoader());
}
* 存在,优先级由高到低:Bootstrap -> Extension -> System -> Customs.
@Test
public void ClassLoaderTreeTest() throws Exception{
// 获取当前类的类加载器
ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
// 判断类加载器是否为空
while (classLoader != null) {
// 打印当前类加载器的名称
System.out.print(classLoader.getClass().getName()+"->");
// 获取当前类加载器的父级
classLoader = classLoader.getParent();
}
System.out.println(classLoader);
}
上述代码中可以看出JVM中类加载器的层次结构。最后输出的是null的愿意是因为根类加载器并不是Java提供的,而是JVM提供的,所以不能获取。
如果一个Java类,被Bootstrap类加载器加载,还会不会被Extension或System加载?
* 不会.
3、类加载机制
JVM的类加载器机制主要有如下三种:
全盘负责。
所谓全盘负责,就是当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
当一个类加载器去加载一个Java类时,该Java类导入的类和依赖的类,都由该类加载器加载.
作用:保证一个Java类在运行时,一定是成功的.
父类委托。
所谓父类委托,则是先让父类加载器视图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
父类优先,先由父类加载器加载指定的Java类,只有当父类加载器无法加载该Java类,才从自身类加载器加载.
作用:保证Java提供的所有类加载器可以顺利执行.
缓存机制。
缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区中搜索该Class,只有当缓存区中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区中。这也是为什么修改Class后,必须重新启动JVM,程序所做的修改才会生效的原因。
4、自定义类加载器
JVM中除根类加载器之外的所有类加载器都是ClassLoader子类的实例,可以通过扩展ClassLoader的子类,并重写该ClassLoader提供的方法来实现自定义的类加载器。
ClassLoader类具有如下两个关键方法:
loadClass(Stringname, boolean resolve):该方法为ClassLoader的入口点,根据指定名称来加载类,系统就是调用ClassLoader的该方法来获取指定类对应的Class对象。
findClass(Stringname):根据指定名称来查找类。
由于loadClass()方法的执行步骤为
1)利用findLoadedClass()方法来检查是否已经加载类。
2)在父类加载器调用loadClass()方法。
3)调用findClass()方法查找类。
重写findClass()方法可以避免默认类加载器的父类委托和缓冲机制,这里推荐重写findClass()方法。
下面来实现一个文件系统类加载器,用于加载存储在文件系统上的Java字节代码。
* Java提供的所有类加载器都是ClassLoader的子类实例.
* 注意:只有根类加载器不是ClassLoader的子类实例.
* 根类加载器是由JVM(Java虚拟机)提供的.(我们获取不到)
* 扩展类加载器和系统类加载器都是Java提供的.
* 自定义类加载器,必须继承于ClassLoader类.
* findClass():指定二进制名称查找类.
* 重写该方法比较简单.
* loadClass():指定二进制名称加载类.
* 利用findLoadedClass()方法来检查是否已经加载类.
* 在父类加载器调用loadClass()方法.
* 调用findClass()方法查找类.
package hcx.java.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 自定义类加载器
* * 继承于ClassLoader类.
* * 重写findClass()
* @author JYL
*
*/
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
/**
* FileSystemClassLoader的构造函数接收绝对路径.
*/
public FileSystemClassLoader(String rootDir) {
this.rootDir = rootDir;
}
/**
* 重写于ClassLoader类的findClass()方法.
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null) {
// Class未找到异常.
throw new ClassNotFoundException();
}
else {
// defineClass()方法是由ClassLoader类提供的.
// defineClass()方法的作用就是通过字节数组获取对应的Class实例.
return defineClass(name, classData, 0, classData.length);
}
}
/**
* getClassData()方法:
* * 通过Class的类名,读取指定Class文件的内容.
*/
private byte[] getClassData(String className) {
// 通过Class类名,查找对应Class文件的路径.
String path = classNameToPath(className);
try {
// 通过Class文件的路径,获取文件输入流.
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;
}
/**
* classNameToPath()
* * 通过Class的类名,查找指定Class文件的路径.
*/
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
}
FileSystemTest
package hcx.java.classloader;
import java.lang.reflect.Method;
public class FileSystemTest {
public static void main(String[] args) {
String webRootPath = "WebRoot\\WEB-INF\\classes";
FileSystemClassLoader loader1 = new FileSystemClassLoader(webRootPath);
FileSystemClassLoader loader2 = new FileSystemClassLoader(webRootPath);
String className = "app.java.classloader.Person";
try {
Class clazz1 = loader1.findClass(className);
Object object1 = clazz1.newInstance();
Class clazz2 = loader2.findClass(className);
Object object2 = clazz2.newInstance();
Method method = clazz2.getMethod("setPerson",Object.class);
method.invoke(object2, object1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Person
package hcx.java.classloader;
public class Person {
private int id;
private String name;
private Person person;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person [id=" + id + ", name=" + name + "]";
}
public void setPerson(Object object){
this.person = (Person)object;
}
}
SystemTest
package hcx.java.classloader;
public class SystemTest {
public static void main(String[] args) {
ClassLoader classLoader = SystemTest.class.getClassLoader();
System.out.println(classLoader);
}
}
5、Tomcat类加载器
对于运行在Java EE™容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。
不同的Web 容器的实现方式也会有所不同。以Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。
该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。
这与一般类加载器的顺序是相反的。
这是Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。
这种代理模式的一个例外是:Java核心库的类是不在查找范围之内的。这也是为了保证Java 核心库的类型安全。
绝大多数情况下,Web应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
- 每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在WEB-INF/classes和WEB-INF/lib目录下面。
- 多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
- 当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。
* 该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器.
* 目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类.
* Java提供的核心类库,依旧是优先加载.(为了保证Java核心类库的安全).