Java源码分析——Class类、ClassLoader类解析(一) 类的抽象与获取
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中拿到想要的信息后存在仓库中,后面直接从仓库中获取。