欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java的类加载器ClassLoader

程序员文章站 2022-07-13 16:54:27
...

前言

 

ClassLoader java的类加载器,其作用就是把编译好的class文件或者jar包中对应的类的元数据加载到jvm的方法区,在堆中创建一个Class对象并返回,调用这个这个Class对象的newInstance()方法就可以创建一个指定类对应的对象。熟悉反射的朋友应该很清楚,如果再在前面加一段通过Class.forName()方法来获取Class对象的话,这其实就是java“反射获取对象的实现过程。

 

Class.forName()方法会调用forName0方法,这是一个jvm实现的native方法,里有一个重要的参数就是ClassLoader对象。Class.forName()方法是显示的获取一个Class对象的方法,jvm会调用根据参数里的ClassLoader对象来查询并加载一个Class对象。

 

Class.forName()方法是一种显示的获取Class对象的方法。这种方式一般是通过反射来创建java对象。我们平时用得更多的是使用new关键字来创建java对象,这个过程一个隐式创建java对象的过程:首先jvm会找到当前上下文的ClassLoader对象,通过这个ClassLoader对象来Load并创建一个Class对象;再通过Class对象的newInstance()方法实例化一个类对象。这个过程完全就jvm自己完成,所以称为隐式加载。通过这个过程,我们还可以发现,jvm是按需加载的,也就是说在new关键字被调用后,才会去加载对应的Class对象。否则如果一上来就把所有的jar包都加载到内存,势必一种内存空间的浪费;同时程序启动时间也会变长。

 

java自带的ClassLoader对象的创建过程

 

提示:本文中贴出的源码都是基于JDK1.8

ClassLoaderjava中定义的是一个抽象类,也说真实的ClassLoader对象是其子类对象。ClassLoader类中定义了一些通用的方法,比如需要一个ClassLoader时可以通过getSystemClassLoader()方法:

public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader(); //初始化ClassLoader对象
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {//权限检查
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
        return scl;
    }
 

 

这个方法没什么好说的关键就是调用initSystemClassLoader()方法 对ClassLoader对象进行初始化。initSystemClassLoader这方法不多说,其中核心部分是创建了一个Launcher对象:

sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

 

java自带的类加载器对象就是在Launcher类中创建的。下面我们主要来看下Launcher类,这个类从整体上来看运用了一个 “类单例模式”,大致结构如下:

public class Launcher {
    private static Launcher launcher = new Launcher();
    private ClassLoader loader;
    //省略其他成员变量
 
    public static sun.misc.Launcher getLauncher() {
        return launcher;
    }
    //公有的构造方法,与典型的单例模式有点差别
    public Launcher() {
        //省略方法体
}
//省略其他方法
}
 

 

这是一种典型的饿汉式单例模式(除了构造方法是public),在ClassLoadinitSystemClassLoader()方法中就是通过getLauncher()方法来获取Launcher对象的,可以看到通过这个方法获取的对象,每次都是同一个。

 

Launcher的构造方法做了三件事:创建ExtClassLoader类加载器对象;创建AppClassLoader类加载器对象;创建SecurityManager安全策略对象。关于java的安全策略这里就不展开了,感兴趣的可以异步到这里https://www.cnblogs.com/yiwangzhibujian/p/6207212.html。下面是该构造方法源码:

 

public Launcher() {
        //创建ExtClassLoader类加载器对象
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }
 
        //创建AppClassLoader类加载器对象
        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
        //把AppClassLoader类加载器对象设置到上线文
        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
       
        //创建SecurityManager安全策略
        if(var2 != null) {
            SecurityManager var3 = null;
            if(!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                    ;
                } catch (InstantiationException var6) {
                    ;
                } catch (ClassNotFoundException var7) {
                    ;
                } catch (ClassCastException var8) {
                    ;
                }
            } else {
                var3 = new SecurityManager();
            }
 
            if(var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }
 
            System.setSecurityManager(var3);
        }
 
    }
 

 

可以看到ExtClassLoaderAppClassLoader是二者都是Launcher的内部类,并且都继承自URLClassLoader;而URLClassLoader又都继承自前面提到的抽象类ClassLoader。二者的区别是AppClassLoader的父加载器是ExtClassLoader,而ExtClassLoader的父加载器为空(AppClassLoader的构造方法需要一个ExtClassLoader对象)。

 

Launcher的构造方法首次执行后,Launcher的单例对象就创建完成。下面继续返回ClassLoader抽象类。

 

当需要加载一个Class对象时:

1、首先通过ClassLoadergetSystemClassLoader()方法获取到到当前的ClassLoader对象(AppClassLoader对象);

2、然后调用该ClassLoader对象的LoadClass方法loadClass()方法进行加载。

