类加载器的作用是完成类加载过程中的装载步骤,即将.class文件加载到JVM
有两种类型的类加载器
1、JVM自带的类加载器
根类加载器(Bootstrap)
扩展类加载器(Extension)
系统类加载器(System)
2、用户自定义类加载器
扩展自java.lang.ClassLoader
这些类加载器以父子关系的形式存在
最顶层的类加载器,根类加载器,负责加载java的核心类,它加载的路径是jre/lib/rt.jar,我们用到的java核心类如集合框架类ArrayList以及String类等,都是在rt.jar中,即这些类是被BootStrap ClassLoader加载的;BootStrap ClassLoader 是用C++实现的;
Extension ClasssLoader负责加载jre的扩展类,它加载的路径是jre/lib/ext,它的父类加载器是BootStrap ClassLoader;
System ClassLoader从CLASSPATH环境变量指定的路径加载类,一般我们自己编写的类都是由System ClassLoader加载,System ClassLoader的父类加载是Extension ClasssLoader。
除了BootStrap ClassLoader外,其他的ClassLoader(包括用户自定义的)都需要继承..ClassLoader
类加载器在加载类时,采取父委托机制,即加载类时,先请求父类加载器去加载,如果能加载,则在父类加载器加载完后直接返回,如果父类加载器无法加载,则自身尝试去加载,比如,加载我们自己编写的
.
.
.
.User这个类,首先由
System ClassLoader加载,
System ClassLoader委托父类加载器
Extension ClasssLoader加载,
Extension ClasssLoader委托
BootStrap ClassLoader加载,
BootStrap ClassLoader加载不到,
Extension ClasssLoader也加载不到,
System ClassLoader尝试自己加载,从CLASSPATH环境变量指定的路径加载,如果该路径下有这个类,则加载进来,如果找不到,抛出
.
.ClassNotFoundException
JVM规范允许类加载器在预料到某个类将要被使用时就预先加载它,如果在预先加载的过程中遇到了class文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报告错误,如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成。在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类;在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
由同一类加载器加载的属于相同包的类组成了运行时包。决定两个类是不是属于同一个运行时包,不仅要看它们的包名是否相同,还要看定义类的加载器是否相同。只有属于同一运行时包的类才能互相访问包可见(默认访问级别)的类和类成员。这样的限制能避免用户自定义的类冒充核心类库的类,去访问核心类库的包可见成员。
同一个命名空间内的类是互相可见的
子加载器的命名空间包含所有父加载器的命名空间,因此,由子加载器加载的类能看见父加载器加载的类,例如,系统类加载器加载的类能看到根类加载器加载的类
由父加载器加载的类不能看见子加载器加载的类
如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见
接下来看几个测试
创建两个类User、Person,代码如下:
public class User {
public User() {
System.out.println("User loaded by " + this.getClass().getClassLoader());
new Person();
}
}
public class Person {
public Person() {
System.
out
.println(
"Person loaded by "
+
this
.getClass().getClassLoader());
}
}
分别在构造器里输出加载当前类的ClassLoader对象,在User的构造器中创建了Person对象
测试1:
@Test
public void testClassLoader1() {
System.out.println(this.getClass().getClassLoader());
new User();
System.out.println("String loaded by " + String.class.getClassLoader());
}
控制台输出如下:
sun.misc.Launcher$AppClassLoader@addbf1
sun.misc.Launcher$AppClassLoader@addbf1
sun.misc.Launcher$AppClassLoader@addbf1
String loaded by null
可以看出,当前测试类以及User、Person类都是由系统类加载器加载的,而String类是由根类加载器加载的,由于根类加载器是由C++实现的,所以在代码中获取会返回null
测试2:
@Test
public void testClassLoader2() {
ClassLoader classLoader = this.getClass().getClassLoader();
while(true) {
System.out.println(classLoader);
if (null == classLoader) {
break;
}
classLoader = classLoader.getParent();
}
}
从加载当前测试类的类加载器开始,输出父类加载器,控制台输出如下:
sun.misc.Launcher$AppClassLoader@addbf1
sun.misc.Launcher$ExtClassLoader@42e816
null
可以看出,类加载器采用父子关系
接下来自定义一个类加载器,从指定的loadPath路径加载class,代码如下:
public class MyClassLoader extends ClassLoader {
private String name;
/**加载路径*/
private String loadPath;
private static final String EXTENSION = ".class";
public MyClassLoader(String name, String loadPath) {
super();
this.name = name;
this.loadPath = loadPath;
}
public MyClassLoader(String name, String loadPath, ClassLoader parent) {
super(parent);
this.name = name;
this.loadPath = loadPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
if (null == b || b.length == 0) {
throw new ClassNotFoundException();
}
return defineClass(name, b, 0, b.length);
}
/**
* 加载读取class文件
* @param name
* @return
*/
private byte[] loadClassData(String name) {
String dir = name.replace(".", File.separator);
String realPath = this.loadPath + File.separator + dir + EXTENSION;
ByteArrayOutputStream baos = null;
DataInputStream dis = null;
int end = 0;
try {
baos = new ByteArrayOutputStream();
dis = new DataInputStream(new FileInputStream(realPath));
while ((end = dis.read()) != -1) {
baos.write(end);
}
} catch (IOException e) {
//e.printStackTrace();
} finally {
if (null != baos) {
try {
baos.close();
} catch (IOException e) {
}
}
if (null != dis) {
try {
dis.close();
} catch (IOException e) {
}
}
}
return baos.toByteArray();
}
@Override
public String toString() {
return this.name;
}
}
类加载器在加载类时,会调用ClassLoader的loadClass(String name),查看jdk源码,loadClass方法中会调用父类加载器去加载,如果加载不到,会调用findClass(String name)自行加载,因此自定义的类加载器都需要覆盖findClass(String name)方法,以下为jdk中ClassLoader的loadClass方法的部分源码:
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
创建测试类,如下:
public class ClassLoaderTest1 {
public static void main(String[] args) {
MyClassLoader classLoader1 = new MyClassLoader("loader1", "E:\\classloader\\path1");
Class<?> clazz1;
try {
System.out.println("---------1----------");
clazz1 = classLoader1.loadClass("com.jiangnan.classloader.classloader.User");
clazz1.newInstance();
} catch (Exception e) {
System.out.println(e.getClass().getName());
}
System.out.println("---------1----------");
MyClassLoader classLoader2 = new MyClassLoader("loader2", "E:\\classloader\\path2", classLoader1);
Class<?> clazz2;
try {
System.out.println("---------2----------");
clazz2 = classLoader2.loadClass("com.jiangnan.classloader.classloader.User");
clazz2.newInstance();
} catch (Exception e) {
System.out.println(e.getClass().getName());
}
System.out.println("---------2----------");
}
}
创建了两个MyClassLoader的实例,其中classLoader1是classLoader2的父类加载器,分别从指定的路径加载User类,运行此main方法,控制台输出如下:
---------1----------
User loaded by sun.misc.Launcher$AppClassLoader@addbf1
Person loaded by sun.misc.Launcher$AppClassLoader@addbf1
---------1----------
---------2----------
User loaded by sun.misc.Launcher$AppClassLoader@addbf1
Person loaded by sun.misc.Launcher$AppClassLoader@addbf1
---------2----------
两次都是由应用类加载器加载的,因为classLoader2的父加载器为应用类加载器,根据父委托机制,应用类加载器在CLASSPATH路径下能加载到User类
将工程bin目录下的User.class文件删除,放到
E:\\classloader\\path1,
注意,需要按照指定的包目录存放,例如,在我的环境中,包名是
com.jiangnan.classloader.classloader,
因此,User.class被放在
E:\classloader\path1\com\jiangnan\classloader\classloader目录下
现在的情况是,测试类
ClassLoaderTest1、自定义类加载器类
MyClassLoader以及Person类位于工程bin目录下,二User类位于path1路径下,运行main方法,控制台输出如下:
---------1----------
User loaded by loader1
Person loaded by sun.misc.Launcher$AppClassLoader@addbf1
---------1----------
---------2----------
User loaded by loader1
Person loaded by sun.misc.Launcher$AppClassLoader@addbf1
---------2----------
User类是由classLoader1实例加载的,因为此时系统类加载器在bin目录下加载不到User类
在main方法中增加classLoader3实例,如下:
MyClassLoader classLoader3 = new MyClassLoader("loader3", "E:\\classloader\\path3", null);
Class<?> clazz3 = null;
try {
System.out.println("---------3----------");
clazz3 = classLoader3.loadClass("com.jiangnan.classloader.classloader.User");
clazz3.newInstance();
} catch (Exception e) {
System.out.println(e.getClass().getName());
}
System.out.println("---------3----------");
System.out.println("clazz1 == clazz2 is " + (clazz1 == clazz2));
System.out.println("clazz1 == clazz3 is " + (clazz1 == clazz3));
指定classLoader3的父类加载器为null,即classLoader3的类加载器为BootStrap根类加载器,同时判断clazz1和clazz2、clazz3是否相等,控制台输出如下:
---------1----------
User loaded by loader1
Person loaded by sun.misc.Launcher$AppClassLoader@addbf1
---------1----------
---------2----------
User loaded by loader1
Person loaded by sun.misc.Launcher$AppClassLoader@addbf1
---------2----------
---------3----------
User loaded by loader3
Person loaded by loader3
---------3----------
clazz1 == clazz2 is true
clazz1 == clazz3 is false
clazz1与clazz3不相等,因为它们是被不同的类加载器加载,它们在内存中的存在形式如下:
测试代码位于
https://github.com/ywu2014/ClassLoader