类加载机制
.java文件中的代码在编译后,就会生成JVM能够识别的二进制字节流class文件,class文件中描述的各种信息,都需要加载到虚拟机中才能被运行和使用。
类加载机制,就是虚拟机把类的数据从class文件加载到内存,并对数据进行校检,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程。
类从加载到虚拟机内存开始,到卸载出内存结束,整个生命周期包括七个阶段,如下图(每个过程作用见文章第四部分):
类加载器
生命周期的第一阶段,即加载阶段需要由类加载器来完成的,类加载器根据一个类的全限定名读取类的二进制字节流到JVM中,然后生成对应的java.lang.Class对象实例,
在虚拟机默认提供了3种类加载器,启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用类加载器(Application ClassLoader),如果有必要还可以加入自己定义的类加载器。
(1)启动类加载器(Bootstrap ClassLoader):负责加载 在<JAVA_HOME>\lib目录 和 被-Xbootclasspath参数所指定的路径中的类库
(2)扩展类加载器(Extension ClassLoader):负责加载 <JAVA_HOME>\lib\ext目录 和 被java.ext.dirs系统变量所指定的路径中的所有类库
(3)应用程序类加载器(Application ClassLoader):负责加载用户类路径classPath所指定的类库,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
(4)自定义加载器(CustomClassLoader):属于应用程序根据自身需要自定义的ClassLoader,如tomcat、jboss都会根据j2ee规范自行实现。
任意一个类在JVM中的唯一性,是由加载它的类加载器和类的全限定名一共同确定的。因此,比较两个类是否“相等”的前提是这两个类是由同一个类加载器加载的,否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。JVM的类加载机制,规定一个类有且只有一个类加载器对它进行加载。而如何保证这个只有一个类加载器对它进行加载呢?则是由双亲委派模型来实现的。
双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余类加载器都应该有自己的父类加载器。(类加载器之间的父子关系不是以继承的关系实现,而是使用组合关系来复用父加载器的代码)
为什么叫双亲委派
既然称为双亲委派机制,则双亲是必须存在的。ClassLoader类存在一个parent属性。因此可以配置双亲。这个双亲是指ExtClassLoader和AppClassLoader,在JDK中则是这样设置:
双亲委派模型的工作原理
如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层级的类加载器都是如此,因此所有请求最终都会被传到最顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
因此,加载过程可以看成自底向上检查类是否已经加载,然后自顶向下加载类。
双亲委派模型的优点
- 使用双亲委派模型来组织类加载器之间的关系,Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
- 避免类的重复加载,当父类加载器已经加载了该类时,子类加载器就没必要再加载一次。
- 解决各个类加载器的基础类的统一问题,越基础的类由越上层的加载器进行加载。避免Java核心API中的类被随意替换,规避风险,防止核心API库被随意篡改。
例如类java.lang.Object,它存在在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
自定义类加载器
- 继承ClassLoader
- 重写findClass()方法
- 调用defineClass()方法
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
/**
* 自定义类加载器
*/
public class MyClassLoader extends ClassLoader{
/**
* 此方法根据双亲委派机制向上传递,向下委派(不一定是自定义的类加载器加载)
* loadClass则是直接指定自定义类加载器加载
* @param name 全类名
* @return
* @throws ClassNotFoundException
*/
@Override
// public Class<?> loadClass(String name) throws ClassNotFoundException {
public Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 通过文件流读取.class文件
String classFile = name.substring(name.lastIndexOf(".") + 1) + ".class";
// 获取二进制的文件流
InputStream in = getClass().getResourceAsStream(classFile);
if(in == null){
return super.loadClass(name);
}
byte[] bytes = new byte[in.available()];
in.read(bytes);
Class<?> aClass = defineClass(name, bytes, 0, bytes.length);
return aClass;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 测试类自定义加载器
* @param args
*/
public static void main(String[] args) throws Exception {
MyClassLoader classLoader = new MyClassLoader();
Class<?> c = classLoader.loadClass("appoint.TestLoadClass");
if(c!=null){
Object obj=c.newInstance();
Method method=c.getMethod("name", null);
method.invoke(obj, null);
System.out.println(c.getClassLoader().toString());
}
}
}
值得注意的是findClass和loadClass两个方法,findClass符合双亲委派机制,向上传递,向下委派,loadClass重写则会覆盖双亲委派逻辑,直接加载类则使用自定义类加载器加载
以下是Java默认的loadClass方法:
// Java双亲委派逻辑
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检查是否已经加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//父加载器不为空,调用父加载器的loadClass
c = parent.loadClass(name, false);
} else {
//父加载器为空则,调用Bootstrap Classloader
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.
long t1 = System.nanoTime();
//父加载器没有找到,则调用findclass
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//调用resolveClass()
resolveClass(c);
}
return c;
}
}
总结
Java双亲委派机制有效防止了重复加载同一个.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。保证数据安全。同时也保证核心.class
不能被篡改。通过委托方式,不会去篡改核心.clas
,即使篡改也不会去加载,即使加载也不会是同一个.class
对象了。不同的加载器加载同一个.class
也不是同一个Class
对象。这样保证了Class
执行安全。
希望本文对大家有所帮助~~
参考文章
https://blog.csdn.net/a745233700/article/details/90232862