JVM加载类的方式及存在的问题(附带代码演示)

JVM当中提供了两种类加载器

1、Java虚拟机自带的类加载器

     根类加载器(Bootstrap)、扩展类加载器(Extension)、系统(应用)类加载器(System)

2、自定义的类加载器

   用户可以通过继承java.lang.ClassLoader类,然后重写自己定义的类加载器。

何时加载一个类:

 类加载器并不需要等到某个类在首次主动使用时再加载它。JVM规范允许类加载器在预料到某个类将要被使用时就预加载它,如果在预加载的过程中出现了.class文件缺失或者存在错误,类加载器会在程序首次主动使用该类时才报告错误(LinkageError),如果这个类一直没有被主动使用,那么将不会抛出错误。

先来介绍下关于JVM自带的类加载器

1、根类加载器(Bootstrap): 该类加载器没有父类加载器,它负责加载虚拟机的核心类库,比如java.lang.* 包。根类加载器从系统属性sun.boot.class.path所指定的目录路中加载类库。根类加载器的实现依赖于底层操作系统,数据虚拟机的一部分,并没有继承java.lang.ClassLoader类。

2、扩展类加载器(Extension):它的父类加载器为根类加载器,它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext 子目录下加载类库,如果把用户创建的jar文件放在这个目录下,也会自动由扩展类加载器加载,扩展类加载器是java代码实现,继承自java.lang.ClassLoader类

3、系统类加载器(System):也称为应用类加载器,它的父类加载器为扩展类加载器,它从环境变量classpath或者系统属性java.class.path 所指定的目录加载类,它是用户自定义类加载器的父类,系统类加载器也是java代码,继承自java.lang.ClassLoader类。

双亲委派模型:当classLoader在寻找类或者资源之前,把这个动作委托为双亲去执行,如果双亲还有双亲,继续向上找,直到找到可以加载的类加载器,进行加载资源

接下来我们自定义实现了一个类加载器

public class MyTest16 extends ClassLoader {
    private String classLoaderName;
    private final String fileExtension = ".class";
    private String path;
    public MyTest16(String classLoaderName) {
        super();
        this.classLoaderName = classLoaderName;
    }

    public void setPath(String path) {
        this.path = path;
    }

    public MyTest16(ClassLoader parent, String classLoaderName){
        super(parent);
        this.classLoaderName = classLoaderName;
    }

    public MyTest16(ClassLoader classLoader) {
        super(classLoader);
    }
    private byte[] loadClassData(String name) {
        InputStream inputStream = null;
        byte[] data = null;
        ByteArrayOutputStream bos = null;
        try{
            name = name.replace(".","\\");
            inputStream = new FileInputStream(new File(path+name + this.fileExtension));
            bos = new ByteArrayOutputStream();
            int ch = 0;
            while(-1 != (ch = inputStream.read())) {
                bos.write(ch);
            }
            data = bos.toByteArray();
        }catch (Exception e) {
            try{
                inputStream.close();
                bos.close();
            }catch (Exception e1){
                e.printStackTrace();
            }
        }
        return data;
    }
    protected Class<?> findClass(String className) throws ClassNotFoundException{
        byte[] data = this.loadClassData(className);
        return this.defineClass(className,data,0,data.length);
    }

    public static void main(String[] args) throws Exception{
        MyTest16 loader1 = new MyTest16("loader1");
        loader1.setPath("C:\\Users\\Administrator\\Desktop\\");
        Class<?> clazz = loader1.loadClass("com.test.jvm.classloader.Test1");
        System.out.println(clazz.hashCode);
        System.out.println(clazz.getClassLoader());
        Object object = clazz.newInstance();
        System.out.println(object);

       

        MyTest16 loader2 = new MyTest16("loader2");        
       // MyTest16 loader2 = new MyTest16(loader1,"loader2");
        loader1.setPath("");
        Class<?> clazz2 = loader2.loadClass("com.test.jvm.classloader.Test1");        
        System.out.println(clazz2.hashCode());
        Object object2 = clazz2.newInstance();
        System.out.println(object2);

    }
}

关于上述自定义的代码,我们本地程序库当中包含有Test1.class文件,如果想要使用自定义的类加载器执行的话,就需要把本地程序当中生成好的class文件删除,将这个class文件放置在桌面上,这样当我们执行代码的时候就可以获取到Test1的类对象了,上述程序当中,同一个类,生成出来了两个不同的类对象,显然是不符合条件的,出现以上情况的原因在于命名空间不同的原因

命名空间

   什么是命名空间?

  每个类加载器都有自己的命名空间,命名空间由该类加载器及父类加载器构成

  在同一个命名空间当中,不会出现类的完整名字(包括类的包名)相同的两个类

  在不同的命名空间当中,有可能会出现类的完整名字(包括类的包名)相同的两个类

