类的加载机制一共有四种,分别是全盘负责机制,父类委托机制,缓存机制,还有最重要的双亲委派机制。
所谓全盘负责,就是说当一个类加载器负责加载某个Class的时候,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显式使用另外一个类加载器来载入。
所谓父类委托则是先让parent(父)类加载器试图加载该Class,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
缓存机制将会保证所有被加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存中搜寻该Class,只有当缓存中不存在该Class对象时,系统才会读取该类对应的二进制数据,并将其转换成Class对象,并存入cache。这就是为什么我们修改了Class后,程序必须重新启动JVM,程序所作的修改才会生效的原因。(因为此时缓存中不存在修改后的Class对象)
java虚拟机对于class文件是按需加载也就是说需要该类时才会把它的class文件加载进内存里生成class对象,而且加载某个class文件时,Java虚拟机采用的是双亲委派机制,即把请求交由父类处理,它是一种任务委派模式。
双亲委派机制的代码实现:
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
protected synchronized Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException {
// 首先判断该类型是否已经被加载
Class c = findLoadedClass(name);
if (c == null) {
//如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
try {
if (parent != null) {
//如果存在父类加载器,就委派给父类加载器加载
c = parent.loadClass(name, false);
} else {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
双亲委派机制的工作原理为:
双亲委派机制的过程:
具体过程如图所示:
实例代码:
package GenericsTest.JVMTest;
public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
import java.io.IOException;
import java.io.InputStream;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
System.out.println("parent ClassLoader: "+ myLoader.getParent());
Class<?> clazz = myLoader.loadClass("GenericsTest.JVMTest.Main");
System.out.println("class name: " + clazz.getSimpleName() + " \nclass hashcode: " + clazz.hashCode() + " \nloader: " + clazz.getClassLoader().getClass().getSimpleName());
}
}
结果: 可以看到的是打印出来的类加载器是APPClassLoader(应用程序类加载器),而非自定义的类加载器,为什么呢?这就涉及到了双亲委派机制,
当我们用 myLoader 去加载 GenericsTest.JVMTest.Main.java 这个类的时候,根据双亲委托机制,自定义的类加载器 myLoader 会委托它的父加载器 AppClassLoader 去加载, AppClassLoader 应用类加载器又会委托它的父类加载器 Bootstrap ClassLoader 启动类去加载。而 Bootstrap ClassLoader 找不到这个类,然后让 AppClassLoader 去加载,而 AppClassLoader 加载的路径是项目的 ClassPath, 这时候找到了 Main类并加载了它,并没有让 myLoader 去加载。
双亲委托机制还有下面的一些特点:
这个机制主要是保护java源代码信息的。如果我们自己建立和源代码相同的包,例如创建类名相同的String类,在我们去使用类加载器去加载此类时,为了防止你自定义的类对源码的破坏,所以他默认不是使用你的String类的本身的系统加载器去加载它,而是选择率先使用引导类加载器去加载,而引导类在加载的过程中会先去加载JDK自带的文件(rt.jar包中的java/lang/String.class),而不是你自己定义的String.class,报错信息会提示没有main方法 ,就是因为加载的是rt.jar包下的String类,这样就可以做到保证对java核心源代码的保护,这就是沙箱保护机制。
实例代码:
package java.lang;
/**
* 报错:
* 错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
* public static void main(String[] args)
* 否则 JavaFX 应用程序类必须扩展javafx.application.Application
*
*/
public class String {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
结果分析: 这个程序可以运行,但是它不会加载当前的java.lang.String,而是会加载默认的java.lang.String包,在那个包下没有main方法,而当前程序需要运行main方法,因此会报错。