类加载机制
类加载器用来把类加载到Java虚拟机中,从JDK1.2开始,类的加载过程采用父亲委托机制。采用这种方式的主要原因是保证Java平台的安全。在这种机制下,除了Jvm自带的根类加载器以外,其余的类加载器都有且只有一个父类加载器。
JVM自带的几种类加载器
- 根类加载器(Bootstrap):该加载器没有父加载器。它负责加载虚拟机的核心类库,如java.lang.*等。可以看出,java.lang.Object就是由根类加载器加载的。根类加载器从系统属性sun.boot.class.path所指定的目录中加载类库。它的实现依赖于底层操作系统。
- 扩展类加载器(Extension):它的父加载器为根类加载器。它从java.ext.dirs系统属性指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录(扩展目录)下加在类库,如果用户创建的JAR文件放在这个目录下,也会自动由扩展类加载器加载。扩展类加载器是纯Java类,是java.lang.ClassLoader类的子类。
- 系统类加载器(System):也称为应用类加载器,它的父加载器为扩展类加载器。它从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器。它也是纯Java类,是java.lang.ClassLoader类的子类。
用户自定义类加载器
除了系统自带的类加载器,用户可以自定义类加载器,继承抽象类java.lang.ClassLoader。
需要注意的是:类加载器之间的父子关系,实际上是加载器之间的包装关系,并不是类之间的继承关系。一对父子加载器可能是一个类加载器的两个实例,也可能不是。
父委托机制
什么是父委托机制?简单来说,当一个类加载器加载一个类时,首先它会查找在自己的命名空间(这个概念下面会讲到)下,该类是否已经加载,如果没有,它会委托给其父加载器加载,它的父亲又会继续向其父父加载器委托,以此类推。当某个父加载器无法加载这个类时,那么就由当前的加载器加载,然后依次把结果返回,最终加载这个类的加载器,我们称为定义类加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始类加载器。显然定义类加载器和它的子加载器都是初始类加载器。
举个例子:
假设有loader1和loader2两个类加载器,loader1为loader2的父加载器。当你使用loader2加载一个类(Test)时,loader2会先检查Test类是否已经加载,如果没有加载,它会委托给loader1加载。loader1又会委托给系统类加载器(默认的自定义类加载器的父加载器)加载。假如系统类加载器加载不了,就由loader1加载器加载。如果loader1加载成功,那么就将结果返回。此时loader1被称为定义类加载器,loader1和loader2被称为初始类加载器。
命名空间
每个类加载器都有自己的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能会出现类的完整名字相同的两个类。
同一个命名空间内的类是相互可见的。子类加载器的命名空间包含所有父类加载器的命名空间。因此由子类加载器加载的类,可以看到父类加载器加载的类。例如系统类加载器加载的类,可以看到根类加载器加载的类。但是由父类加载器加载的类,不能看见子类加载器加载的类。
如果两个加载器之间没有直接或者间接的父子关系,那么他们各自加载的类相互不可见。
运行时包
由同一个类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看定义类加载器是否相同。只有属于同一个运行时包的类才能互相访问。也即是包内可见(类的默认访问级别)。这样能够限制用户自定义类冒充核心类库的类,去访问核心类库的包可见成员。
自定义类加载器
这里我们写一段测试代码,来测试我们上面所讲的内容:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* Created by chengli on 2017/1/2.
*/
public class MyClassLoader extends ClassLoader {
//类加载器的名称
private String name;
//类存放的路径
private String path = "D:/classloader";
MyClassLoader(String name) {
this.name = name;
}
MyClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
/**
* 重写findClass方法
*/
@Override
public Class<?> findClass(String name) {
byte[] data = loadClassData(name);
if (data == null) {
return null;
}
return this.defineClass(name, data, 0, data.length);
}
public byte[] loadClassData(String name) {
FileInputStream is = null;
try {
name = name.replace(".", "/");
File file = new File(String.format("%s/%s%s", path, name, ".class"));
if (!file.exists()) {
return null;
}
is = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b;
while ((b = is.read()) != -1) {
baos.write(b);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return name;
}
public static void main(String[] args) throws Exception {
//新建一个类加载器
MyClassLoader parent = new MyClassLoader("parent");
parent.setPath("d:/classloader/parent");
MyClassLoader child = new MyClassLoader(parent, "child");
child.setPath("d:/classloader/child");
//加载类,得到Class对象,这里使用child类加载器来加载
Class<?> clazz = child.loadClass("Example");
//得到类的实例
clazz.newInstance();
}
}
/**
* Created by chengli on 2017/1/2.
*/
public class Example {
public Example() {
System.out.println("Example class loaded by :" + this.getClass().getClassLoader());
}
}
我们创建了两个类加载器,parent加载d:/classloader/parent下的类,child加载d:/classloader/child的类。类放置路径结构如下:
运行main方法结果如下:
可见目前使用的是系统类加载器,加载的是main文件夹下的Example。
现在我们把main文件夹下的Example类删除:
再运行:
现在把parent目录下的Example也删除:
运行结果:
从以上实例,我们可以看到类加载的父委托机制是怎么运行的。
现在我们把程序改一下,并增加一个Dog类:
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
/**
* Created by chengli on 2017/1/2.
*/
public class MyClassLoader extends ClassLoader {
//类加载器的名称
private String name;
//类存放的路径
private String path = "D:/classloader";
MyClassLoader(String name) {
this.name = name;
}
MyClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
/**
* 重写findClass方法
*/
@Override
public Class<?> findClass(String name) {
byte[] data = loadClassData(name);
if (data == null) {
return null;
}
return this.defineClass(name, data, 0, data.length);
}
public byte[] loadClassData(String name) {
FileInputStream is = null;
try {
name = name.replace(".", "/");
File file = new File(String.format("%s/%s%s", path, name, ".class"));
if (!file.exists()) {
return null;
}
is = new FileInputStream(file);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int b;
while ((b = is.read()) != -1) {
baos.write(b);
}
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
public void setPath(String path) {
this.path = path;
}
@Override
public String toString() {
return name;
}
public static void main(String[] args) throws Exception {
//新建一个类加载器
MyClassLoader parent = new MyClassLoader("parent");
parent.setPath("d:/classloader/parent");
MyClassLoader child = new MyClassLoader(parent, "child");
child.setPath("d:/classloader/child");
/**改动的地方在这里*/
//加载类,得到Class对象
child.loadClass("Dog");
Class<?> clazz = parent.loadClass("Example");
//得到类的实例
clazz.newInstance();
}
}
/**
* Created by chengli on 2017/1/2.
*/
public class Dog {
public Dog() {
System.out.println("this Dog loaded by :" + this.getClass().getClassLoader());
}
}
/**
* Created by chengli on 2017/1/2.
*/
public class Example {
public Example() {
System.out.println("Example class loaded by :" + this.getClass().getClassLoader());
new Dog();
}
}
这里我让父加载器加载Example,子加载器加载Dog,但是在Example有引用Dog类。
结构如下:
此时运行会发现有异常:
此时我们印证了上面的一句话:
同一个命名空间内的类是相互可见的。子类加载器的命名空间包含所有父类加载器的命名空间。因此由子类加载器加载的类,可以看到父类加载器加载的类。例如系统类加载器加载的类可以看到根类加载器加载的类。但是由父类加载器加载的类不能看见子类加载器加载的类。
反过来,让child加载Example,parent加载Dog是可以运行正常的:
public class MyClassLoader extends ClassLoader {
…
…
…
public static void main(String[] args) throws Exception {
…
…
…
/**篇幅原因这里至贴出改动的地方*/
Class<?> clazz = child.loadClass("Example");
parent.loadClass("Dog");
//得到类的实例
clazz.newInstance();
}
}
代码目录结构:
运行结果: