java:classLoader.loadClass() 和 Class.forName()

java:classLoader.loadClass() 和 Class.forName()

1 前言

什么是JVM的类加载机制?

Java虚拟机把描述类的数据,从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这个过程被称为虚拟机的类加载机制。- 《深入理解java虚拟机》

类加载的生命周期?

一个类型从被加载到虚拟机内存中开始,到卸载出内存,整个生命周期包含:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸载(Unloading)七个阶段,其中验证、准备、解析三个部分统称为连接(Linking)。

其中加载(Loading)、验证(Verification)、准备(Preparation)、初始化(Initialization)和卸载(Unloading)这五个阶段的顺序是确定的,而解析(Resolution)则不一定,某些情况下可以在初始化(Initialization)之后再开始,这是为了支持java语言运行时绑定特性(也称动态绑定或晚期绑定)。

初始化(Initialization)的特殊场景

提到第一个阶段加载(Loading)前,先说下初始化(Initialization)的一些特殊场景,在《Java虚拟机规范》中,严格规范了有且仅有6种情况必须立即对类进行初始化(Initialization)(当然初始化前,必须执行加载和连接):

1 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段:

(1)比如使用new关键字时:

@ToString
class A{
    public static void classRun(){
        System.out.println("class run");
    }

    public void run(){
        System.out.println("run");
    }

    static {
        System.out.println("初始化A");
    }
}
new A();

结果:

初始化A

(2) 读取或设置一个类型的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)

有且仅有可以触发初始化的6种场景,可称之为:主动引用,除此以外所有引用类型的方式都不会触发初始化,称为被动引用

class Parent{
    static {
        System.out.println("parent init");
    }
    public static int a = 1000;
    public static final int b = 2000;
}

class Child extends Parent{
    static {
        System.out.println("child init");
    }
}

执行如下:

System.out.println(Child.a);

结果如下,因为Parent的a为类变量,且非final修饰,故而子类调用父类的类变量时,父类执行初始化,而子类不执行初始化,由此可得,静态字段获取或设置时,只有直接定义这个字段的类才会触发初始化,通过子类引用父类中定义的静态字段,子类不会触发初始化

parent init
1000

若执行如下:

public class TestClassLoad {
    public static void main(String[] args) throws Exception{
        System.out.println(Parent.b);
    }
}

结果仅打印2000,也没有打印出:parent init,这是因为Parent的类变量b由final修饰,在编译阶段进行了常量传播优化,被加入到TestClassLoad的常量池中,以后TestClassLoad对于Parent.b的引用,实际转换为了TestClassLoad对自身常量池的引用了

2000

当然,若Parent类中定义的是public static final Map等非常量对象,则子类引用父类该Map对象,会触发父类Parent的初始化(当然子类不会触发初始化)。

我们知道,因为接口中定义的常量,默认即是public static final类型,但是接口的加载过程与类加载过程有所不同,接口的常量在子类中引用时,接口会执行初始化(和上述有区别,如果是类的static final变量引用,不会执行初始化),虽然接口不具有static{}代码块语法,但编译器仍然会为接口生成"< clinit >()"类构造器,用于初始化接口中定义的成员变量,故而引用接口中定义的场景,是会执行接口的初始化

interface Fruit{
    // 即  String a = "apple come in";    
    public static final String a = "apple come in";

    static void run(){
        System.out.println("run");
    }
}

另外还有些不会触发初始化的被动引用场景,如下为针对new关键字的被动引用场景:

System.out.println(new Child[0]);

结果:

