ClassLoader的核心作用是:加载指定路径的class
JVM将所需要运行的*.class文件加载到JVM进程中需要有一个类加载器(ClassLoader,系统会提供默认类加载器,我们也可以自己写),类加载器的好处就在于我们可以随意指定*.class文件的位置(包括网络上的)。
类的初始化时间
调用ClassLoader类的loadClass方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
示例:
class TestClass{
static {
System.out.println("TestClass");
}
}
public class Test2 {
public static voidmain(String[] args) throws ClassNotFoundException {
ClassLoader classLoader= ClassLoader.getSystemClassLoader();
Class<?> clazz= classLoader.loadClass("edu.swust.JVM.classloader.TestClass");
// 只是加载,没有初始化
System.out.println("-----------------------");
clazz = Class.forName("edu.swust.JVM.classloader.TestClass");
// 初始化
/**
* 结果
* -----------------------
* TestClass
* -----------------------
* classedu.swust.JVM.classloader.TestClass
*/
}
}
双亲委托机制
类加载器用来把类加载到java虚拟机中,从JDK1.2开始,类加载过程采用了父亲委托机制,这种机制更好的保证了java平台的安全。在这个委托机制中,除了java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当加载器请求加载器loader1加载Sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能够加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载Sample类。
若有一个类加载器能够成功加载Sample类,那么这个类加载器称为定义类加载器,所有能够成功返回class对象的引用的类加载器(包括定义类加载器和它的子加载器)都称为初始类加载器
类加载器分两种
Java虚拟机自带的加载器
BootstrapClassLoader根加载器(Bootstrap) C++编写 未开源
ExtensionClassLoader扩展类加载器(Extension)java编写 父加载器是Bootstrap
AppClassLoader:系统类加载器(System)也叫应用类加载器 java编写父加载去是Extension
用户自定义类加载器
CustomClassLoader:继承java.lang.ClassLoader,用户自定义加载方式
注意:这里的父子加载器不一定是继承的关系,也可能是组合的关系
图一:双亲加载模式示例
这么做的原因是出于安全考虑,防止用户定义的类加载器加载不合法的,如果加载不了,抛出ClassNotFoundException。
需要指出的是,加载器之间的父子关系实际上是加载器对象之间的包装关系,而不是类之间的继承关系。一对父子加载器可能是同一个类加载器的两个实例,也可能不是。在子加载器的对象中包装了一个父加载器对象。例如以下loader1和loader2都是MyClassLoader类的实例,并且loader1包装了loader2,loader2是loader1的父加载器 被包装的的父加载器 包装者是子加载器。
当生成一个自定义类加载器实例时,如果没有指定它的父类加载器,那么系统类加载器就会成为该类加载器的父加载器
双亲模式的问题:顶层ClassLoader无法加载底层ClassLoader
双亲模式的破坏:
双亲模式是默认的模式,但并非只能这么做
Tomcat的WebappClassLoader就会先加载自己的Class,找不到在委托parent
OSGi的ClassLoader就形成网站结构,根据需要自由加载Class
自定义类加载器:
首先查看API得到如下例子
图二:ClassLoader 的API的例子
例子:MyClassLoader.java
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
public class MyClassLoader extendsClassLoader {
private String name; //加载类名
private String path = "D:\\"; //加载类的路径
private final String fileType = ".class";
public MyClassLoader(String name) {
super();//系统类为父加载器
this.name = name;
}
public MyClassLoader(ClassLoader loader, String name) {
super(loader); //显示指定父加载器
this.name = name;
}
/**
* 找到指定的类字节码
*@param name 类的名字
*/
@Override
public Class<?> findClass(String name) throwsClassNotFoundException {
byte[] data = loadClassData(name);
return defineClass(name, data, 0,data.length); //根据类字节码的ASCII码流 加载类
}
/**
* 读取类的字节码
*@param name 类的名字
*@return 指定类字节码的ASCII码流
*/
private byte[] loadClassData(String name) {
InputStreaminputStream = null;
byte[] data = null;
ByteArrayOutputStream baos = null;
try {
inputStream= new FileInputStream(new File(path + name + fileType));
baos= new ByteArrayOutputStream();
intch = 0;
while((ch = inputStream.read()) != -1) {
baos.write(ch);
}
data= baos.toByteArray();
}catch (Exception e) {
}finally {
try{
inputStream.close();
baos.close();
}catch (Exception e2) {
}
}
return data;
}
public static void main(String[] args) throws Exception {
// 测试代码
}
public static void test(ClassLoader loader) throws Exception {
Class<?>clazz = loader.loadClass("Sample");
@SuppressWarnings("unused")
Objectobject = clazz.newInstance();
}
publicString getName() {
returnname;
}
publicvoid setName(String name) {
this.name= name;
}
publicString getPath() {
returnpath;
}
publicvoid setPath(String path) {
this.path= path;
}
publicString getFileType() {
returnfileType;
}
@Override
public String toString() {
return name;
}
}
测试用例:Sample.java:
public class Sample {
public int value = 1;
public Sample() {
System.out.println("Sample loader : " + this.getClass().getClassLoader());
new Dog();
}
}
测试用例:Dog.java
public class Dog {
public Dog() {
System.out.println("Dog loader :" + this.getClass().getClassLoader());
}
}
图三、文件目录结构
测试1:
MyClassLoaderloader1 = new MyClassLoader("loader1");
loader1.setPath("D:\\myapp\\serverlib\\");
MyClassLoaderloader2 = new MyClassLoader(loader1, "loader2");
loader2.setPath("D:\\myapp\\clientlib\\");
MyClassLoaderloader3 = new MyClassLoader(null, "loader3");//使用根加载器
loader3.setPath("D:\\myapp\\otherlib\\");
test(loader2);
test(loader3);
图四:测试结果
图五:loader2加载顺序
对于D:\myapp\syslib>javaMyClassLoader
从图中可以看,从上往下加载,只有在loader1才能找到Sample
对于java -cp.;D:\myapp\otherlib MyClassLoader
通过改变了classpath 在classpath中找到了Sample,所以直接用System加载
图六:loader3加载顺序
无论怎么变classpath 都只有;loader3才能加载Sample类
类的卸载
当一个类被加载,连接和初始化后,它的生命周期就开始了。当代表这个类的Class对象不在被引用,即不可触及时Class对象就会结束生命周期,这个类在方法区内的数据也会被卸载,从而结束这个类的生命周期。由此可见,一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
由java虚拟机自带的类加载器(根类加载器,扩展类加载器,系统类加载器),在虚拟机的生命周期中始终不会被卸载,原因是虚拟机会引用这三个类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些类对象始终是可触及的。
用用户自定义的类加载器所加载的类是可以卸载的
测试代码:
MyClassLoaderloader1 = new MyClassLoader("loader1");
loader1.setPath("D:\\myapp\\serverlib\\");
Class<?>clazz1 = loader1.loadClass("Sample");
@SuppressWarnings("unused")
Objectobject = clazz1.newInstance();
System.out.println(clazz1.hashCode());
/***************************过程1结束***********************/
loader1 =null;
clazz1 =null;
object =null;
/***************************过程2结束***********************/
loader1 =new MyClassLoader("loader1");
loader1.setPath("D:\\myapp\\serverlib\\");
clazz1 =loader1.loadClass("Sample");
object =clazz1.newInstance();
System.out.println(clazz1.hashCode());
/***************************过程3结束***********************/
Class<?>clazz2 = loader1.loadClass("Sample");
MyClassLoaderloader2 = new MyClassLoader(loader1, "loader2");
loader2.setPath("D:\\myapp\\clientlib\\");
Class<?>clazz3 = loader2.loadClass("Sample");
System.out.println("clazz2 == clazz1: " + (clazz2 == clazz1));
System.out.println("clazz2 == clazz3: " + (clazz2 == clazz3));
System.out.println("clazz3 == clazz1: " + (clazz3 == clazz1));
/**
* 结果
* D:\myapp\syslib>java MyClassLoader
* Sample loader : loader1
* Dog loader : loader1
* 1311053135
* Sample loader : loader1
* Dog loader : loader1
* 865113938
* clazz2 == clazz1: true
* clazz2 == clazz3: true
* clazz3 == clazz1: true
*/
当执行完过程1,得到的内存模型如下图:
图七:执行完过程1的内存模型
执行完过程2,内存中1、2、3连接断开,JVM会卸载加载的类,在执行完过程3之后又会重新加载类,所以两次的clazz1.hashCode()会不同。
后面的clazz1,clazz2,clazz3都是由loader1加载的serverlib目录下的类(clazz3本质上还是loader1加载的,之前有分析),在内存中加载一个类只加载一份,所以三个clazz指向的是同一个对象,所以三个比较为TRUE。