Java源码分析——Class类、ClassLoader类解析(一) 类的抽象与获取

2020-09-29 17:38发布

    Class类是集合了所有类的属性、行为的抽象,描述类的修饰、类的构造器、类的字段以及类的方法等抽象,这里的类是指广泛的类,包括了接口、注解、数组等。简单的来说,它涵盖了所有类的共性,所以研究它时,应当从所有类的共性出发,来探讨其中的内容。而ClassLoader类则是负责了类的加载这一块,负责将Class类对象从jvm中加载出来。

    虽然Class类是所有类的抽象,但是它依旧是与其它类一样具有共同性,创建一个自定义Test类,经过编译会产生一个Test.class字节码文件,该字节码文件保存着Test类的抽象,也就是保存着Test类的各种类型与方法等,jvm会通过它来创建Test类对应的Class类对象,jvm再通过该Class类对象来创建Test实例。不管创建多少个实例,字节码文件始终只有一个,且所有的实例都依赖于该Class对象。这也是所有类的hashcode都有唯一性的一个原因。

    当我们new一个新对象或者引用静态成员变量时,JVM中的类加载器子系统会将对应Class类对象加载到JVM中,这个过程也就是通过类加载器将字节码文件转化为Class类对象,然后JVM再根据这个类型信息相关的Class类对象创建我们需要实例对象或者提供静态变量的引用值。但在实际中中,Class类对象是不能被外部创建的,它只能从jvm中加载其对象,因为只有私有构造方法,必须通过forName与getClass方法来获取,所以不会存在Class.class文件,但会存在其它类的字节码文件:

private Class(ClassLoader loader) {
        classLoader = loader;
    }

    那么如何来加载一个Class类对象或者获取一个Class类对象的引用呢?在其内部提供了forName方法:

@CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }

    该方法通过Reflection反射类的getCallerClass本地方法来获取当前类的Class对象的引用,但这个Class类对象是只有标明是属于哪一个类的,但里面并没有加载进对应类的信息。后调用forName0本地方法来对Class类对象进行初始化加载,加载对应类的静态代码块与静态成员变量的,也就是说getCallerClass获取其Class对象时并没有对类进行初始化,先用另外一个forName方法测试,这个方法可以手动开关forName0方法的初始化,如下代码所示:

class Kt{
    static{
        System.out.println("加载了3。。。。");
    }
    {
        System.out.println("加载了。。。。");
    }
    public Kt(){
        System.out.println("加载了2。。。。");
    }
    public void gg() throws IllegalAccessException, InstantiationException {

        Reflection.getCallerClass(1);
    }
}

public class Test {
    public static void main(String args[]) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        System.out.println("未开启初始化。。。。。。");
        Class.forName("test.Kt",false,Kt.class.getClassLoader());
        System.out.println("开启初始化。。。。。。");
        System.out.println("开启初始化。。。。。。");
        Class.forName("test.Kt",true,Kt.class.getClassLoader());
    }
}