[Lcom.xiaoxu.test.Child;@cc34f4d

这里一维数组对象的new执行中,却没有触发Child或Parent的初始化,但是却触发了[Lcom.xiaoxu.test.Child的初始化,这里的字节码指令触发的是newarray,故而也没有Child或Parent的初始化

(3) 调用一个类型的静态方法的时候

class Parent{
    static {
        System.out.println("parent init");
    }
    public static int a = 100000;
    public static final int b = 2000;

    public static void parentRun(){
        System.out.println("parentRun");
    }

    public static final void parentRunAgain(){
        System.out.println("parentRunAgain");
    }
}

class Child extends Parent{
    static {
        System.out.println("child init");
    }
}

执行:

Child.parentRun();

结果如下,子类调用父类的类方法,依然是父类执行初始化,即直接定义这个类方法的类才会触发初始化

parent init
parentRun

当然,执行static final方法,也会触发定义该方法的类的初始化

Parent.parentRunAgain();
parent init
parentRunAgain

2 使用java.lang.reflect包的方法对类型进行反射调用,若类型未执行过初始化,则需要先触发其初始化

3 当初始化类的时候,若父类还没有进行初始化,则需要先触发其父类的初始化

第3点需要注意的是,当一个类在初始化时,要求其父类全部都初始化了,但接口初始化时,并不会要求其父接口都全部完成了初始化,只有在真正使用到父接口时(比如父接口定义的常量),才会执行初始化(java对于类Class为单继承,但是接口可以继承多个父接口(interface),为多继承)

4 虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的类即为主类),虚拟机会先初始化这个主类

5 使用JDK7新加入的动态语言支持时,若java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句柄,并且这个方法句柄对应的类没有初始化,则先触发其初始化

6 若接口中定义了JDK8新加入得到默认方法(default修饰的接口方法),若这个接口的实现类发生了初始化,那该接口要在其之前被初始化

2 加载(Loading)

加载阶段,JVM虚拟机会完成以下三件事:

(1)通过一个类的全限定名来获取定义此类的二进制字节流

(2)将这个字节流所代表的静态存储结构转换为方法区的运行时数据结构

(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载阶段,可以使用java虚拟机内置的引导类加载器来完成,也可由用户自定义的类加载器去完成,开发人员可通过定义自己的类加载器,去控制字节流的获取方式(重写一个类加载器的findClass()或loadClass方法),来根据自己的想法赋予应用程序获取运行代码的动态性。

加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区中,方法区中的数据存储格式完全由虚拟机实现自行定义,类型数据安置在方法区后,会在java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的类型数据的外部接口。

Java虚拟机团队有意把类加载阶段中的"通过一个类的全限定名来获取描述该类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类,实现这个动作的代码被称为"类加载器"(classLoader)

那么这里就会提到需要关注的classLoader.loadClass方法了,java.lang包下,具有抽象类:abstract class ClassLoader,而ClassLoader抽象类中的loadClass方法,源码如下:

public供调用的loadClass方法,name参数需要传入全限定类名(包名 + 类名),用以加载Class:

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

实际调用如下的protected方法,参考如下java doc 的参数描述,resolve代表是否解析该class,false代表不解析该Class:

* @param  resolve
*         If <tt>true</tt> then resolve the class
protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        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) {
                // If still not found, then invoke findClass in order
                // to find the class.
                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;
    }
}

而上面这个核心的loadClass(String name, boolean resolve)方法,就是双亲委派模型的实现

在JVM角度看来,只存在两种不同的类加载器,一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++实现(限于HotSpot虚拟机),是虚拟机自身一部分;另外一种即其他所有类的加载器,这些类加载器都由Java语言实现,独立存在于虚拟机外部,且全都继承自抽象类java.lang.ClassLoader。

自JDK1.2以来,Java一直保持着三层类加载器、双亲委派的类加载构架。

三层类加载器为:

启动类加载器(BootStrap Class Loader): 这个类加载器负责加载存放在< JAVA_HOME >\lib目录,或者被 -Xbootclasspath参数所指定路径中存放的,且是java虚拟机能够识别的(按照文件名识别,如rt.jar,tools.jar,名字的不符合的类库即便放在lib目录中也不会被加载)类库加载到虚拟机内存中。

启动类加载器无法被java程序直接引用,用户在编写自定义类加载器时,若需要把加载请求委派给引导类加载器,直接使用null代替即可,如下java.lang.getClassLoader()返回类的加载器时也提到,可以用null值代表引导类加载器的约定规则:

/**
 * Returns the class loader for the class.  Some implementations may use
 * null to represent the bootstrap class loader. This method will return
 * null in such implementations if this class was loaded by the bootstrap
 * class loader.
 *
 * <p> If a security manager is present, and the caller's class loader is
 * not null and the caller's class loader is not the same as or an ancestor of
 * the class loader for the class whose class loader is requested, then
 * this method calls the security manager's {@code checkPermission}
 * method with a {@code RuntimePermission("getClassLoader")}
 * permission to ensure it's ok to access the class loader for the class.
 *
 * <p>If this object
 * represents a primitive type or void, null is returned.
 *
 * @return  the class loader that loaded the class or interface
 *          represented by this object.
 * @throws SecurityException
 *    if a security manager exists and its
 *    {@code checkPermission} method denies
 *    access to the class loader for the class.
 * @see java.lang.ClassLoader
 * @see SecurityManager#checkPermission
 * @see java.lang.RuntimePermission
 */
