这篇文章介绍了java的类加载机制(https://blog.csdn.net/liuxiao723846/article/details/109901300),本文重点介绍ClassLoader类是如何对类进行加载的。
ClassLoader 类是负责加载类的,是一个抽象类。除了启动类加载器,所有的类加载器都继承自ClassLoader。
1、获取ClassLoader对象:
- 获取当前类的classLoader:claszz.getClassLoader()
- 获取系统的classLoader(应用类加载器):ClassLoader.getSystemClassLoader()
- 获取当前线程上下文的classLoader:Thread.currentThread().getContextClassLoader();
- 获取调用者的ClassLoader:Reflection.getCallerClass().getClassLoader();
- 获取父ClassLoader:classLoader.getParent();
1.1)示例:
@CallerSensitive
private void test0() throws ClassNotFoundException {
//获取当前Class对象的classLoader
System.out.println(String.class.getClassLoader());//String类的加载器为bootstrap,所以输出 null
Class<?> forName = Class.forName("cn.nuc.edu.LogTest.ClassLoaderTest");
System.out.println(forName.getClassLoader());//sun.misc.Launcher$AppClassLoader
//系统classloader
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
System.out.println(systemClassLoader.getClass().getName()); //sun.misc.Launcher$AppClassLoader
//线程上下文classloader
ClassLoader threadContextClassLoader = Thread.currentThread().getContextClassLoader();
System.out.println(threadContextClassLoader.getClass().getName());//sun.misc.Launcher$AppClassLoader
//调用者
Class<?> caller = Reflection.getCallerClass();
System.out.println(caller.getClassLoader());//null
//获取父加载器
ClassLoader classLoader = this.getClass().getClassLoader();
System.out.println(classLoader.getParent().getClass().getName());//sun.misc.Launcher$ExtClassLoader
}
1.2)Reflection.getCallerClass()的用法:
https://blog.csdn.net/liuxiao723846/article/details/110008081
2、动态(手动)加载类:
2.1)为什么要动态加载类?
我们在程序中使用“主动调用”的方式都会触发类加载(例如:使用new创建一个类的对象),这种行为属于静态类加载,是JVM自动完成的。java语言最大的优势就是动态性,在系统中我们可以使用某些api来动态的加载某个类,然后再进行该类的实例化。
假设我们有如下程序,根据不同参数执行不同对象的方法:
public class Test{
public static void main(String[] args){
if("A".equals(args[0])){
new A().print();
}
if("B".equals(args[0])){
new B().print();
}
}
}
上面的写法,我们必须在程序中定义A、B类,否则编译不会通过。如果这样的判断非常多,而我们只想用其中的一种,这种静态方式必须把其余的类也定义出来,如过使用动态类加载可以简化:
public class Test{
public static void main(String[] args){
try{
Class c = Class.forName(args[0]);
MyStandard me = (MyStandard)c.newInstance();
me.print();
}catch(Exception e){
e.printStackTrace();
}
}
}
具体的类通过实现MyStandaard接口来定义:
interface MyStandard{
public void print();
}
public class A implements MyStandard{
public void print(){
System.out.println("A");
}
}
public class B implements MyStandard{
public void print(){
System.out.println("B");
}
}
2.2)生成Class对象方法:
前面介绍任何一个类在加载阶段结束,都会生成一个该类对应的Class对象,所以可以简单认为生成Class对象的过程就是类加载过程。
- 类名.class:加载类(前提是没加载过),不做类的初始化,返回Class的对象;
- 对象.getClass():既然都创建了对象,那一定完成了类的加载、连接、初始化,以及对象的创建和初始化,返回Class的对象;
- Class.forName(“类名字符串”):加载类,并做类的静态初始化(不做对象初始化),返回Class的对象;(反射,属于主动调用,所以会触发类初始化)
- Class.forName(“类名字符串”,false,classLoader):加载类,不做类的初始化,返回Class的对象;
- classLoader.loadClass():加载类,不做类的初始化,返回Class的对象;
除了前两种,可以通过如下两种方法来动态加载类:Class.forName()和classLoader.loadClass()
示例:
class MyBean {
private static String str = "abc";
static {
System.out.println("static code");
}
public MyBean() {
System.out.println("construct code");
}
}
public class ClassLoaderTest {
public static void main(String[] args) {
test1();
}
private static void test1() {
//没有任何输出
Class<?> class0 = MyBean.class;
//输出:static code 和 construct code
Class<? extends MyBean> class1 = new MyBean().getClass();
try {
//输出:static code
Class<?> class2 = Class.forName("cn.nuc.edu.LogTest.MyBean");
//没有任何输出
Class<?> class3 = Class.forName("cn.nuc.edu.LogTest.MyBean",false,ClassLoader.getSystemClassLoader());
//输出:static code 和 construct code
MyBean bean = (MyBean)class3.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
try {
//没有任何输出
Class<?> class4 = systemClassLoader.loadClass("cn.nuc.edu.LogTest.MyBean");
//输出:static code 和 construct code
Class<?> class5 = systemClassLoader.loadClass("cn.nuc.edu.LogTest.MyBean");
MyBean bean2 = (MyBean)class5.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
说明:上面代码只是为了说明问题,要运行时得到正确的输出,需要把其他段注释掉,只执行一段(因为类初始化只有一次)。
2.3)实例初始化的方法:
类加载后生成了对应的Class对象,接下来我们可以借助Class对象的newInstance()方法来创建实例,从而触发实例初始化(如果此时没有进行类初始化,会先进行类初始化)。该api属于反射,符合JVM定义的6中主动调用。
示例:
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
try {
//先进行类初始化
Class<?> class4 = Class.forName("cn.nuc.edu.LogTest.MyBean",false,ClassLoader.getSystemClassLoader());
MyBean bean = (MyBean)class4.newInstance();
Class<?> class5 = systemClassLoader.loadClass("cn.nuc.edu.LogTest.MyBean");
MyBean bean2 = (MyBean)class5.newInstance();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
2.3)loadClass()和Class.forName()区别:
java中可以使用一下两种方法加载类:(二者均返回Class示例)
- loadClass():ClassLoader实例方法,仅加载类,不做解析和初始化;
- Class.forName():加载类,并做类初始化;
1)loadClass()方法:
从源码来看,调用ClassLoader.loadClass(String)方法时,内部会调用重载的loadClass(name,false),第二个参数表示resolve(是否解析),默认是false,所以loadClass只是加载,不会解析更不会做类的初始化。(见下面详细介绍)
2)Class.forName()方法:
默认加载并初始化类(类初始化,不会实例初始化)。其内部调用重载方法forName(string,boolean,classloader),第二个参数指定是否初始化(之类初始化),最终调用native的方法。
@CallerSensitive
public static Class<?> forName(String className)throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
@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 (loader == null) {
ClassLoader ccl = ClassLoader.getClassLoader(caller);
if (ccl != null) {
sm.checkPermission(
SecurityConstants.GET_CLASSLOADER_PERMISSION);
}
}
}
return forName0(name, initialize, loader, caller);
}
/** Called after security check for system loader access checks have been made. */
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller) throws ClassNotFoundException;
3、ClassLoader常用方法:
api文档:http://itmyhome.com/java-api/java/lang/ClassLoader.html
3.1)loadClass(String):
该方法加载指定名称(包括包名)的二进制类型,该方法在JDK1.2之后不再建议用户重写但用户可以直接调用该方法,loadClass()方法是ClassLoader类自己实现的,该方法中的逻辑就是双亲委派模式的实现,其源码如下,loadClass(String name, boolean resolve)是一个重载方法,resolve参数代表是否生成class对象的同时进行解析相关操作。
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class c = findLoadedClass(name);//1、检查请求的类是否已经被加载过了
if (c == null) {
try {
if (parent != null) {
//如果父加载器不是boot,递归调用loadClass(name, false)
c = parent.loadClass(name, false);
} else {
//如果父加载器是boot,调用findBootstrapClassOrNull(name)
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException,说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载的时候,再调用本身(包括ext和app)的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
假设我现在从类路径下加载一个类A,步骤如下:
- appClassloader先查找是否加载过A(findLoadedClass()方法),若有,直接返回;
- 若无,委托ext检查是否加载过A,若有,直接返回;
- 若无,委托boot检查是否加载过A,若有,直接返回;
- 若无,那就boot加载,若在E:\Java\jdk1.6\jre\lib\*.jar下找到了指定名称的类,则加载,结束;
- 若没找到,boot加载失败;
- ext开始加载,若在E:\Java\jdk1.6\jre\lib\ext\*.jar下找到了指定名称的类,则加载,结束;
- 若没找到,ext加载失败;
- app加载,若在类路径下找到了指定名称的类,则加载,结束;
- 若没有找到,抛出异常ClassNotFoundException
说明:
- 3、4、5步骤是通过findBootstrapClassOrNull()方法来完成的;
- 6、7、8、9都是调用findClass(name)方法来完成的,在findClass中调用了defineClass方法,该方法会生成当前类的java.lang.Class对象。
3.2)findClass(String name):
在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载类,但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,前面可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委托模式。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
3.3)defineClass(byte[] b, int off, int len)
defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够通过class文件实例化class对象,也可以通过其他方式实例化class对象,如通过网络接收一个类的字节码,然后转换为byte字节流创建对应的Class对象,defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
3.4)resolveClass(Class<?> c)
使用该方法可以使用类的Class对象创建完成也同时被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
private native void resolveClass0(Class c);
4、AppClassLoader 、ExtClassLoader 和URLClassLoader
4.1)URLClassLoader:
URLClassLoader是在java.net包下的一个类,继承了ClassLoader,并且重写了其中的findClass方法,URLClassLoader提供了一下几种方式加载类功能(Class.forName()只能从已有的系统中加载类):
- *文件: (从文件系统目录加载)
- jar包: (从Jar包进行加载)
- Http: (从远程的Http服务进行加载)
当class文件或者resources资源文件更新后,我们需要重新加载这些类或者Jar。从理论上来说,当应用清理了对所加载的对象的引用,那么垃圾收集器就会将这些对象给收集掉,然后我们再重新加载新的JAR文件,并创建一个新的URLClassLoader来加载。可是这里有一个问题,就是我们不知道垃圾收集器什么时候将那些未被引用的对象给收集掉,特别是在Windows中,因为在Windows中打开的文件是不可以被删除或被替换的。
在Java7的Build 48版中,URLClassLoader提供了close()这个方法,可以将打开的资源全部释放掉,这个给开发者节省了大量的时间来精力来处理这方面的问题。
4.2)App、ExtClassLoader:
上面我们了解了ClassLoader类中的loadClass()方法,它主要实现了双亲委派模型,真正的类加载是调用了findClass()方法完成的,那么有一个疑问:在ClassLoader类中findClass方法是一个空方法,没有具体实现,那么Ext、App类加载器是如何加载的类呢?
sun.misc.Launcher.ExtClassLoader和sun.misc.Launcher$AppClassLoader都在rt.jar中,二者都是Launcher的内部类,这两个类是不开源的。在openjdk中可以看到二者的实现:二者都是继承URLClassLoader类。
总结:
- 除了bootstrap加载器,Ext、App都是继承ClassLoader,实现了双亲委派模型来加载类;
- 自定义类加载器继承ClassLoader(或者URLClassLoader),重写findClass方法,同样在保证了双亲委派模型的前提下,实现自己的类加载逻辑;如果要破坏双亲委派,那么可以重写loadClass方法;
5、ClassNotFoundException和NoClassDefFoundError的区别:
正如它们的名字所说明的:NoClassDefFoundError是一个错误(Error),而ClassNOtFoundException是一个异常,在Java中错误和异常是有区别的,我们可以从异常中恢复程序但却不应该尝试从错误中恢复程序。
5.1)ClassNotFoundException的产生原因:
Java支持使用Class.forName、ClassLoader.loadClass()方法来动态地加载类,如果这个类在类路径中没有被找到,那么此时就会在运行时抛出ClassNotFoundException异常。
要解决这个问题很容易,唯一需要做的就是要确保所需的类连同它依赖的包存在于类路径中。
另外还有一个导致ClassNotFoundException的原因就是:当一个类已经某个类加载器加载到内存中了,此时另一个类加载器又尝试着动态地从同一个包中加载这个类。
由于类的动态加载在某种程度上是被开发者所控制的,所以他可以选择catch这个异常然后采取相应的补救措施。有些程序可能希望忽略这个异常而采取其他方法。还有一些程序则会终止程序然后让用户再次尝试前做点事情。
5.2)NoClassDefFoundError产生的原因:
如果JVM或者ClassLoader实例尝试加载(可以通过正常的方法调用,也可能是使用new来创建新的对象)类的时候却找不到类的定义。要查找的类在编译的时候是存在的,运行的时候却找不到了。这个错误往往是你使用new操作符来创建一个新的对象但却找不到该对象对应的类。这个时候就会导致NoClassDefFoundError。
在slf4j的静态绑定中,因为slf4j-api.jar在打包后将StaticBind类去掉了,所以如果没有日志实现类,那么调用StaticBind.getSingletong()时会报该错误。
总结:
- 加载时从外存储器找不到需要的class就出现ClassNotFoundException
- 连接时从内存找不到需要的class就出现NoClassDefFoundError
参考:
https://www.cnblogs.com/codeobj/p/12082587.html
https://www.cnblogs.com/chinaifae/p/10401523.html
https://blog.csdn.net/w372426096/article/details/81901482
http://bbs.itheima.com/thread-79064-1-1.html
https://my.oschina.net/jasonultimate/blog/166932