类加载器

由 笔尖 发布

概述


ClassLoader是JVM运行的基础,所有的class文件都有加载器加载,仅负责加载,将class文件加载到内存中的动态数据结构,然后由JVM执行链接、初始化操作。而不管class是否可以运行。

image-20210204100454758

正是由于没有将classloader放在JVM内部,因此能够实现更加灵活的类加载操作。

类加载器虽然离日常开发较远,但仍有相当重要的作用:

  • 当遇到ClassNotFoundExceptionNoClassDefinedError异常时,知道如何解决
  • 如果需要支持类的动态加载以及字节码文件加载后解密操作,需要自定义类的加载器
  • 开发人员编写自定义类加载器,实现自定义处理逻辑。

类加载方式的分类

显式加载

如class.forname()、classloader.loadclass()加载类,注意加载和初始化的区别,loadclass属于类的被动使用,不会导致初始化。

隐式加载

没有人为指明加载哪个类,诸如new关键字,以及间接引用到的类属于隐式加载。

public class UserTest {
    public static void main(String[] args) {
        User user = new User(); //隐式
        try {
            Class clazz = Class.forName("com.dsh.jvmp2.chapter04.java.User"); //显式
            ClassLoader.getSystemClassLoader().loadClass("com.dsh.jvmp2.chapter04.java.User");//显式
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

隐式加载可以实现class的运行时动态加载,而无需在编译器就确定所需的类,大大提高灵活性。

命名空间及唯一性

类的唯一性由类本身和加载它的类确定,同一个类,被不同加载器加载,属于JVM环境下的不同类。借助这一特性,tomcat用不同加载器加载同一个类,实现运行隔离。

public static void main(String[] args) {
    String rootDir = "/Users/dongshuhuan/JavaProjects/JVM_study/src";
    try {
        //创建自定义的类的加载器1
        UserClassLoader loader1 = new UserClassLoader(rootDir);
        Class clazz1 = loader1.findClass("com.dsh.jvmp2.chapter04.java.User");

        //创建自定义的类的加载器2
        UserClassLoader loader2 = new UserClassLoader(rootDir);
        Class clazz2 = loader2.findClass("com.dsh.jvmp2.chapter04.java.User");

        System.out.println(clazz1 == clazz2); //false clazz1与clazz2对应了不同的类模板结构。
                                                    System.out.println(clazz1.getClassLoader());//com.dsh.jvmp2.chapter04.java.UserClassLoader@1d44bcfa
            System.out.println(clazz2.getClassLoader());//com.dsh.jvmp2.chapter04.java.UserClassLoader@6f94fa3e

            //######################
         Class clazz3 = ClassLoader.getSystemClassLoader().loadClass("com.dsh.jvmp2.chapter04.java.User");
        System.out.println(clazz3.getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2

        //自定义类加载器的父类就是系统类加载器
            System.out.println(clazz1.getClassLoader().getParent());//sun.misc.Launcher$AppClassLoader@18b4aac2

    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

类加载基本特征

  • 双亲委派机制。但自定义类加载器可以不遵循这一机制
  • 可见性。子类加载器可以通过getparentClassloader获取父类加载器。
  • 单一性。由于双亲委派这一机制,父类加载器加载过的类,子类不会重复再加载。

加载器的分类

引导类加载器和自定义类加载器,

  • 前者由C++编写,负责加载JVM核心类库,加载来自JAVA_HOME/jre/lib/rt.jarbsun.boot.class.path路径下的内容,并不继承classloader类,没有父加载器。
  • 后者由java层面编写,加载业务逻辑处理类,全部派生于classloader加载器。

结构如下所示:

image-20210204153451567

  • 扩展类加载器:继承于classloader,父类加载器为启动类加载器,从java.ext.dirs系统属性所指定的目录中加载类库,以及jre/lib/ext子目录下加载类库,如果用户将class放于此,也会被其加载。
  • 应用程序类加载器:继承于classloader,父类加载器为扩展类加载器,负责加载classpath或java.class.path指定路径下的类库,大部分编写的class文件都由该加载器加载。同时也作为用户自定义类加载器的默认父加载器。
  • 用户自定义类加载器:体现java强大生命力的重要因素,通过自定义类加载器,可以加载网路、数据库中的class,例如SGI组件框架,Eclipse插件,自定义类的加载使得无需重新打包,动态的加载类从而改变程序功能。自定义类加载器也继承于classLoader。

测试不同类加载器

  • 获得当前类的ClassLoader -> clazz.getClassLoader()
  • 获得当前线程上下文的ClassLoader -> Thread.currentThread().getContextClassLoader()
  • 获得系统类加载器-> ClassLoader.getSystemClassLoader()

注意:由于引导类加载器不是Java类,因此在Java程序中只能打印出空值。对于数组类的类加载器来说,是通过Class.getClassLoader()返回的,与数组当中元素类型的类加载器是一样的;如果数组当中的元素类型是基本数据类型,数组类是没有类加载器的。

public class ClassLoaderTest1 {
    public static void main(String[] args) {
        //获取系统该类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2
        //获取扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d
        //试图获取引导类加载器:失败
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null

        //###########################
        try {
            ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
            System.out.println(classLoader);//null
            //自定义的类默认使用系统类加载器
            ClassLoader classLoader1 = Class.forName("com.dsh.jvmp2.chapter04.java.ClassLoaderTest1").getClassLoader();
            System.out.println(classLoader1);//sun.misc.Launcher$AppClassLoader@18b4aac2

            //关于数组类型的加载:使用的类的加载器与数组元素的类的加载器相同
            String[] arrStr = new String[10];
            System.out.println(arrStr.getClass().getClassLoader());//null:表示使用的是引导类加载器

            ClassLoaderTest1[] arr1 = new ClassLoaderTest1[10];
            System.out.println(arr1.getClass().getClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2

            int[] arr2 = new int[10];
            System.out.println(arr2.getClass().getClassLoader());//null:基本数据类型不需要类的加载器(虚拟机预先定义)                
 System.out.println(Thread.currentThread().getContextClassLoader());//sun.misc.Launcher$AppClassLoader@18b4aac2
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

ClassLoader源码解析

image-20210204163017797

首先明确所有自定义类加载器都继承于ClassLoader类,主要关注如下方法:

  • getParent():获取父类加载器,协助实现双亲委派机制。
  • loadclass(String name):加载名称为name的类,这是一个总体大框架,其实现逻辑遵循了双亲委派机制。
  • findclass(String name):查找名为name的类,返回class类对象,JVM鼓励程序员重写该方法,实现自己加载逻辑的同时,也遵循了JVM的双亲委派规范。当上层父类无法加载类时,会调用该类该方法加载指定类。
  • defineClass(String name, byte[] b, int off, int len):在findclass内部,从二进制字节流中获取class类对象。通常,在实现自定义类加载器时,一般继承classLoader,并重写findclass方法,改变获取字节流的方式,然后调用URLClassLoader实现好的defineClass实现逻辑。
  • resolveclass(class<?> c):对已加载的类执行解析操作,从符号引用转换为直接引用。
  • findLoadedClass(String name):查找名为name的class类是否被加载,否则返回null。
/*
* 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 thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {//当前类的加载器的父类加载器未加载此类 or 当前类的加载器未加载此类
                // 调用当前classLoader的findClass方法
                long t1 = System.nanoTime();
                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;
    }
}

SecureClassLoader与URLClassLoader

classLoader是一个抽象类,很多方法没有实现,如findclass()、defineclass(),而URLClassloader为其提供了具体实现方法,因此,如果没有特殊需求,建议直接继承URLClassLoader,这样可以方便的获取字节码流。

Class.forName()与ClassLoader.loadClass()

都会导致类的加载,不过前者是类的主动使用,会导致初始化;后者是类的被动使用,不会导致初始化。

双亲委派机制

通常,系统类加载器在准备加载一个类时,不会自己尝试加载这个类,而是把请求任务委托给父类加载器完成,依次向上委托,如果无法加载,再将加载权限下放给扩展类、系统类加载器。加载过程如图所示:

image-20210204205927157

优势:

  • 避免类的重复加载,确保类的全局唯一性
  • 保护程序安全,尤其是核心类库,譬如java.lang包下的类库只能由启动类加载器加载,如果恶意篡改String类,则无法被加载进JVM中。

体现:

loadclass()中的实现逻辑确保了双亲委派机制,具体执行过程如下:

  1. 在当前类加载器查找是否加载过类,如果有,则直接返回。
  2. 判断是否有父类加载器,如果有,则调用parent.loadClass()方法,循环加载。
  3. 如果没有父类加载器,则调用findclass()方法,自己来加载该类。

弊端:

应用类加载器如果加载的应用类,启动类加载器是无法访问到的,且无法再自行加载,如果此时需要实现一些应用类的功能,则无法实现。

破坏双亲委派机制:

通过重写classload()方法破坏掉该逻辑,线程上下文类加载器,热替换。

热替换是指在程序运行过程中,不停止服务,通过修改源程序文件,来动态改变运行行为,web服务器使用居多。大致思路是:自定义一个类加载器,重写findclass方法或loadclass方法,加载类,生成实例对象。

80

被加载类

public class Demo1 {
    static {
        System.out.println("dsada");
    }
    public void hot() {
        System.out.println("OldDemo1---> NewDemo1");//替换后输出
    }
}

自定义类加载器

public class Myclassloader extends ClassLoader {
    private String rootDir;

    public Myclassloader(String rootDir) {
        this.rootDir = rootDir;
    }

    protected Class<?> findClass(String className) throws ClassNotFoundException {
        Class clazz = this.findLoadedClass(className);
        FileChannel fileChannel = null;
        WritableByteChannel outChannel = null;
        if (null == clazz) {
            try {
                String classFile = getClassFile(className);
                FileInputStream fis = new FileInputStream(classFile);
                fileChannel = fis.getChannel();
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                outChannel = Channels.newChannel(baos);
                ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
                while (true) {
                    int i = fileChannel.read(buffer);
                    if (i == 0 || i == -1) {
                        break;
                    }
                    buffer.flip();
                    outChannel.write(buffer);
                    buffer.clear();
                }
                byte[] bytes = baos.toByteArray();
                clazz = defineClass(className, bytes, 0, bytes.length);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    if (fileChannel != null)
                        fileChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                try {
                    if (outChannel != null)
                        outChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return clazz;
    }

    /**
     * 类文件的完全路径
     */
    private String getClassFile(String className) {
        return rootDir + "/" + className.replace('.', '/') + ".class";
    }
}

测试代码

public class Test {
    public static void main(String args[]) {
        while (true) {
            try {
                //1. 创建自定义类加载器的实例
                String rootDir = "C:\\Users\\10488\\IdeaProjects\\Jvm\\out\\production\\Jvm";
                Myclassloader loader = new Myclassloader(rootDir);
                //2. 加载指定的类
                Class clazz = loader.findClass("Demo1");
                //3. 创建运行时类的实例
                Object demo = clazz.newInstance();
                //4. 获取运行时类中指定的方法
                Method m = clazz.getMethod("hot");
                //5. 调用指定的方法
                m.invoke(demo);
                Thread.sleep(5000);
            } catch (Exception e) {
                System.out.println("not find");

                try {
                    Thread.sleep(5000);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
}

自定义类加载器

为了隔离某些应用和容器,需要将运行类加载到不同加载器环境中。

  • 除了启动类加载器机器加载类,其余类可以按需加载
  • 扩展加载源,从数据库,网络中加载类
  • 防止java代码被反编译,因此会对class进行加解密

不同加载器加载的相同类,无法进行类型转换,即类型“不相等”

实现方式:

从逻辑上讲我们最好不要直接修改loadClass()的内部逻辑。建议的做法是只在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用。当编写好自定义类加载器后,便可以在程序中调用loadClass()方法来实现类加载操作。注意:自定义类加载器的parent加载器是系统类加载器,要想不被app加载,需要将工程文件中Demo.java删除,否则会被paren的加载到。

测试

public class MyClassLoader extends ClassLoader {
    private String byteCodePath;

    public MyClassLoader(String byteCodePath) {
        this.byteCodePath = byteCodePath;
    }

    public MyClassLoader(ClassLoader parent, String byteCodePath) {
        super(parent);
        this.byteCodePath = byteCodePath;
    }

    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        BufferedInputStream bis = null;
        ByteArrayOutputStream baos = null;
        try {
            //获取字节码文件的完整路径
            String fileName = byteCodePath + className + ".class";
            //获取一个输入流
            bis = new BufferedInputStream(new FileInputStream(fileName));
            //获取一个输出流
            baos = new ByteArrayOutputStream();
            //具体读入数据并写出的过程
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                baos.write(data, 0, len);
            }
            //获取内存中的完整的字节数组的数据
            byte[] byteCodes = baos.toByteArray();
            //调用defineClass(),将字节数组的数据转换为Class的实例。
            Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);
            return clazz;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (baos != null)
                    baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (bis != null)
                    bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return null;
    }
}

class MyClassLoaderTest {
    public static void main(String[] args) {
        MyClassLoader loader = new MyClassLoader("F:\\");

        try {
            Class clazz = loader.loadClass("Demo");
            System.out.println("加载此类的类的加载器为:" + clazz.getClassLoader().getClass().getName());//com.dsh.jvmp2.chapter04.java2.MyClassLoader

            System.out.println("加载当前Demo1类的类的加载器的父类加载器为:" + clazz.getClassLoader().getParent().getClass().getName());//sun.misc.Launcher$AppClassLoader
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

暂无评论

发表评论