下面就来看LoadClass方法的实现流程:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 检查该Class对象是否已经加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //如果有父加载器就交给父加载器加载
                    //否则就交给BootstrapClassLoader 加载器加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                //如果父加载器 以及BootstrapClassLoader加载器都没有加载,就自己加载
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //自己加载,如果加载不到就抛出ClassNotFoundException异常。
                    //该方式收抽象方法,交给子类实现
                    c = findClass(name);
 
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
 

 

LoadClass方法的核心流程如下:

1、首先通过findLoadedClass方法,检查自己是否已经加载过该class对象。最终调用的是native方法findLoadedClass0进行检查,由于是jvm实现的看不到源码。可以猜测到已经该类加载器 加载过的class对象,应该是被缓存起来了。这里还可以猜测到“类加载器”和class对象之前相互持有对方的引用,这也就是为什么class对象 以及方法区很难被“垃圾回收”的原因,除非首先想办法回收掉“类加载器”或者切断二者之间的引用关系(要自己去实现很复杂,OSGI在回收Bundle时,应该就是这么做的)。

 

2、类加载器,首先不自己加载,而是交给自己的父类加载器加载。从前面的讲解我们可以得知getSystemClassLoader()方法获取的真实的ClassLoaderAppClassLoader,其父类加载器是ExtClassLoader;父加载器ExtClassLoader又会重复步骤1检查自己是否已经加载过,否则到步骤2,此时ExtClassLoader的父加载器为空,就会交给BootstrapClassLoader加载器加载。

 

3、如果步骤2中,父加载器ExtClassLoader、以及BootstrapClassLoader加载器都没有加载,则自己调用findClass方法进行加载。

 

在上述三个步骤中提到的三个类加载器:BootstrapClassLoaderExtClassLoaderAppClassLoader,下一节再详细说明这三者的关系。先来看下findClass方法,在ClassLoader中是抽象方法,在UrlClassLoader中进行了实现,内容就比粘贴了,大致就是URI路径找到class文件,然后调用自己defineClass方法进行取去文件内容到ByteBuffer中,最后再调用父类ClassLoader中的defineClass方法进行加载,该方式是native的,由jvm实现。简易序列图如下:


Java的类加载器ClassLoader
            
    
    博客分类: jvm java ClassLoader 
 

简单的理解就是 loadClass方法-->调用findClass方法-->调用defineClass方法

 

findClass方法一般交由子类子类字节去实现从哪里找“class”文件,UrlClassLoader的实现是在一个目录下找。当然也可以在网络中去找,只要把文件内容读取到ByteBuffer中就行。

 

至此关于类加载器如何加载一个Class对象的流程就分析完毕。

 

java中自带的三个类加载器

 

在上一节中,可以看到java中自带的三个类加载器:BootstrapClassLoaderExtClassLoaderAppClassLoader。准确的说BootstrapClassLoader应该是JVM自带的类加载器,ExtClassLoaderAppClassLoaderjdk api中定义的类加载器,他们都是ClassLoader的间接子类,并且是Launcher的内部类。

 

三个类加载器的最大区别就是 他们各自加载类的路径不同,也就是我们常说的ClassPath。它们的类路径在Launcher类中都可以看到:

 

BootstrapClassLoader:读取系统配置sun.boot.class.path,默认配置路径为:System.out.println(System.getProperty("sun.boot.class.path"));

 

ExtClassLoader:读取系统配置java.ext.dirs,默认配置路径为:System.out.println(System.getProperty("java.ext.dirs"));

 

AppClassLoader:读取系统配置java.class.path,默认配置路径为:System.out.println(System.getProperty("java.class.path"));

 

执行上面的System.out.println就可以看到你电脑中,三个类加载器的加载路径。简单的理解:

BootstrapClassLoader加载的是java的核心包,比如rt.jar

ExtClassLoader加载的是扩展包 $JAVA_HOME/ lib/ext

AppClassLoader加载的是自己应用开发的jar包(或者class文件)。

 

java 类加载器的父优先原则

 

从第二节中,我们还可以看到java类加载器的父优先原则 即:在需要加载一个Class对象时,AppClassLoader判断自己是否加载过,如果没有,自己先不加载,而是交给ExtClassLoaderExtClassLoader判断自己是否加载过,如果没有就交给BootstrapClassLoader进行加载。

 

反过来,如果BootstrapClassLoader发现这个class文件不在自己的类路径下,就交给ExtClassLoaderExtClassLoader尝试在自己的类路径下找,如果还是没有找到,就交给AppClassLoaderAppClassLoader尝试在自己的类路径下找,如果还是没有找到,就抛出ClassNotFoundException。在上述三步中的任意一步中 如果找到,就由该类加载器加载,并被缓存起来。这就是java 类加载器的父优先原则(也有称做:双亲委派模型)。

 

