1.类加载器
类加载器用来把类加载到Java虚拟机当中。从JDK1.2版本开始,类的加载过程采用双委托机制,这种机制能更好的保证Java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且仅有一个父加载器。当Java程序请求加载器Loader1加载类Sample,首先委托父类加载器加载,如果能加载则返回,否则由Loader1加载器加载Sample。
2.类加载器的类型
- Java虚拟机自带的类加载器
-
根类加载器(BootStrap):
该类加载器没有父类加载器,由虚拟机实现,主要负责加载虚拟机的核心类库,如:java.lang.*
等。根加载器从系统属性sun.boot.class.path
所指定的目录中加载类库。根加载器的底层实现主要依赖于底层操作系统,属于虚拟机实现的一部分,它并没有继承java.lang.ClassLoader类。 -
扩展类加载器(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
的子类- 用户可以自定义类的加载方式需要继承
java.lang.ClassLoader
扩展点:
- JVM规范允许类加载器在预料某个类将要被使用时就预先加载他,如果在预先加载过程中遇到了
.class
文件缺失或者存在错误,类加载器必须在程序首次主动使用
该类时才报告错误(LinkageError错误)。 - 如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误。
类加载器的加载流程图:
3.自定义类加载器代码示例
代码:
public class ClassLoaderTest extends ClassLoader {
private String classLoaderName;
private static final String FILE_EXTENSION = ".class";
public ClassLoaderTest(String classLoaderName) {
// 使用父类默认方式,将应用类加载器设置为该类加载器的父加载器
super();
this.classLoaderName = classLoaderName;
}
public ClassLoaderTest(ClassLoader parentClassLoader, String classLoaderName) {
// 显示指定该类的父加载器
super(parentClassLoader);
this.classLoaderName = classLoaderName;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
System.out.println("find class execute!");
byte[] data = loadClassData(className);
return this.defineClass(className, data, 0, data.length);
}
private byte[] loadClassData(String name) {
try (InputStream inputStream = new FileInputStream(new File(name + this.FILE_EXTENSION));
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
int ch = 0;
while (-1 != (ch = inputStream.read())) {
baos.write(ch);
}
return baos.toByteArray();
} catch (Exception e) {
e.getStackTrace();
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoaderTest classLoaderTest = new ClassLoaderTest("classLoaderTest");
Class<?> clazz = classLoaderTest.loadClass("com.jvm.test.MyTest1");
System.out.println(clazz);
Object o = clazz.newInstance();
System.out.println(o);
}
}
执行结果为:
class com.jvm.test.MyTest1
com.jvm.test.MyTest1@61bbe9ba
分析结果:
findClass方法并没有打印出来我们的输入的内容。主要原因在于,该类加载器加载MyTest1
类时,首先委托给父类加载器(该类的父类加载器设置为系统类加载器)去加载,系统类加载器能够加载classpath下的MyTest1类,所以直接在于系统类加载器加载返回MyTest1,所以该类加载器定义的方法没有执行。
如果想用我们自己定义的类加载器,我们可以修改加载类的资源路径,从其他路径去加载:
public class ClassLoaderTest extends ClassLoader {
private String classLoaderName;
private static final String FILE_EXTENSION = ".class";
private String path;
public void setPath(String path) {
this.path = path;
}
public ClassLoaderTest(String classLoaderName) {
// 使用父类默认方式,将应用类加载器设置为该类加载器的父加载器
super();
this.classLoaderName = classLoaderName;
}
public ClassLoaderTest(ClassLoader parentClassLoader, String classLoaderName) {
// 显示指定该类的父加载器
super(parentClassLoader);
this.classLoaderName = classLoaderName;
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
System.out.println("find class execute!");
byte[] data = loadClassData(className);
return this.defineClass(className, data, 0, data.length);
}
private byte[] loadClassData(String className) {
className = className.replace(".", "/");
try (InputStream inputStream = new FileInputStream(new File(this.path+className + this.FILE_EXTENSION));
ByteArrayOutputStream boas = new ByteArrayOutputStream()) {
int ch = 0;
while (-1 != (ch = inputStream.read())) {
boas.write(ch);
}
return boas.toByteArray();
} catch (Exception e) {
e.getStackTrace();
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoaderTest classLoaderTest = new ClassLoaderTest("classLoaderTest");
classLoaderTest.setPath("/Users/zheng/Desktop/");
Class<?> clazz = classLoaderTest.loadClass("com.jvm.test.MyTest1");
System.out.println(clazz);
Object o = clazz.newInstance();
System.out.println(o);
}
}
程序修改后的执行结果为:
find class execute!
class com.jvm.test.MyTest1
com.jvm.test.MyTest1@511d50c0
由执行结果可知我们自定义的类加载器的加载方式是通过我们自己写的加载方式加载的(注意:classPath下如果存在com.jvm.test.MyTest1.class
,需要删除,否则程序首先会用系统类加载器从classPath下加载)。
4.类加载器的命名空间:
示例代码
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoaderTest classLoaderTest = new ClassLoaderTest("classLoaderTest");
classLoaderTest.setPath("/Users/zheng/Desktop/");
Class<?> clazz = classLoaderTest.loadClass("com.jvm.test.MyTest1");
System.out.println(clazz.hashCode());
System.out.println(clazz);
ClassLoaderTest classLoaderTest2 = new ClassLoaderTest("classLoaderTest");
classLoaderTest2.setPath("/Users/zheng/Desktop/");
Class<?> clazz2 = classLoaderTest2.loadClass("com.jvm.test.MyTest1");
System.out.println(clazz2.hashCode());
System.out.println(clazz2);
}
执行结果:
find class execute!
1360875712
class com.jvm.test.MyTest1
find class execute!
491044090
class com.jvm.test.MyTest1
分析:
由执行结果可以看出类MyTest1
被加载了两次(hash值不一样),造成这种情况的现象主要是因为类加载器的命名空间造成的。
- 每个类加载器都有自己的命名空间,命名空间由该类加载器及所有父加载器所加载的类组成。
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类。
- 在不同的命名空间中,有可能出现类的完整名字(包括类的包名)相同的两个类。
5.命名空间的深入理解:
代码:
public class NameSpaceTest {
private NameSpaceTest nameSpaceTest;
public void setNameSpace(Object o){
this.nameSpaceTest = (NameSpaceTest)o;
}
}
public class ClassLoaderNameSpaceTest {
public static void main(String[] args) throws Exception {
// 次处类加载器用了文章上面我们自定义的类加载器
ClassLoaderTest classLoaderTest1 = new ClassLoaderTest("loader1");
ClassLoaderTest classLoaderTest2 = new ClassLoaderTest("loader2");
classLoaderTest1.setPath("/Users/zheng/Desktop/");
classLoaderTest2.setPath("/Users/zheng/Desktop/");
Class<?> clazz1 = classLoaderTest1.loadClass("com.jvm.test.NameSpaceTest");
Class<?> clazz2 = classLoaderTest2.loadClass("com.jvm.test.NameSpaceTest");
System.out.println(clazz1 == clazz2);
Object obj1 = clazz1.newInstance();
Object obj2 = clazz2.newInstance();
Method method = clazz1.getMethod("setNameSpace",Object.class);
method.invoke(obj1,obj2);
}
}
执行结果为:
true
分析为:编译项目当Classpath下NameSpaceTest.class
存在时,classLoaderTest1
和classLoaderTest2
会委托系统类加载器加载类,所以class对象只会加载一次,所以结果为true。
我们将NameSpaceTest.class
在classPath下删除,将起复制于其他路径下我这边复制在 /Users/zyw/Desktop/com/jvm/test
目录下,然后程序去执行,执行结果为:
find class execute!
find class execute!
false
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.jvm.test.ClassLoaderNameSpaceTest.main(ClassLoaderNameSpaceTest.java:25)
Caused by: java.lang.ClassCastException: com.jvm.test.NameSpaceTest cannot be cast to com.jvm.test.NameSpaceTest
at com.jvm.test.NameSpaceTest.setNameSpace(NameSpaceTest.java:15)
... 5 more
结果分析:
当我们将classPath
下的NameSpaceTest.class
删除,这时我们自定义的类加载器,去委托系统类加载器去加载就会加载不到,所以最终由我们的自定义的类加载器去加载,因为我们的classLoaderTest1
和classLoaderTest2
没有直接或者间接的父子关系,所以这两个加载器加载器会在两个不同的命名空间去加载,所以NameSpaceTest
的类对象会被加载两遍,而报错信息java.lang.ClassCastException: com.jvm.test.NameSpaceTest cannot be cast to com.jvm.test.NameSpaceTest
主要是因为不同的命名空间下类对象是不可见的。
结论:
- 同一个命名空间的类是相互可见的。
- 子类加载器的命名空间包含所有父类加载器的命名空间。因此由子类加载器加载的类能看见父类加载器加载的类。如系统类加载器加载的类能看见根类加载器加载的类。
- 由父类加载器加载的类不能看见子类加载器加载的类。
- 如果两个类加载器没有直接或者间接的父子关系,那么他们各自加载的类相互不可见。