在以上的代码例子中loader1加载器的命名空间和loader2的命名空间是完全不同的,因此会出现两个不同的Test1对象,我们可以通过给出的另外一个构造方法,在创建loader2时,使用loader1作为其父类加载器,这样子的话,loader1和loader2创建出来的class对象就是相同的,原因在于在同一个命名空间中,不会出现相同的两个类

不同类的不同加载器

public class MyCat {
   public MyCat() {
       System.out.println("MyCat is loaded by :" + this.getClass().getClassLoader());
     
   }
}

public class MySample {
    public MySample() {
        System.out.println("MySample is loaded by : " + this.getClass().getClassLoader());
        new MyCat();
      
    }
}
public class MyTest18 {
    public static void main(String[] args)throws Exception {
        MyTest16 loader1 = new MyTest16("loader1");
        loader1.setPath("C:\\Users\\Administrator\\Desktop\\");
        Class<?> clazz = loader1.loadClass("com.test.jvm.classloader.MySample");
        System.out.println("clazz: "  + clazz.hashCode());
        //如果注释掉改行,不会实例化MySample对象,即MySample构造方法不会被调用,因此不会实例化MyCat对象。
        Object object = clazz.newInstance();

    }
}

1、上述代码如果正常执行的话,打印出来的信息是

clazz: 617901222
MySample is loaded by : sun.misc.Launcher$AppClassLoader@18b4aac2
MyCat is loaded by :sun.misc.Launcher$AppClassLoader@18b4aac2

2、如果我们删除掉程序当中的MySample.class文件,保留MyCat.class文件

clazz: 925858445
MySample is loaded by : com.shengsiyuan.jvm.classloader.MyTest16@24d46ca6
MyCat is loaded by :sun.misc.Launcher$AppClassLoader@18b4aac2

3、如果我们保留程序当中MySample.class文件,删除MyCat.class文件,MySample类使用的AppCluster进行加载,而MyCat类就会提示错误信息

clazz: 617901222
MySample is loaded by : sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: com/test/jvm/classloader/MyCat
	at com.test.jvm.classloader.MySample.<init>(MySample.java:9)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
	at java.lang.reflect.Constructor.newInstance(Unknown Source)
	at java.lang.Class.newInstance(Unknown Source)
	at com.shengsiyuan.jvm.classloader.MyTest18.main(MyTest18.java:13)
Caused by: java.lang.ClassNotFoundException: com.test.jvm.classloader.MyCat
	at java.net.URLClassLoader.findClass(Unknown Source)
	at java.lang.ClassLoader.loadClass(Unknown Source)
	at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
	at java.lang.ClassLoader.loadClass(Unknown Source)
	... 7 more

总结:如果当前类使用的是子类加载器,对于当前类当中引入的其他类的加载,会尝试从父类加载器当中加载,如果父类加载器可以进行加载,就会进行加载,如果父类加载器不能加载就会使用当前类所使用的类加载器加载。如果当前类加载器不能加载,就会抛出异常,即便当前类的子类加载器可以加载引入的类。

对上述的MyCat代码进行修改

public class MyCat {
   public MyCat() {
       System.out.println("MyCat is loaded by :" + this.getClass().getClassLoader());
       System.out.println("from myCat :"+ MySample.class);
   }
}

其余代码均不变,我们再次执行测试代码

1、删除MySample.class文件,保留MyCat.class文件,执行结果如下

Exception in thread "main" java.lang.NoClassDefFoundError: com/test/jvm/classloader/MySample
	at com.test.jvm.classloader.MyCat.<init>(MyCat.java:9)
	at com.test.jvm.classloader.MySample.<init>(MySample.java:9)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Source)
	at java.lang.reflect.Constructor.newInstance(Unknown Source)
	at java.lang.Class.newInstance(Unknown Source)
	at com.test.jvm.classloader.MyTest18.main(MyTest18.java:13)
Caused by: java.lang.ClassNotFoundException: com.test.jvm.classloader.MySample
	at java.net.URLClassLoader.findClass(Unknown Source)
	at java.lang.ClassLoader.loadClass(Unknown Source)
	at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
	at java.lang.ClassLoader.loadClass(Unknown Source)
	... 8 more
clazz: 925858445
MySample is loaded by : com.test.jvm.classloader.MyTest16@24d46ca6
MyCat is loaded by :sun.misc.Launcher$AppClassLoader@18b4aac2

对于上述错误产生的原因:MySample类是自定义类加载器加载的,MyCat类是AppClassLoader应用类加载器加载的,当我们在应用类加载器加载的类中访问其子类加载器加载的对象时,就会抛出错误

