类加载器的深入解析及重要特性剖析(三)

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存在时,classLoaderTest1classLoaderTest2会委托系统类加载器加载类,所以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删除,这时我们自定义的类加载器,去委托系统类加载器去加载就会加载不到,所以最终由我们的自定义的类加载器去加载,因为我们的classLoaderTest1classLoaderTest2没有直接或者间接的父子关系,所以这两个加载器加载器会在两个不同的命名空间去加载,所以NameSpaceTest的类对象会被加载两遍,而报错信息java.lang.ClassCastException: com.jvm.test.NameSpaceTest cannot be cast to com.jvm.test.NameSpaceTest
主要是因为不同的命名空间下类对象是不可见的。
在这里插入图片描述
结论:

  • 同一个命名空间的类是相互可见的。
  • 子类加载器的命名空间包含所有父类加载器的命名空间。因此由子类加载器加载的类能看见父类加载器加载的类。如系统类加载器加载的类能看见根类加载器加载的类。
  • 由父类加载器加载的类不能看见子类加载器加载的类。
  • 如果两个类加载器没有直接或者间接的父子关系,那么他们各自加载的类相互不可见。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值