JVM(2)类加载器详解

一、类加载器

     虚拟机设计团队把类加载阶段中“通过一个类的全限定名来获取描述该类的二进制字节流” 这个动作放到了Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的的类,实现这个动作的代码模块称为"类加载器"。

二、类加载器的分类

1.启动类加载器(Bootstrap ClassLoader):也称根类加载器,这个类加载器是有C++语言实现,是虚拟机的一部分。这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib中也不会被识别,这是针对放在lib中的,对于其他的路径没有这个规定如放到jdk\jre\classes\中的只要是class字节码文件就可以)。
2.扩展类加载器(Extension ClassLoader):这个类加载器,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
3.应用程序类加载器(Application ClassLoader):这个类加载器由"sun.misc.Launcher$AppClassLoader"实现,由于这个类加载器是有ClassLoader中的getSystemClassLoader()的返回值,所以一般也称为系统类加载器。它负责加载用户类路径(classpath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过类加载器,一般情况下系统类加载器就是程序中默认的类加载器,我们写的类一般也是由这个类加载器加载。
我们的程序应用都是由这3中类加载器互相配合进行加载的,如果有必要也可以添加自己自定义的类加载器。
4.线程上下文类加载器:通过Thread.currentThread().getContextClassLoader()获得。默认值是应用程序类加载器。当然也可以通过setContextClassLoader来设置其值。

二、双亲委派机制

类加载器之间如下图所示:
在这里插入图片描述
双亲委派机制要求除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器,这里面的类加载器的父子关系不是继承关系实现的而是组合的关系,包含父类的引用。
双亲委派机制工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个了,而是把这个请求委托给父加载器去完成, 每一个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层启动类加载器中,只有当父加载器反馈其无法加载(它的搜索范围中没有找到所需要的的类)时,自加载器才会尝试自己去加载。实现双亲委派的代码都集中在ClassLoader的loadClass()方法中如下图:

  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) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父类加载器抛出异常ClassNotFoundException
                    // 说明父类加载器无法加载请求
                }
                if (c == null) {
                    // 父类加载器无法加载请求时
                    // 在调用本身的findClass进行加载
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

以上是概念介绍的部分,下面用程序来了实际感受下类加载器。

  public static void main(String[] args){
        System.out.println("系统类加载器:"+ClassLoader.getSystemClassLoader());
        System.out.println("扩展类加载器:"+ClassLoader.getSystemClassLoader().getParent());
        System.out.println("启动了加载器:"+ClassLoader.getSystemClassLoader().getParent().getParent());
    }

我们知道通过ClassLoader.getSystemClassLoader()可以得到系统类加载器,每个加载器之间存在父子关系,ClassLoader中提供了getParent()方法可以显示获取响应的类加载器在java程序中的表现形式。运行结果如下图:
在这里插入图片描述
由此可见在oracle HotSpot虚拟机中启动类加载器是用null标识的。

以上是java中提供的类加载器。我们也可以自定义自己的类加载器,通过自定义自己的类加载器可以对类加载双亲委托机制,以及类加载器命名空间等问题有更深入的了解。
java中要求自定义自己的类加载器需要继承ClassLoader这个抽象类。主要先去大致的了解这个抽象类(有时间的小伙伴可以去研究下源码),然后再去自定义一个自己的类加载器。类加载器中有很多重要的方法如findClass defineClass等。自定义类加载器需要继承ClassLoader类并且重写findClass()。

下面来自定义的一个ClassLoader:

import java.io.*;

public class MyClassLoader extends ClassLoader{
    private static final String classExtepsion = ".class";
    private  String classLoaderName;
    private String path;

    public String getPath() {
        return path;
    }

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

    public MyClassLoader(String classLoaderName){
        super();//应用程序类加载器为该类的父类加载器
        this.classLoaderName = classLoaderName;
    }
    public MyClassLoader(ClassLoader parent,String classLoaderName){
        super(parent);//指定一个类加载器为该类的父类加载器
        this.classLoaderName = classLoaderName;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        System.out.println("自定义的类加载器className:"+className+"执行了!");
        //System.out.println("classLoader:"+this.getClass().getClassLoader());
        byte[] data = this.loadClassData(className);
       return defineClass(className,data,0,data.length);
    }

    public byte[] loadClassData(String className){
        String name = className.replace(".","\\");
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream bos = null;
        try {
            is = new FileInputStream(new File(this.path+name+classExtepsion));
            bos = new ByteArrayOutputStream();
            int c = 0;
            while((c=is.read())!=-1){
                bos.write(ch);
            }
            data = bos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                is.close();
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }
    public static void main(String[] args) throws Exception{
        MyClassLoader loader1 = new MyClassLoader("loader1");
        Class<?> classzz = loader1.loadClass("com.newgain.classloader.Test1");
        System.out.println("class:"+classzz);
    }
}

自定义的ClassLoader中有几个需要注意的点。
1.两个构造方法,第一个构造方法调用了ClassLoader中的无参数构造,指定系统类加载器为我们当前自定义类加载器的父类,第二个构造方法调用的父类的有参构造,可以自定义传入一个加载器为当前构造器的父类加载器。
2.自定义构造器实际上就是重写了findClass把文件通过流的方式读取到内存中,并通过defineClass()创建Class对象。

上面程序的运行结果为:
class:class com.newgain.classloader.Test1

由此可见我们自定义的类加载器成功加载类了。是不是很高兴。然并卵,我们在findClass()方法加入了输出语句,并没有输出。说明根本没走我们自定义的类加载器来加载Test1类。那是什么加载器加载的呢?我们在Test1类中增加如下的构造方法:
public Test1(){
System.out.println(“加载Test1的类加载器为:”+this.getClass().getClassLoader());
}
并在得到Class对象后通过反射的方法实例化对象,增加如下语句到自定义类加载器的main方法最后面:
Object object = classzz.newInstance();
重新运行,我们会得到如下输出结果:
class:class com.newgain.classloader.Test1
加载Test1的类加载器为:sun.misc.Launcher$AppClassLoader@18b4aac2

我们可以看出是系统类加载器加载了Test1这个类。为什么呢,其实这就是双亲委派机制的体现。在创建我们自定的类加载器对象时,我们使用的是一个参数的构造器,也就是会默认系统类加载器为我们的父类加载器。根据双亲委派机制。自定义的类加载器会把请求委托给父类加载,以次类推。最后只有系统类加载器的类路径classpath,也就是当前项目可以找到这个Test1.class字节码文件并进行加载。

那么我们如何使用自己的类加载器加载字节码文件呢?
我们知道上面提到的3大加载器,每个加载器都有其指定的加载路径(在没有修改的情况下)。我们可以给我们的类加载器自定义一个加载路径,并把classpath中的Test1这个class文件删除。那么最终父类无法加载此Test1也就会使用我们自定义的类加载器来加载Test1这个类了。
我们把连同包名把Test1复制到桌面。并把项目中的Test1.class删除。然后修改下自定义类加载器中的main方法,代码如下:

MyClassLoader loader1 = new MyClassLoader("loader1");
loader1.setPath("C:\\Users\\development\\Desktop\\");
Class<?> classzz = loader1.loadClass("com.newgain.classloader.Test1");
System.out.println("class:"+classzz);
Object object = classzz.newInstance();

运行结果如下:
在这里插入图片描述
可以看到我们的类加载器执行了。

我们在修改下上面的main方法,在创建一个loader2类加载器,一起加载桌面的Test1,代码如下:

 MyClassLoader loader1 = new MyClassLoader("loader1");
        loader1.setPath("C:\\Users\\development\\Desktop\\");
        Class<?> classzz = loader1.loadClass("com.newgain.classloader.Test1");
        Object object = classzz.newInstance();
        System.out.println(classzz.hashCode());
        Object object = classzz.newInstance();
        MyClassLoader loader2 = new MyClassLoader("loader2");
        loader2.setPath("C:\\Users\\development\\Desktop\\");
        Class<?> classzz1 = loader2.loadClass("com.newgain.classloader.Test1");
        System.out.println(classzz1.hashCode());
        Object object1 = classzz.newInstance();

运行结果如下:
在这里插入图片描述

得到的2个class的hashcode值不一样,说明加载得到的2个class对象是不相等的。2个加载器加载了同一个class文件但是得到的是不同的class对象,这是为什么呢。由此引出类加载器中另外一个重要的概念,命名空间。
命名空间是指:,一个类加载器和其父类加载器所加载的类组成一个命名空间。而loader1和loader2属于2个不同的类加载器,他们所加载得到的Class就不一样。类加载器不仅仅是加载类,对于任意一个类,都需要有加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。每个类加载器都有一个独立的类名称空间。同俗一点讲判断一个2个类对象是否相等。要建立在是由同一个类加载器类加载器加载的。如果不是。那么这两个Class对象肯定不相等。

接下来我们在添加2个类,并把类的这2个类复制到桌面的同包名下代码如下:

public class MySample {
    public MySample(){
        System.out.println("MySample class load by classloader:"+this.getClass().getClassLoader());
        new Mycat();//主动使用  加载类会默认使用当前类的类加载器加载此类	
    }
}

public class Mycat {
    public Mycat(){
        System.out.println("Mycat class load by classloader:"+this.getClass().getClassLoader());
    }
}

还要使用刚刚自定义的MyClassLoader类。创建一个测试类Test,代码如下;

 public static void main(String[] args) throws Exception{
        MyClassLoader loader1 = new MyClassLoader("loader1");
        loader1.setPath("C:\\Users\\development\\Desktop\\");
        Class classzz = loader1.loadClass("com.newgain.classloader.MySample");
        System.out.println("class:"+classzz.hashCode());
        Object object = classzz.newInstance();
    }

正常运行结果如下:
在这里插入图片描述
我们先对以上程序做第一次改动:把classpath中的MySample.class和Mycat.class删除。再次运行下程序,得到如下结果:
在这里插入图片描述
分析下改动前和改动后的程序。我们在MySample类中创建Mycat对象,就是主动使用这个类,那么这个类会进行加载。那么程序就会默认当前类的加载器(即MySamle类的类加载器)为new 对象的默认加载器,当然也会遵循双亲委托机制。

下面我们来做第二次变动,重新rebuild下项目,把MySample.class和Mycat.class恢复到项目当中去,再把classpath中的MySample.class删除。然后在Mycat构造方法中添加一句代码如下:

public Mycat(){
        System.out.println("Mycat class load by classloader:"+this.getClass().getClassLoader());
        System.out.println("MySample is from classloader:"+MySample.class.getClassLoader());
    }

得到如下的运行结果:
在这里插入图片描述
可以看到的是程序报错了。原因就处在我们添加的这句代码:
System.out.println(“MySample is from classloader:”+MySample.class.getClassLoader());找不到MySample这个类class,我们注释掉这句话,修改下MySample的构造方法,打印输出Mycat的ClassLoader,看看会不会报错。MySample的代码修改为;

  public MySample(){
        System.out.println("MySample class load by classloader:"+this.getClass().getClassLoader());
        new Mycat();//主动使用  加载类会默认使用当前类的类加载器加载此类
        System.out.println("Mycat is from classLoader:"+Mycat.class.getClassLoader());
    }

在运行下程序,结果如下:
在这里插入图片描述
没有报错,分析下这2次改动,首先确认MySample是自定义的类加载器加载的MyClassLoader加载的,Mycat是系统类加载器加载的,这2个类加载器是子类和父类的关系。在父加载器加载的Mycat类中访问子类加载器加载的类MySample报错找不到类,在子类加载器加载的MySample类中访问父类加载器加载的Mycat类可以正常访问。

由此得到如下的结论:子类加载器加载的类可以访问父类加载器加载的类,但是父类加载器加载的类无法访问子类加载器加载的类。原因其实很简单。也是双亲委托机制导致的。其实就是双亲委托机制的约束,导致父类加载器无法委托子类加载器去加载类(父类加载器中加载的类无法获取子类加载器然后再去加载子类,当然前提是子类只能子类加载器去加载)。委托机制是向上委托,(可以通过上面的类加载器源码看出)委托不了自己在加载,这也是双亲委托机制的缺陷,就导致出现了后来的线程上下文类加载器。

可以通过代码获取类加载器的加载路径,由上而下分别为根类加载器,扩展类加载器,系统类加载器。

        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.class.path"));

其实各个类加载器的路径是可以通过java -D+路径 = 路径 来进行更改的。更改之后也可以通过根类加载器和扩展类加载器来加载自己定义的类,或者把字节码文件放到对应加载器的路径当中。注意扩展类加载器加载文件时必须先将其打包成jar包之后才能进行加载。这里就不做演示了。

线程上下问类加载器:如果全部遵循双亲委托机制,那么Java中有很多功能是实现不了。SPI(Service provider Interface)服务提供者接口就是基于线程上下文类加载器来完成的。SPI中的代码是在Java核心库中由启动类加载器加载的,具体的实现类是由各个厂商去完成的。使用时是将实现类放到classpath中。但是当SPI中使用实现时,根据双亲委托机制,会通过启动类加载器去加载实现类,但是肯定加载不了,这时候就需要获取线程上下文类加载器(系统类加载器)去加载(实例化)实现类。
线程上下文类加载器默认是系统类加载器。在jvm初始化的时候。也提供的想要的方法去手动设置。我们一般使用的时候分为三步,获取,使用,还原(恢复为系统类加载器)。

补充一个类容:类的卸载。类加载分为加载,连接,初始化,使用,卸载,在此补充卸载的原因是因为只有自定义类加载器加载的类才可以被卸载。同样使用上面的自定义类加载器。修改下main方法里面的代码:

   MyClassLoader loader1 = new MyClassLoader("loader1");
        loader1.setPath("C:\\Users\\development\\Desktop\\");
        Class<?> classzz = loader1.loadClass("com.newgain.classloader.Test1");
        Object object = classzz.newInstance();
        System.out.println(classzz.hashCode());
        /*Object object = classzz.newInstance();
        System.out.println(object);*/
        loader1 = null;
        classzz = null;
        object = null;
        System.gc();
        Thread.sleep(100000);
        loader1 = new MyClassLoader("loader1");
        loader1.setPath("C:\\Users\\development\\Desktop\\");
        classzz = loader1.loadClass("com.newgain.classloader.Test1");
        object = classzz.newInstance();
        System.out.println(classzz.hashCode());
        System.out.println(object);

删除classpath中的Test1.class用自定义加载器加载。通过jvisualvm命令打开Java Visual 点击监视在已卸载总数里面可以看到类已卸载总数是1;
在这里插入图片描述
当然也可以通过添加VM options -XX:+TraceClassUnloading来打印输出卸载信息。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值