@CallerSensitive
public ClassLoader getClassLoader() {
    ClassLoader cl = getClassLoader0();
    if (cl == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
    }
    return cl;
}

扩展类加载器(Extension Class Loader): 这个类加载器是在类sun.misc.Launcher$ExtClassLoader中以java代码形式实现,负责加载 < JAVA_HOME >\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。由于扩展类加载器是由java实现,开发者可以直接在程序中使用扩展类加载器来加载Class文件。

应用程序类加载器(Application Class Loader): 这个类加载器是由sun.misc.Launcher$AppClassLoader实现,由ClassLoader类的getSystemClassLoader()返回。它负责加载用户类路径(ClassPath)上所有的类库。

双亲委派模型的好处是,java中的类随着它的类加载器一起具备了带有优先级的层次关系。例如java.lang.Object,它存在于rt.jar中,无论哪个类加载器需要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object在任意的类加载器环境中都能保证是同一个类。

3 ClassLoader类中的Class<?> loadClass(String name, boolean resolve)

有了上述的介绍,那么不难理解,抽象类ClassLoader中的loadClass(String name, boolean resolve)方法,本质就是对类进行加载,resolve为true时还会对类进行解析操作:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        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) {
                // If still not found, then invoke findClass in order
                // to find the class.
                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;
    }
}

逻辑上,首先检查类是否已经加载过,若没有加载过,则调用父类加载器的loadClass()方法,若父类加载器抛出ClassNotFoundException,则调用自身的findClass()方法尝试进行加载。

如下自定义的ClassLoader继承类MyClassLoader,调用原本父类ClassLoader的loadClass方法(因为ClassLoader的loadClass方法为protected方法,官方不建议重写此方法,建议重写findClass方法,可参考URLClassLoader中重写的findClass方法),这里仅做调试演示,如下:

class MyClassLoader extends ClassLoader{
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        return super.loadClass(name, resolve);
    }
}
MyClassLoader cle = new MyClassLoader();
Class<?> a = cle.loadClass("com.xiaoxu.test.A", false);

在这里插入图片描述

可见,双亲委派执行时,先是执行的AppClassLoader的loadClass(name, false),因为继承了ClassLoader,所以这里是递归调用:

在这里插入图片描述

递归时,AppClassLoader的parent是ExtClassLoader,再次执行其loadClass方法,直至parent == null,即启动类ClassLoader获取到时,先由为null的Boot strap ClassLoader尝试加载类:

在这里插入图片描述

执行findBootstrapClassOrNull(name)后发现Boot strap ClassLoader未找到该类,则调用自身的findClass(name),若也没找到,则抛出ClassNotFoundException:

在这里插入图片描述

源码可见Launcher类中ExtClassLoader继承了URLClassLoader(AppClassLoader同样继承了URLClassLoader),而URLClassLoader重写了findClass方法,如下:

protected Class<?> findClass(final String name)
    throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}

因为是递归调用,所以ExtClassLoader也未找到加载类时,findClass方法抛出ClassNotFoundException异常,且c为null,故而再次由AppClassLoader执行URLClassLoader中的findClass方法:

在这里插入图片描述

如下可见,URLClassLoader中findClass方法,先将我们传入的com.xiaoxu.test.A全限定类名,替换为了com/xiaoxu/test/A.class路径,且可以通过URLClassPath类获取到该class资源,再通过defineClass(name, res)方法返回Class类型:

在这里插入图片描述

此时已经获取到Class对象,resolve表示是否解析:

在这里插入图片描述
最后loadClass加载到该Class对象,若AppClassLoader通过findClass方法也未加载到该Class,那么就直接抛出ClassNotFoundException异常了。

MyClassLoader cle = new MyClassLoader();
Class<?> a = cle.loadClass("com.xiaoxu.test.A", false);
System.out.println(a);

执行结果(不会打印:初始化A,因为该方法是用于加载类的):

class com.xiaoxu.test.A

由此注意,loadClass方法是类的加载,并不会执行初始化操作(解析操作也需要由参数resolve决定),一般直接调用的classLoader.loadClass(String name),默认是不对class进行解析。

4 Class.forName

那么Class.forName和classLoader.loadClass的区别就显而易见了,Class.forName是java.lang.Class类中的静态方法,参考Class.forName的源码可知,其返回的也是Class对象,但是此Class对象已经执行过初始化了(当然加载和连接也都执行了),源码如下:

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

