浅谈类加载

时间:2022-07-24
本文章向大家介绍浅谈类加载,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

类加载包括加载,链接,初始化三个过程。加载过程中通过全限定类名将二进制数据加载到内存中;链接阶段包括验证,准备以及解析,其中验证指的是对class二进制文件格式的校验,准备阶段为类中的静态变量分配内存,解析阶段为这些静态变量赋默认值;初始化阶段才为静态变量赋初值,以及执行静态代码块。

加载

1)通过全限定类名获取该类的二进制字节流

2)将字节流(.class二进制文件)表示的静态存储结构转化为方法区的运行时数据机构

3)内存中生成表示该类的Class对象。

类加载器

主要完成加载过程中的第一步,通过该类的全限定类名来获取描述该类的二进制字节流。

jdk1.8之后类加载器有如下四种,启动类加载器(Bootstrap Class Loader),扩展类加载器(Extension Class Loader),应用程序类加载器(Application Class Loader)以及自定义类加载器(User Class Loader)。

其中bootstrap Class Loader 负责加载存放在lib目录下能被JVM识别的类。

Extension Class Loader负责加载libext目录中的类。

Application Class Loader负责加载用户类路径CLASSPATH上的所有类。

双亲委派机制

类加载器的双亲委派模型如上图所示。需要注意的是上述箭头并不是继承关系,其关系为各个孩子中保存其父亲的引用,为如下代码中的parent。其被final修饰也就意味着父子关系是不能改变的。

public abstract class ClassLoader {
    private final ClassLoader parent;
}

类加载的过程是采用双亲委派模型,其工作过程为:当一个类需要被加载时其会先在自己的类加载器的缓存中查找,该类是否之前已经被加载了,若加载了返回即可,若未被加载则不会自己直接加载,而是询问其的父亲是否已经加载,如此直到启动类加载器,若启动类加载器缓存中还是没有,启动类加载器会尝试着加载,若该类在lib目录下,则加载即可,如若不存在,则让其的孩子加载器进行加载,如此直到其自己的加载类,此时才可以使用自己的类加载器加载。其源码如下:

    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) {
                        // 委派给其parent进行加载
                        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) {

                    long t1 = System.nanoTime();
                    // 父类加载器无法加载时,再调用本身的findClass进行加载
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    // 使用模板设计模式 
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
为何要使用双亲委派机制?

使用双亲委派机制主要解决安全问题。反之,假设对于每个类都使用自己的类加载器加载的话,当某个用户编写一个java.lang.Object类,覆盖了系统中的Object类,java体系中最基础的行为都就不能保证了。使用双亲委派就不会有这问题,其会首先让自底而上查找,查找启动类加载器发现已经加载过了,因此不会加载该用户写的这个Object。

自定义类加载器

只需继承ClassLoader抽象类,并重写其模板方法findClass()即可。

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;

public class MyClassLoader extends ClassLoader{

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File file = new File("....", name.replaceAll(".", "/").concat(".class"));
        try {
            // 将class文件写入字节数组
            FileInputStream fis = new FileInputStream(file);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int b = 0;
            while((b = fis.read()) != 0) {
                baos.write(b);
            }
            byte[] bytes = baos.toByteArray();
            baos.close();
            fis.close();
            return defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
             e.printStackTrace();
        }
         return super.findClass(name);
    }

}

链接

验证:验证二进制class文件是否符合JVM规定

比较:静态变量赋默认值,例如int 0, long 0L boolean false, reference null等。

解析:将常量池中的符号引用替换为直接引用。

初始化

调用类初始化代码< clinit > ,给静态成员变量赋初始值。