面试:什么是类的加载器

类装载工作由ClassLoader及其子类负责,ClassLoader是一个重要的java执行时系统组件,它负责在运行时查找和装入Class字节码文件。
JVM在运行时会产生三个ClassLoader:Bootstrap(启动类加载器)、ExtensionClassLoader(扩展类装载器)和AppClassLoader(系统类装载器)。
image.png
上代码:

  1. public class TestClassLoad {
  2. public static void main(String[] args) {
  3. System.out.println("1********启动类加载器*********");
  4. //1.启动类加载器,从sun.boot.class.path系统属性,所指定的目录加载类库
  5. String bootclass= System.getProperty("sun.boot.class.path");
  6. for (String path : bootclass.split(":")){
  7. System.out.println(path);
  8. }
  9. //2.从上面的路径中随意选择一个类, 看看它的类加载器是什么?
  10. //Provider位于 /jdk1.8.0_171.jdk/Contents/Home/jre/lib/jsse.jar 下
  11. ClassLoader classLoader = Provider.class.getClassLoader();
  12. System.out.println("启动类加载器的加载类:"+classLoader);//null ,就是加载器取不到,因为顶级启动类加载器为null
  13. System.out.println();
  14. System.out.println("2********拓展类加载器********");
  15. //1.拓展类加载器,从java.ext.dirs系统属性,所指定的目录加载类库
  16. String extDirs = System.getProperty("java.ext.dirs");
  17. for (String path : extDirs.split(":")){
  18. System.out.println(path);
  19. }
  20. //2.从上面的路径中随意选择一个类, 看看它的类加载器是什么?
  21. ClassLoader classLoader1 = CurveDB.class.getClassLoader();
  22. System.out.println("拓展类加载器的加载类:"+classLoader1);//ExtClassLoader
  23. System.out.println();
  24. System.out.println("3********应用程序类加载器********");
  25. //1.应用程序类加载器,从java.class.path系统属性,所指定的目录加载类库
  26. String classpath= System.getProperty("java.class.path");
  27. for (String path : classpath.split(":")){
  28. System.out.println(path);
  29. }
  30. //2.从上面的路径中随意选择一个类, 看看它的类加载器是什么?
  31. ClassLoader classLoader2 = TestClassLoad.class.getClassLoader();
  32. System.out.println("应用程序类加载器的加载类:"+classLoader2);//AppClassLoader
  33. }
  34. }

image.png

面试:为什么需要自定义类加载器?什么情况下用到

用户在需要的情况下,可以实现自己的自定义类加载器,一般而言,在以下几种情况需要自定义类加载器:

  1. 隔离加载类。某些框架为了实现中间件和应用程序的模块的隔离,就需要中间件和引用程序使用不同的类加载器;
  2. 修改类加载的方式。类的双亲委派模型并不是强制的,用户可以根据需要在某个时间点动态加载类;
  3. 扩展类加载源,例如从数据库、网络进行类加载
  4. 防止源代码泄露。Java代码很容易被反编译和篡改,为了防止源码泄露,可以对类的字节码文件进行加密,并编写自定义的类加载器来加载自己的应用程序的类。

如何实现自定义类加载器?