Class.forName(name) ,调用的forName0方法中,第二个参数传的是true,代表执行初始化。调用此方法,等同于调用:Class.forName(className, true, currentLoader)。

Class.forName(className, true, currentLoader)方法源码如下:

@CallerSensitive
public static Class<?> forName(String name, boolean initialize,
                               ClassLoader loader)
    throws ClassNotFoundException
{
    Class<?> caller = null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        // Reflective call to get caller class is only needed if a security manager
        // is present.  Avoid the overhead of making this call otherwise.
        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);
}

initialize为true,代表执行初始化,即static代码块中的代码也会执行,反之则不执行初始化。

所以数据库连接注册驱动Driver时,可见Class.forName的使用(因为会执行static代码块的初始化):

比如执行:

Class.forName("com.mysql.cj.jdbc.Driver");

注册驱动:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

执行如下:

Class<?> a = Class.forName("com.xiaoxu.test.A");
System.out.println("a:" + a);

结果执行了初始化:

初始化A
a:class com.xiaoxu.test.A

5 总结

classLoader.loadClass和Class.forName获取Class对象的使用场景:

1 若只希望加载类,判断该类是否存在(不会抛出ClassNotFoundException异常),则直接使用classLoader.loadClass即可

2 若希望加载类时,还会自动执行初始化,即执行static代码块,则可使用Class.forName

参考Spring的ClassUtils工具类,可有如下工具方法封装,用于获取Class对象(是否需要初始化可自行参考决定):

public static Class<?> forName(String name, @Nullable ClassLoader loader) {

     ExcpUtils.throwExpIfFalse(null != name, "class name should not be null.");
     Class<?> clazz;

     clazz = resolvePrimitiveType(name);

     if(clazz != null){
         return clazz;
     }

     ClassLoader cltUse = loader;
     if(cltUse == null){
         cltUse = fetchClassLoader();
     }

     try {
         clazz = Class.forName(name, false, cltUse);
     } catch (ClassNotFoundException notFoundException) {
         throw new CrawlerForJException(
                 "forName raise classNotFound error, name:[" + name + "]." ,
                 notFoundException.getCause());
     }

     return clazz;
 }

另外参考Spring的ClassUtils工具类的isCacheSafe方法,因为双亲委派模型是:自定义类加载器(User Class Loader) > 应用程序类加载器 (Application Class Loader) > 扩展类加载器 (Extenssion Class Loader) > 启动类加载器 (Bootstrap Class Loader),又因为任意一个类,都必须由它的类加载器和这个类本身一起共同确立其在java虚拟机中的唯一性,每个类加载器,都拥有一个独立的类名称空间,即两个类是否"相等",只有在这两个类来源于同一个类加载器的前提下才有意义,否则即使这两个类来源于同一个Class文件,被同一个java虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等,当然这里的相等指的是代表类Class的equals方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括instanceof关键字判定等各种情况,所以该工具方法判定了在指定ClassLoader和Class时,由指定ClassLoader加载的Class,和传入的Class是否一致的情况,也体现了对于ClassLoader类加载器的深入理解:

public static boolean isCacheSafe(Class<?> clazz, @Nullable ClassLoader classLoader) {
	Assert.notNull(clazz, "Class must not be null");
	try {
		ClassLoader target = clazz.getClassLoader();
		// Common cases
		if (target == classLoader || target == null) {
			return true;
		}
		if (classLoader == null) {
			return false;
		}
		// Check for match in ancestors -> positive
		ClassLoader current = classLoader;
		while (current != null) {
			current = current.getParent();
			if (current == target) {
				return true;
			}
		}
		// Check for match in children -> negative
		while (target != null) {
			target = target.getParent();
			if (target == classLoader) {
				return false;
			}
		}
	}
	catch (SecurityException ex) {
		// Fall through to loadable check below
	}

	// Fallback for ClassLoaders without parent/child relationship:
	// safe if same Class can be loaded from given ClassLoader
	return (classLoader != null && isLoadable(clazz, classLoader));
}
private static boolean isLoadable(Class<?> clazz, ClassLoader classLoader) {
	try {
		return (clazz == classLoader.loadClass(clazz.getName()));
		// Else: different class with same name found
	}
	catch (ClassNotFoundException ex) {
		// No corresponding class found at all
		return false;
	}
}

参考: 《深入理解java虚拟机》

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值