面试必看-Java类加载器(自定义类加载器)
Posted LuckyWangxs
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试必看-Java类加载器(自定义类加载器)相关的知识,希望对你有一定的参考价值。
类加载器
一、类加载器的作用
Java代码是不能直接运行的,需要通过编译器编译成JVM能够识别的二进制字节码文件,而类加载器的作用就是将这些二进制字节码文件即.class文件装载进虚拟机,这样虚拟机才能将程序运行起来。
二、Java虚拟机类加载器结构
1. 引导类(启动类)加载器
引导(Bootstrap)类加载器:
又称之为根类加载器或原始类加载器,引导类加载器是由本地代码(C语言)实现的类加载器,它负责将JAVA_HOME/jre/lib下面的核心类库如rt.jar、resource.jar或者将-Xbootclasspath参数指定目录下的jar包等JVM能够识别的类库加载到内存,该加载器是以文件名去lib目录下识别的,例如文件名不为rt.jar,即使放到lib目录下也不会被加载。由于启动类是由C语言实现的,开发人员无法直接获取到启动类加载器的引用。所以启动类加载器加载到的路径可以由System.getProperty(“sun.boot.class.path”)查看。
拓展: 引导类和其他类加载器一样,但它不是一个Java类,在java中所有的类都要被加载到内存,扩展类即系统类加载器也一样,需要被加载,当JVM启动时,会运行特殊的机器代码,去加载系统类加载器和扩展类加载器,而这段特殊机器指令会启动整个类加载过程,它就是引导类加载器,加载一个纯java类加载器,是他的工作,一句话,引导类加载器是由c++编写,由JVM启动的。引导类加载器还负责加载支持基本Java运行时环境所需的所有代码,包括java.util和java.lang包下的类。
2. 扩展类加载器
扩展(Extend)类加载器:
扩展类加载器是由Sun的ExtClassLoad实现的,是sun.misc.Luncher的静态内部类。它负责将JAVA_HOME/jre/lib/ext下的jar加载到内存或者由-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器,具体加载路径可通过System.getProperty(“java.ext.dir”)。
3. 系统类加载器
系统(System)加载器:
系统类加载器是由Sun的AppClassLoad实现的,跟ExtClassLoad一样,也是sun.misc.Luncher的静态内部类。它负责将用户类路径(java -classpath)或-Djava.class.path参数指定的位置的jar加载到内存,即当前类所在路径及其引用的三方类库,开发者可以直接使用系统类加载器,可以用过System.getProperty(“java.class.path”)查看加载路径。
三、类加载器的加载机制
类加载器负责将.class文件加载到内存,系统为所有被载入到内存的类生成Class对象,类一但被加载,便不会加载第二次,而判断一个类是否被加载的标识是:类全限定名+类加载器,例如pg包下的Person类被CL1类加载器加载到内存,唯一标识为(Person, pg, CL1),又被CL2加载到内存唯一标识为(Person, pg, CL2),则跟上一个是完全不同且互不兼容的两个类,不过显然两个加载器加载同一个类,实际上是冗余了,那么类加载器的加载机制会如何防止这种情况发生呢?
类加载器加载机制有三个,这三种机制共同完成了类的加载,使得类的加载安全高效。
1. 全盘负责
当一个类加载器负责加载某个类时,那这个类所引用的所有类都用这个加载器去加载,除非显示调用其他类加载器,这样可以避免一个类被重复加载。
2. 双亲委派
双亲委派,说白了就是坑爹机制,就是什么事都交给爹去擦屁股,只有爹说这事儿我干不了,儿子才自己想办法去干。哈哈哈,只是一个比喻,下面圆规正转,啊不,是言归正传~
双亲委派,又叫做父类委托,即在加载一个类时,先让父类加载器试图去加载该类,如果父类还有父类,则一直递归到顶级父类加载器(引导类加载器没有父类,它为始祖类加载器),如果始祖类加载器无法加载当前类,则反向委托给始祖类的子类加载器,以此类推,直到源类加载器,此处的父子关系并非继承,而是一种组合关系,是类加载器的复用。
将.class文件载入内存的过程由双亲委派机制完成的。
优点:
1. 避免一个类被多次加载
2. 比较安全,防止篡改核心API。Java的核心API是由引导类加载器加载的,而双亲委派会一直请求父类加载器加载,一般都会找到引导类加载器先去加载类,那么,如果从网络上传输过来一个java.util.Integer类需要加载,此时找到了引导类加载器,引导类加载器发现这个类已经被我加载了,所以不再加载直接返回
3. 又或者,我随便定义了一个java.lang.MyInteger,又找到了引导类加载器,引导类加载器发现java核心类库java.lang下根本没这个类,就会反向委托给子类,但是java.lang需要访问权限,普通加载器无法访问加载,强制加载会报错。
3. 缓存机制
当jvm加载完成一个类是会将类放入jvm缓存中,加载流程为先去缓存区查看当前类是否被加载,如果没有则读.class文件并加载,如果加载则直接返回
四、自定义类加载器
1. 编写自定义类加载器
实现一个用户自定义类加载器需要去继承ClassLoader类并重写findClass方法,代码如下:
public class MyClassLoader extends ClassLoader {
// 默认读取的class路径, 你可以任意定义
private String path = "D:\\\\workspace-IDEA\\\\cloud2020\\\\spring-cloud-test\\\\target\\\\classes\\\\";
private final String fileType = ".class"; // 文件类型
private String name; // 文件全限定名, 例如 com.entity.Person
// name为类加载器的名称, 在此构造函数调用父类无参构造函数, 会初始化类加载器, 默认将系统类加载器作为父类加载器
// 也可以用下面那个构造函数指定父类加载器. 大家可以去看下ClassLoader类的无参构造函数源码
public MyClassLoader(String name) {
super();
this.name = name;
}
// 可以指定父类加载器的构造方法
public MyClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
// 找到自己需要加载的class文件, 并按自己的方式读取到字节数组, 最后复用java的api得到一个Class类型的对象并返回
@Override
protected Class<?> findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
// 将class字节码文件读到字节数组
private byte[] loadClassData(String name) {
byte[] data=null;
InputStream in=null;
name=name.replace('.', '/');
ByteArrayOutputStream out=new ByteArrayOutputStream();
try {
in=new FileInputStream(new File(path+name+fileType));
int len=0;
while(-1!=(len=in.read())){
out.write(len);
}
data=out.toByteArray();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
in.close();
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return data;
}
// 重写toString方法, 以便后续this.getClass().getClassLoader()
@Override
public String toString() {
// TODO Auto-generated method stub
return this.name;
}
}
如果你想要打破双亲委派模型,那么你需要重写loadClass方法,ClassLoader的loadClass方法,大家可以去看下源码。在这里我就不打破双亲委派模型了。
2. 测试
被加载的类:
public class One{
public One() {
// 在控制台输出加载当前类的类加载器
System.out.println("One: i am loaded by " + this.getClass().getClassLoader());
}
}
------------------
public class Two{
public One() {
// 在控制台输出加载当前类的类加载器
System.out.println("Two: i am loaded by " + this.getClass().getClassLoader());
}
}
Main方法:
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// 创建自己的根类加载器, 默认父类加载器是java的系统类加载器, 并设置加载路径
MyClassLoader myClassLoader0 = new MyClassLoader("myRootClassLoader");
myClassLoader0.setPath("/app/zero/");
// 创建自己的拓展类加载器, 父类为自定义根类加载器, 并设置加载路径
MyClassLoader myClassLoader1 = new MyClassLoader(myClassLoader0, "myExtendClassLoader");
myClassLoader1.setPath("/app/one/");
// 调用自定义拓展类加载器加载程序中不存在但存在于/app/zero/中的class文件
Class<?> aClass1 = myClassLoader1.loadClass("com.wangxs.springcloud.classloader.One");
aClass1.newInstance(); // 输出结果应该是 myRootClassLoader
}
上述代码,我们自定义的根类加载器myClassLoader0,父类为java系统类加载器,再定义一个拓展类加载器myClassLoader2,父类为myClassLoader1,现在One.class文件存在于/app/zero/目录下,而不存在于程序类路径下,那我们用自定义拓展类加载器myClassLoader2去加载One.class,由双亲委派机制,我们可以推断出,会向上委托至java系统类加载器去加载One.class,但是系统类加载器发现自己不能加载该类,便反向委托给自定义根类加载器myClassLoader0去加载,输出结果应为:myRootClassLoader
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
// 创建自己的系统类加载器, 无父类加载器, 设置加载路径
MyClassLoader myClassLoader2 = new MyClassLoader(null, "myAppClassLoader");
myClassLoader2.setPath("/app/two/");
// 加载程序中不存在, 但在/app/two/中存在的class文件
Class<?> aClass2 = myClassLoader2.loadClass("com.wangxs.springcloud.classloader.Two");
aClass2.newInstance(); // 输出结果应该是 myAppClassLoader
}
上述代码创建了自定义系统类加载器myClassLoader2,无父类加载器,那么直接由该加载器加载类,结果应为myAppClassLoader
以上结果如下图:
五、应用场景
1. 加密保护
公司的有些核心类库的字节码是经过加密的,这样的话,就需要实现自己的加载器,在加载这些类库的时候进行解密,然后再载入到内存
2. 其他来源
字节码是放在数据库,硬盘其他路径,甚至有可能放在云上。
以上便为自己学习JVM类加载器相关知识的总结,之前的文章也介绍了类加载过程以及垃圾回收机制,大家可以去看下。
以上是关于面试必看-Java类加载器(自定义类加载器)的主要内容,如果未能解决你的问题,请参考以下文章