美文网首页Java进阶必知必会
深入浅出Java类加载机制

深入浅出Java类加载机制

作者: 逐梦々少年 | 来源:发表于2019-10-30 23:47 被阅读0次

    在之前的文章中,我经常提到java类加载,ClassLoader等名词,而ClassLoader是什么?有什么职责?
    ClassLoader和java类加载机制有什么关系?java类加载机制具体过程是怎么做的?能不能自定义实现类加
    载?相信你此时已经充满了疑惑,那么本篇我们就来深入浅出的分析ClassLoader类加载器和JAVA类加载机
    制吧
    

    初识ClassLoader

    ClassLoader类加载器在Java中的定义就是用来加载其他类到Jvm中的操作类,负责将字节码文件加载到内存中,在内存中创建对应的Class对象。同样的ClassLoader一般使用系统提供的,但是在开发过程中往往会遇到一些特殊的功能,我们需要自定义ClassLoader来实现一些强大灵活的功能,如下:

    热部署机制

    即在不重启Java程序的情况下,动态替换部分类的实现,重新将新的Class字节码文件加载到jvm内存中,将原来的内存中的Class进行替换操作,而java自身的ClassLoader并不能实现热部署。而在一些常用的框架中,早已实现了热部署机制,例如在早期的java web开发中,我们使用的jsp就是使用了自定义的ClassLoader实现的jsp代码修改后不需要重启直接刷新生效,实现代码的动态更新

    应用的模块化与隔离

    ClassLoader还有一个特性,即不同的ClassLoader加载的Class类之间是相互隔离的,彼此互不影响,在我们常用的web容器服务器--Tomcat、jetty等都是利用了此技术,从而实现可以同时加载多个项目工程,并且web工程彼此之间互不干扰,而OSGI和Java9中,都实现了一个动态模块化的结构,每个模块使用独立的ClassLoader做到模块间隔离互不干扰

    不同地方灵活加载类

    系统默认的ClassLoader一般固定从本地指定目录的.class文件或者jar包文件中加载字节码文件。而实现自定义的ClassLoader,甚至可以做到远程加载Class、从服务器、数据库等地方加载,甚至可以做到任意生命周期加载,随心所欲

    类加载机制与加载过程

    当我们运行java程序的时候,JDK实际上就是帮我们执行了java命令,指定了包含main方法的完整类名,以及一个classpath类路径,作为程序的入口,然后根据类的完全限定名查找并且加载类,而查找的规则是在系统类和指定的文件类路径中寻找,如果是class文件的根目录中,则直接查看是否有对应的子目录以及class文件,如果当前路径是jar文件,首先执行解压,然后再去到目录中查找是否有对应的类。而这个查找加载的过程中,负责完成操作的类就是ClassLoader类加载器,输入为完全限定类名,输出是对应的Class对象,而在Java9之前,系统默认的类加载器有三种(java9有模块化概念),如下:

    启动类加载器--Bootstrap ClassLoader

    Bootstrap ClassLoader加载器是Java虚拟机内部实现的,不在java代码中实现,此类负责加载java的基础类,如String、Array等class,还有jdk文件夹中lib文件夹目录中的rt.jar

    扩展类加载器---Extension ClassLoader

    Extension ClassLoader类加载器默认的实现类是sun.misc.Launcher包中的ExtClassLoader类,此类默认负责加载JDK中一些扩展的jar,如lib文件夹中ext目录中的jar文件

    应用程序类加载器--Application ClassLoader

    Application ClassLoader类加载器的默认实现类为sun.misc.Launcher包中的AppClassLoader类,此加载器默认负责加载应用程序的类,包括自己实现的类与引入的第三方类库,即会加载整个java程序目录中的所有指定的类

    双亲委派模型

    这三个系统的类加载器都能实现类加载功能,并且负责的职责和加载的范围都不一样,那么这三个类加载器之间的关系是什么呢?顺序是什么?首先我们可以把这三个类加载器理解为父子关系,当然不是java中的继承关系,而是一种叫"父子委派"的模式,即每一个ClassLoader都有一个变量parent指向父ClassLoader,代码如下:

    public abstract class ClassLoader {
        private static native void registerNatives();
        static {
            registerNatives();
        }
        //指向父类的ClassLoader
        private final ClassLoader parent;
        ........
    }
    

    而三个系统ClassLoader之间的父子关系大致如下:

    Application ClassLoader的父亲是Extension ClassLoader,而Extension ClassLoader的父亲是Bootstrap ClassLoader,而在加载类的时候,一般会先通过父ClassLoader加载,具体的过程大致如下:

    1.判断当前Class是否已经加载过了,如果已经被加载,直接返回Class对象,因为在java中一个Class类只会被同一个Class-Loader加载一次

    2.如果当前Class没有被加载,首先需要调用父ClassLoader去加载,加载成功后,得到父ClassLoader返回的Class对象

    3.父ClassLoader加载成功后,自身就会去加载当前的Class

    而以上的过程称之为“双亲委派模型”,即优先让父ClassLoader加载Class,而如此设计的好处,则是可以避免Java中的类库被覆盖的问题,例如,开发者自己实现了一个java.lang.String 类,通过双亲委派模型,只会被Bootstrap ClassLoader加载,避免了系统的String类被覆盖

    打破双亲委派

    需要注意的一点是,虽然ClassLoader默认的是双亲委派模型,但是我们依然存在一些例外,或者人为改变的情况,例如:

    1.自定义Class类加载顺序:尽管java希望我们按照默认的双亲委派加载的顺序执行,但是我们的确在自定义ClassLoader的时候,不遵循这个约定,不过即使如此,一些被java安全机制限制的类依然不能随便被自定义的ClassLoader加载,例如包名为java开头的类

    2.网格化加载顺序:在OSGI和java9中,类加载器之间的关系甚至更复杂,形成了一个网状,每个模块都有自己的类加载器,并且模块之间可以存在依赖关系,也就是说此时可以是当前模块加载Class,也可以传递给其他模块的加载器加载

    3.JNDI:JNDI(Java Naming and Directory Interface)技术是企业级应用的一种常见服务,使用的方式就是父加载器委托给子加载器进行加载,和默认的双亲委派机制是反过来的

    Class.forName与类加载

    之前的文章中,我们有学习到反射技术,也知道Class对象中都有一个反射方法,可以获取到当前Class的ClassLoader,如下:

    public ClassLoader getClassLoader()
    

    而每一个ClassLoader都有一个方法获取父ClassLoader,如下:

    public final ClassLoader getParent()
    

    除此之外,我们还可以通过Class对象获取默认的系统类加载器,如下:

    public static ClassLoader getSystemClassLoader()
    

    与之对应的是,ClassLoader中也有一个主要的方法用来加载class,如下:

    public Class<?> loadClass(String name) throws ClassNotFoundException
    

    了解了这些后,我们开始尝试利用反射和ClassLoader来加载一个常用的类--ArrayList,代码如下:

    ClassLoader cl = ClassLoader.getSystemClassLoader();//获取默认系统类加载器
    try {
        Class<?> cls = cl.loadClass("java.util.ArrayList");//加载系统类ArrayList
        ClassLoader actualLoader = cls.getClassLoader();
        System.out.println(actualLoader);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
    

    注意:由于双亲委派机制,父ClassLoader可能会加载失败或者返回的不是当前ClassLoader加载的结果,ArrayList由于是系统包下的类,实际上已经被BootStrap ClassLoader加载了,所以这里返回的反而是null

    在前面反射篇我们还了解到了一个加载Class的方法--forName,但是ClassLoader的loadClass看起来和forName功能一样,这两个有什么区别呢?其实熟悉原理的都知道,基本实现原理是相同的,都是使用的ClassLoader代码进行加载,不过,ClassLoader的loadClass方法不会初始化类的初始化代码,并且有一点需要注意的是forName方法有多个重载,其中一个为:

    public static Class<?> forName(String name,boolean initialize, ClassLoader loader)
    

    这里需要指定三个参数,第一个是class全量限定类名,第二个则是表示是否在Class类加载后立刻初始化代码块(static代码块),第三个参数则是传递一个类加载器实现Class的加载,如果我们这个时候分别用ClassLoader和forName的方式加载一个有static代码块的类,就会发现,forName方式加载的Class输出了static代码块的内容,为了弄懂其中缘由,我们直接从ClassLoader类的loadClass方法的源码来一探究竟:

     public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);//调用了重载方法loadClass,第二个参数传递为false
       }
    

    可以看到内部调用了重载方法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
                    }
                    //第二次检查Class是否被加载--双短检查加锁机制保障Class的唯一性
                    if (c == null) {
                        long t1 = System.nanoTime();
                        //如果仍然找不到,请按顺序调用findClass顺序查找当前Class
                        c = findClass(name);
                        sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);                      
                        sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                        sun.misc.PerfCounter.getFindClasses().increment();
                    }
                }
                //是否需要在Class加载后调用class的初始化代码块
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    

    从上述的源码以及我添加的注释中,我们大概明白了,在loadClass方法执行过程中,还会传递一个resolve标示符,此标示符和forName的initialize参数是一样的,用来判断是否在Class加载后进行初始化代码块的操作,但是我们从上面的方法明显看到,默认传递的值为false,即仅仅加载Class类,并不去调用类的初始化代码块部分,两者的区别至此已经真相大白

    自定义ClassLoader

    前面我们也多次提到过自定义ClassLoader,此技术也是tomcat、OSGI等实现应用隔离、动态模块加载的基础,那么如何自定义呢?一般来说,我们需要继承类ClassLoader,重写其方法findClass即可,现在我们来看一个开发中常遇到的问题:我们在开发过程中经常遇到本地运行程序的情况,往往有些时候会遇到服务端的jar与我们自定义的代码中有部分类名一致的时候,因为系统加载的时候默认优先显示服务端的jar中的calss,而不是本地的class,那么究其原因,就是jvm默认使用AppClassLoader加载classpath中的类 ,那么我们能否重写AppClassLoader来实现优先显示本地实现的类,再去加载服务端的jar中呢?说做就做,我们参考系统默认实现的URLClassLoader 类的代码来写一个简单的功能实现,代码如下:

    public class MyClassLoader extends URLClassLoader {
        public URLClassPath ucp;
        private Map<String, Class<?>> cache = new HashMap();
        private static final Method defineClassNoVerifyMethod;
    
        static String[] paths = System.getProperty("java.class.path").split(";");
    
        static URL[] urls = new URL[paths.length];
    
        //初始化每一个类的URL
        static{
            for(int i=0; i<urls.length; i++){
                try {
                    urls[i] = new URL("file:"+paths[paths.length-1-i]);
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(Arrays.toString(urls));
    
            SharedSecrets.setJavaNetAccess(new JavaNetAccess() {
                public URLClassPath getURLClassPath(URLClassLoader u) {
                    return ((MyClassLoader)u).ucp;
                }
                @Override
                public String getOriginalHostName(InetAddress inetAddress) {
                    return null;
                }
            } );
    
            Method m;
            try {
                m = SecureClassLoader.class.getDeclaredMethod("defineClassNoVerify",
                        new Class[] { String.class, ByteBuffer.class, CodeSource.class });
                m.setAccessible(true);
            } catch (NoSuchMethodException nsme) {
                m = null;
            }
            defineClassNoVerifyMethod = m;
        }
    
        public MyClassLoader(URL[] urls) {
            super(MyClassLoader.urls);
            this.ucp = new URLClassPath(MyClassLoader.urls);
        }
    
        public MyClassLoader(ClassLoader parent) {
            super(MyClassLoader.urls, parent);
            this.ucp = new URLClassPath(MyClassLoader.urls);
        }
    
        //重写loadClass,实现自定义的类加载
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException{
            Class c = null;
            if (name.contains("hadoop")) {
                c = (Class)this.cache.get(name);
                if (c == null) {
                    c = findClass(name);
                    this.cache.put(name, c);
                }
            } else {
                c = loadClass(name, false);
            }
            return c;
        }
    
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            //替换路径查找对应的类资源
            String path = name.replace('.', '/').concat(".class");
            Resource res = this.ucp.getResource(path);
            if (res != null) {
                try {
                    return defineClass(name, res, true);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name, e);
                }
            }
            throw new ClassNotFoundException(name);
        }
    
        private Class<?> defineClass(String name, Resource res, boolean verify) throws IOException {
            int i = name.lastIndexOf('.');
            URL url = res.getCodeSourceURL();
            if (i != -1) {
                //根据class最后一个.的下标截取包名
                String pkgname = name.substring(0, i);
                Package pkg = getPackage(pkgname);
                Manifest man = res.getManifest();
                if (pkg != null){
                    //校验当前包名是否为私密包名
                    if (pkg.isSealed()){
                        if (!pkg.isSealed(url)){
                            throw new SecurityException(
                                    "sealing violation: package " + pkgname +
                                            " is sealed");
                        }
                    }
                    else if ((man != null) && (isSealed(pkgname, man))) {
                        throw new SecurityException(
                                "sealing violation: can't seal package " +
                                        pkgname + ": already loaded");
                    }
                }
                //Manifest不为null
                else if (man != null)
                    definePackage(pkgname, man, url);
                else {
                    definePackage(pkgname, null, null, null, null, null, null,
                            null);
                }
            }
    
            ByteBuffer bb = res.getByteBuffer();
            byte[] bytes = bb == null ? res.getBytes() : null;
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
    
            if (!verify){
                Object[] args = { name, bb == null ? ByteBuffer.wrap(bytes) : bb,cs };
                try {
                    return (Class)defineClassNoVerifyMethod.invoke(this, args);
                }
                catch (IllegalAccessException localIllegalAccessException) {}
                catch (InvocationTargetException ite) {
                    Throwable te = ite.getTargetException();
                    if ((te instanceof LinkageError))
                        throw ((LinkageError)te);
                    if ((te instanceof RuntimeException)) {
                        throw ((RuntimeException)te);
                    }
                    throw new RuntimeException("Error defining class " + name,
                            te);
                }
            }
            return defineClass(name, bytes, 0, bytes.length, cs);
        }
    
        //校验是否为私密(密闭)包==>查找类路径是否存在
        private boolean isSealed(String name, Manifest man) {
            String path = name.replace('.', '/').concat("/");
            Attributes attr = man.getAttributes(path);
            String sealed = null;
            if (attr != null) {
                sealed = attr.getValue(Attributes.Name.SEALED);
            }
            if ((sealed == null) &&
                    ((attr = man.getMainAttributes()) != null)) {
                sealed = attr.getValue(Attributes.Name.SEALED);
            }
            return "true".equalsIgnoreCase(sealed);
        }
    }
    

    而实现了以后,在运行的时候只需要加上对应的变量指定使用的classLoader即可:

    java -Djava.system.class.loader=test.MyClassLoader -classpath ...  MyTestClass
    

    相关文章

      网友评论

        本文标题:深入浅出Java类加载机制

        本文链接:https://www.haomeiwen.com/subject/nhfzvctx.html