在这里插入图片描述
    这里用getCallerClass(int )来代替getCallerClass(),两者效果是一样的,证明了getCallerClass只是获取其对应Class类对象,但是并没有对类进行初始化加载加载。对forName0方法的解释,官方的文档写到:返回与具有给定字符串名称的类或接口关联的Class对象,使用给定的类加载器加载。给定类或接口的完全限定名称(以getName返回的相同格式)此方法尝试查找,加载和链接类或接口。指定的类加载器用于加载类或接口。如果参数加载器为null,则通过根类加载器加载该类。 仅当initialize参数为true且之前尚未初始化时,才会初始化该类。

 private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

    另外一个forName方法可以让调用者手动加载静态属性与方法,也就是上面验证代码用到的,里面加入了java的安全管理器,如果是系统级别的加载器则不需要验证权限,否则就需要验证加载器是否有加载该字节码文件的权限:

 /**
     * @param name 类的完全限定名
     * @param initialize 是否加载
     * @param loader 类加载器
     * @return 一个Class对象
     * @throws ClassNotFoundException
     */
    @CallerSensitive
    public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
          //只有安全管理员才需要反射调用来获取调用者类的存在。否则,避免调用来产生额外的开销
            caller = Reflection.getCallerClass();
            if (sun.misc.VM.isSystemDomainLoader(loader)) {
                ClassLoader ccl = ClassLoader.getClassLoader(caller);
                if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                    sm.checkPermission(
                        SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(name, initialize, loader, caller);
    }

    其实可以从上文看出java对类的加载策略,加载方式是动态加载的,当需要创建类的实例时,这时候创建一个对应Class类对象,加载进静态代码块、静态属性与静态方法,然后再加载进对应类的其它方法。也就是jvm在第一次创建使用该类时加载的,将类注册进jvm,不仅是Class类,ClassLoader类也存在这个方法,Object类、Class类、ClassLoader类是直接与jvm打交道的,它们的对象都由jvm产生与管理:

private static native void registerNatives();
    static {
        registerNatives();
    }

    当调用类中静态的成员变量的引用时或者创建该类实例时(包含静态成员变量与方法,其中构造方法也属于静态成员方法,所以这两者可以统称静态成员变量与方法),jvm会调用类加载器加载字节码文件来创建对应的Class类对象,再通过Class类对象里面类的信息来创建对应的实例。同时为防止字节码文件被破坏而导致的问题,会加入了对字节码文件的检测。

    在java中还提供了另外的方式来获取Class类对象,这种方式就是类名加.class,如下代码:

Cat cat=new Cat();
System.out.println(Cat.class==cat.getClass());//true
System.out.println(int.class);//打印int

    要注意的是,非引用类型以及void类型也会创建一个Class类对象。 不仅在Class类里面提供了类的对象获取,在ClassLoader类加载器类里面也存在着Class类对象的获取,也就是loadClass方法,该方法是一个同步方法,会调用父加载器来加载,该方法通过:

private native final Class<?> findLoadedClass0(String name);

    findLoadedClass0本地方法来查找jvm中指定完全限定名的一个已经加载完的Class类对象:

 private final ClassLoader parent;
 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) {
                        //父加载器不为空则调用父加载器的loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        //父加载器为空则调用Bootstrap Classloader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //父加载器没有找到,则调用findclass,抛出未找到异常
                    //该方法就是用来抛异常的
                    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()
                resolveClass(c);
            }
            return c;
        }
    }

    我们可以用以下代码来判断forName方法与loadClass方法有什么不同:

class Kt{
    static{
        System.out.println("加载了3。。。。");
    }
    {
        System.out.println("加载了。。。。");
    }
    public Kt(){
        System.out.println("加载了2。。。。");
    }

}

public class Test {
    public static void main(String args[]) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        System.out.println("未开启初始化的forName方法。。。。。。");
        Class.forName("test.Kt",false,Kt.class.getClassLoader());
        System.out.println("开启初始化的forName方法。。。。。。");
        Class.forName("test.Kt",true,Kt.class.getClassLoader());
        System.out.println("loadClass方法。。。。。。");
        ClassLoader.getSystemClassLoader().loadClass("test.Kt");
    }
}

在这里插入图片描述
    从结果来看,loadClass方法与未开启初始化操作的forName方法一样只会得到一个含有完全限定名的Class类对象,也就是说loadClass并没有加载其静态方法、静态代码块以及静态属性。

    上述讲解了怎么获得一个Class类对象。下面将要讲解怎么获得类的其它组成部分,如获得实现接口的名字、构造体、方法、属性、包名、父类/接口等等。在java中,每个类的属性都定义了一个的特殊的类来描述,比如Method类就是用来描述方法的,这些类统一的定义在java.lang.reflect反射包里,这些都会在讲解反射的时候讲解到。在讨论之前,先来看看java中的AbstractRepository抽象类,这个类是用来存贮信息的,是一个抽象仓库类。GenericDeclRepository抽象类是用来存贮通用信息的,ClassRepository类是用用来存贮类的信息的,ConstructorRepository类用来存贮构造器的信息的,而MethodRepository是用来存贮方法信息的。它们之间的关系如图示:
在这里插入图片描述

    从图中可以看出,它们之间的继承关系,以及实现的方法名,除了FiledRepository,其它都继承GenericDeclRepository仓库,在GenericDeclRepository里实现了获取类型变量的getTypeParameters函数,因为Java默认Filed不需要类型变量的,但是Filed是可以使用泛型的,从其中的getGenericType方法可以看出。而方法仓库继承构造器仓库,从另一方面说明了构造器是特殊的方法。拿获取类的父类来做例子:

 public Type getGenericSuperclass() {
 		//先得到一个仓库
        ClassRepository info = getGenericInfo();
        //仓库未建立,直接调用本地方法找父类
        if (info == null) {
            return getSuperclass();
        }
        //判定不是接口
        if (isInterface()) {
            return null;
        }
        //调用类仓库的方法获取父类
        return info.getSuperclass();
    }

    其实仓库的建立是起着一种缓存的机制,因为如果每次都从jvm中拿类的属性,会降低java整体的性能,为了提高性能,在第一次从jvm中拿到想要的信息后存在仓库中,后面直接从仓库中获取。

作者:suye233

链接:https://blog.csdn.net/hackersuye/article/details/83387888

来源:CSDN
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。