若要自己实现一个类加载器,只需要继承java.lang.ClassLoader类,并且重写其findClass()方法即可。
java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中生成一个java.lang.Class实例。
ClassLoader的核心方法如下:

  1. getParent()返回该类加载器的父类加载器
  2. loadClass(String name) 加载名称为 二进制名称为name的类,返回的结果是java.lang.Class类的实例。
  3. findClass(String name)查找名称为 name的类,返回的结果是java.lang.Class类的实例。
  4. findLoadedClass(String name)查找名称为name的已经被加载过的类,返回的结果是java.lang.Class类的实例。

    talk is cheap,show me the code

    自定义类加载器: ```java // // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) //

package com.oyb.jvm.test01.lesson08;

import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException;

public class MyClassLoader extends ClassLoader { private String name; private String path;

  1. public void setPath(String path) {
  2. this.path = path;
  3. }
  4. public MyClassLoader(String name) {
  5. this.name = name;
  6. }
  7. MyClassLoader(ClassLoader parent, String name) {
  8. super(parent);
  9. this.name = name;
  10. }
  11. public Class<?> findClass(String name) {
  12. byte[] data = this.getDatas(name);
  13. return this.defineClass(name, data, 0, data.length);
  14. }
  15. private byte[] getDatas(String name) {
  16. name = name.replace(".", "/");
  17. File file = new File(this.path + name + ".class");
  18. System.out.println("加载二进制文件:" + file.getPath());
  19. FileInputStream in = null;
  20. ByteArrayOutputStream outputStream = null;
  21. if (file.exists()) {
  22. try {
  23. in = new FileInputStream(file);
  24. outputStream = new ByteArrayOutputStream();
  25. byte[] buffer = new byte[1024];
  26. boolean var6 = false;
  27. int size;
  28. while((size = in.read(buffer)) != -1) {
  29. outputStream.write(buffer, 0, size);
  30. }
  31. } catch (FileNotFoundException var17) {
  32. var17.printStackTrace();
  33. } catch (IOException var18) {
  34. var18.printStackTrace();
  35. } finally {
  36. try {
  37. in.close();
  38. } catch (IOException var16) {
  39. var16.printStackTrace();
  40. }
  41. }
  42. return outputStream.toByteArray();
  43. } else {
  44. return null;
  45. }
  46. }

}

  1. 自定义类Hello.class,并编译
  2. ```java
  3. package com.oyb.jvm.test01.lesson08;
  4. public class Hello {
  5. public static void main(String[] args){
  6. ClassLoader loader = Hello.class.getClassLoader();
  7. ClassLoader parent = loader.getParent();
  8. ClassLoader parent1 = parent.getParent();
  9. System.out.println("加载器:" + loader);
  10. System.out.println("父类加载器:" + parent);
  11. System.out.println("爷爷加载器:" + parent1);
  12. }
  13. }

如果是maven项目,编译之后在target目录下,有Hello.class文件
image.png
定义测试类:

  1. package com.oyb.jvm.test01.lesson08;
  2. import java.lang.reflect.InvocationTargetException;
  3. import java.lang.reflect.Method;
  4. public class ClassLoaderTest {
  5. public static void main(String[] args) throws InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
  6. //新建一个类加载器
  7. MyClassLoader cl = new MyClassLoader("myClassLoader");
  8. //类存放的路径
  9. cl.setPath("E:\\develop\\jvm\\jvm-study\\target\\test-classes\\");
  10. //查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例
  11. Class<?> classTemp = cl.findClass("com.oyb.jvm.test01.lesson08.Hello");
  12. System.out.println("hello的加载器:" + classTemp.getClassLoader());
  13. Method method = classTemp.getDeclaredMethod("main",String[].class);
  14. Object obj = classTemp.newInstance();
  15. String[] strings = {};
  16. method.invoke(obj, (Object)strings);
  17. }
  18. }

面试:为什么需要双亲委派模型?

创建类:

  1. package java.lang;
  2. public class String {
  3. public static void main(String[] args) {
  4. System.out.println("Aaa");
  5. }
  6. }

运行发现:
Error:(40, 20) java: 找不到符号
符号: 方法 replace(java.lang.String,java.lang.String)
位置: 类型为java.lang.String的变量 name
image.png
为了解决这个问题,不一致性安全性问题,设计了双亲委派模型
image.png
比如,定义了一个类,先在自定义类加载器去自己的内存区域去找,如果找不到,就说明还没有加载,所以要开始加载这个类,但是自定义加载器并不会自己去加载,它先会委派给自己的父加载器去加载,同样的,父类也会交给父类的父类去加载,如果到达了根加载器,根加载器加载失败了,那么就会反馈给子加载器,交给适合加载该类的加载器去加载。

拿我们之前定义的java.lang.String这个类来说,我们定义的类一般都是通过ApplicationClassLoader去加载的,一开始ApplicatonClassLoader先去内存中找,自己是否已经加载过该类了,发现并没有,于是准备去加载,这时候,它会先问问父类加载器有没有加载,就交给扩展类加载器,扩展类加载器并没有加载过,于是扩展类加载器就去跑去问父类加载器,即到达了根加载器,就是Bootstrap加载器,我们知道Bootstrap加载器在JVM启动的时候就会将java.lang下的所有类加载进JVM,所以此时Bootstrap已经加载过java.lang.String了,所以就会抛出下面的异常:

Error:(40, 20) java: 找不到符号
符号: 方法 replace(java.lang.String,java.lang.String)
位置: 类型为java.lang.String的变量 name
错误说明:根类加载器已经加载了系统的java.lang.String

源码剖析:从源码的角度,剖析双亲委派原理