为什么java的类加载要采用父优先原则呢?其中一个最重要的目的就是为了防止我们重新java中定义的类。比如:你在自己的项目中定义了一个java.lang.String类,在运行时是永远都加载不到你自己这个类的,因为每次加载时都由BootstrapClassLoader类加载器 加载自己类路径下的java.lang.String类。

 

自定义类加载器

 

通过前面几节的讲解,相信都对java的类加载器有了大致的了解,如果我们想定义自己的类加载器也是件很容易的时。无非就是继承ClassLoader类,重新其findClass方法,也就是指定类加载器到哪里去读Class文件。当然还有一个跟简单的方法就是,仿照ExtClassLoaderAppClassLoader,直接继承UrlClassLoader。这里我们采用后者来展示一个最简单的示例:

public class MyClassLoader extends URLClassLoader{
    public MyClassLoader(URL[] urls,ClassLoader parent) {
        super(urls,parent);
    }
}
 
写个main方法测试下:
public static void main(String[] args) throws Exception{
        URL temp = new URL("file:///D:/test/");
        URL [] urls = {temp};
        MyClassLoader myClassLoader = new MyClassLoader(urls,MainTest.class.getClassLoader().getParent());
        Class c1 = myClassLoader.loadClass("com.sky.main.Hello");
        Object obj1 = c1.newInstance();
        System.out.println("obj1类名:"+obj1.getClass().getName());
        Method sayhello = c1.getMethod("sayHello");
        sayhello.invoke(obj1);//调用该对象的sayhello方法
 
        System.out.println("##################");
 
        Hello obj2 = new Hello();
        System.out.println("obj2类名:"+obj2.getClass().getName());
 
        if(obj1 instanceof Hello){
            System.out.println("obj1是com.sky.main.Hello类型");
        }else{
            System.out.println("obj1不是com.sky.main.Hello类型");
        }
 
    }
 

 

这里指定了自定义MyClassLoader的类路径为“D:/test/”目录,同时设定了他的父类加载器为ExtClassLoader。另外还创建了一个Hello类,这里首先使用ExtClassLoader进行显式的加载;然后是又在mian方法中创建了一个Hello类的对象,这里会使用默认的AppClassLoader再次加载Hello类。Hello类很简单:

package com.sky.main;
 
/**
 * Created by gantianxing on 2018/2/10.
 */
public class Hello {
    public void sayHello(){
        System.out.println("hello,xiaoming");
    }
}
 

 

我们把编译好的Hello.class放到“D:\test\com\sky\main\”目录下,即可运行main方法了,打印信息为:

obj1类名:com.sky.main.Hello
hello,xiaoming
##################
obj2类名:com.sky.main.Hello
obj1不是com.sky.main.Hello类型

是不是发现了一个很奇怪的问题:使用我们自定义的ClassLoader加载的obj1对象的类名是com.sky.main.Hello,但instanceof检测时却发现不是com.sky.main.Hello类型。这是因为这里分别使用了两个类加载器,加载Hello类。

 

感兴趣的朋友还可以把上面测试代码的MyClassLoader myClassLoader = new MyClassLoader(urls,MainTest.class.getClassLoader().getParent());这行代码改为:

MyClassLoader myClassLoader = new MyClassLoader(urls,MainTest.class.getClassLoader());

 

即把MyClassLoader的父类加载器设置为AppClassLoader,再运行一次,运行结果会不同。具体为什么呢?答案就是java类加载器的父优先原则,自己摸索下。

 

这里的自定义ClassLoader很简单,当然你还可以根据自己的需要重写findClass方法。

 

总结

 

本文首先讲解了什么是类加载器,以及java的类加载器是如何创建的,并对整个创建过程,以及类加载过程源码进行了分析。然后讲解了java自带类加载器的父优先原则。最后展示了如何创建自己的类加载器。

 

创建自定义类加载器的,可以在运行时实时的加载新的class对象,并且可以通过一定的手段卸载指定的class对象,从而实现class对象的热更新。但要实现对一个jar包的热更新,使用自定义ClassLoader来实现就显得非常复杂,因为你不知道哪些Class已经加载,也就不知道要卸载哪些Class对象,除非把整个ClassLoader先卸载。这是一个复杂的过程,庆幸的是OSGI已经帮我们做了这些事情,如果想实现对jar包的热更新,选择OSGI就行了。

 

 

  • Java的类加载器ClassLoader
            
    
    博客分类: jvm java ClassLoader 
  • 大小: 33.6 KB
相关标签: java ClassLoader