public class MySample {
    public MySample() {
        System.out.println("MySample is loaded by : " + this.getClass().getClassLoader());
        new MyCat();
        System.out.println("from MySample: " + MyCat.class);
    }
}

public class MyCat {
   public MyCat() {
       System.out.println("MyCat is loaded by :" + this.getClass().getClassLoader());
       //System.out.println("from myCat :"+ MySample.class);
   }
}

修改MySample类,在MySample类当中访问MyCat,测试类不变

1、删除MySample类,保留MyCat类

打印结果

clazz: 925858445
MySample is loaded by : com.test.jvm.classloader.MyTest16@24d46ca6
MyCat is loaded by :sun.misc.Launcher$AppClassLoader@18b4aac2
from MySample: class com.test.jvm.classloader.MyCat

MySample类是通过自定义类加载器加载的,MyCat是父类加载器加载的,自定义加载器加载出来的是对象是可以访问父类加载器加载出来的对象。

总结:子类加载器可以访问父类加载器加载出来的类,而父类加载器不能访问子类加载器加载出来的对象。

不同类加载器命名空间之间的关系

public class MyPerson {
    private MyPerson myPerson;
    public void setMyPerson(Object object) {
        this.myPerson = (MyPerson) object;
    }
}
public class MyTest21 {
    public static void main(String[] args) throws Exception {
        MyTest16 loader1 = new MyTest16("loader1");
        MyTest16 loader2 = new MyTest16("loader2");
        loader1.setPath("C:\\Users\\Administrator\\Desktop\\");
        loader2.setPath("C:\\Users\\Administrator\\Desktop\\");
        Class<?> clazz1 = loader1.loadClass("com.test.jvm.classloader.MyPerson");
        Class<?> clazz2 = loader2.loadClass("com.test.jvm.classloader.MyPerson");
        System.out.println(clazz1 == clazz2);

        Object object1 = clazz1.newInstance();
        Object object2 = clazz2.newInstance();

        Method method = clazz1.getMethod("setMyPerson",Object.class);
        method.invoke(object1,object2);
    }
}

1、上述代码执行结果为true, 原因在于他们都是用的AppClassLoader进行加载

2、如果我们删除掉MyPerson的class文件,使用自定义的类加载器进行加载,执行结果如下:

Exception in thread "main" false
java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
	at java.lang.reflect.Method.invoke(Unknown Source)
	at com.test.jvm.classloader.MyTest21.main(MyTest21.java:22)
Caused by: java.lang.ClassCastException: com.test.jvm.classloader.MyPerson cannot be cast to com.test.jvm.classloader.MyPerson
	at com.test.jvm.classloader.MyPerson.setMyPerson(MyPerson.java:9)
	... 5 more

由两个不同的类加载器创建出来的Class对象,其之间并不能互相调用,clazz1是由loader1加载的,clazz2是由loader2加载的,loader1和loader2之间并没有继承关系,也不存在双亲委派关系,所以他们创建出来的对象,彼此是无法使用的。

双亲委派模型的特点

1、可以确保java类库的安全性,所有的java应用都至少会引用java.lang.Object类,在运行期,java.lang.Object类会被加载到java虚拟机中,如果这个加载过程是由java应用自己的类加载器加载进去的,很有可能在jvm当中存在多个版本的Object类对象,而这些类之间互相还不兼容,不可见。借助双亲委派模式,java核心类库都是由启动类加载器同一完成,从而保证了java应用所使用的都是同一个版本的java类库,

2、可以确保核心类库所提供的类不会被自定义的类取代

3、不同的类加载器可以为相同名称的类创建额外的命名空间,相同名称的类可以并存在java虚拟机中,只需要不同的类加载器来加载他们即可。不同的类加载器加载的类之间是不兼容的。相当于在java虚拟机内部创建了一个又一个相互隔离的java类空间。

类加载器是由哪个类来加载的?

我们知道jvm提供的三种类加载器,分别是根类加载器,扩展类加载,应用类加载器,除了根类加载器,另外两个类加载器本身也是java类编写的,那么他们是在什么如何被加载的呢?

   ExtClassLoader和AppClassLoader是由Bootstrap类加载器进行加载的,Bootstrap类加载器是用c++编写的,并且内嵌于jvm虚拟机当中,Bootstrap类加载器也叫启动类加载器,是特定于平台的指令,负责开启整个家在过程,所有的类加载器,除了启动类加载器,都是java类编写的,总是需要一个组件来加载java类,从而能够让整个类顺利的执行下去,加载第一个纯java类加载器就是启动类加载器的职责。

关于数组的类加载器,如果数组是元素是引用类型,其类加载器是数组元素的类加载器,如果数组是原生数据类型,那么其类加载器为null,

参考:圣思园-jvm课程

猜你喜欢

转载自blog.csdn.net/summerzbh123/article/details/81096531
今日推荐