Java类的加载机制及常见异常

Java类的加载机制

Java中一个类若是要被使用,必然经过加载以及初始化的过程。
这里我们来研究一下一个类是如何被加载的,以及加载类时可能会出现的异常

  • 类加载器简介
  • 自定义类加载器
  • 加载过程中可能会出现的异常
  • Class的加载过程
  • 总结

1.类的加载器简介

一般加载器分为四级:引导类加载器,扩展类加载器,系统类加载器,用户自定义加载器。

通常:
系统类加载器:加载我们自己写的java文件。
扩展类加载器:一些导入的jar包。
引导类加载器:java运行所需的一些核心类。
自定义加载器:用户自己创建用以加载指定类的加载器。

//一个打印出加载器之间的层级关系的小demo
 public class ClassLoaderTree { 

    public static void main(String[] args) { 
        ClassLoader loader = ClassLoaderTree.class.getClassLoader(); 
        while (loader != null) { 
            System.out.println(loader.toString()); 
            loader = loader.getParent(); 
        } 
    } 
 }

output:

//至于这里没有显示引导类加载器,是因为JDK的自身实现,当获取引导类加载器的时候,返回null。
sun.misc.Launcher$AppClassLoader@4edde6e5
sun.misc.Launcher$ExtClassLoader@79fc0f2f

2.自定义类加载器

虽然在绝大多数情况下,系统默认提供的类加载器实现已经可以满足需求。但是在某些情况下,我们还是需要为应用开发出自己的类加载器以满足一些特殊需求。
通常的,自定义类加载器是以继承ClassLoader,但通常是用继承URLClassLoader来实现的。

public class MyClassLoader extends ClassLoader { 

    protected Class findClass(String name) throws ClassNotFoundException { 
        byte[] data = getClassData(name); 
        if (data == null) { 
            throw new ClassNotFoundException(); 
        } 
        else { 
//一般的类加载,我们是提供类名,或者一个路径,让加载器去读取类的字节码,defineClass的功能是将获取到的类的字节码进行加载,我们需要提供类的字节码。
            return defineClass(name, classData, 0, classData.length); 
        } 
    } 

    private byte[] getClassData(String className) { 
        String path = classNameToPath(className); 
        try { 
            InputStream ins = new FileInputStream(path); 
            ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
            int bufferSize = 4096; 
            byte[] buffer = new byte[bufferSize]; 
            int bytesNumRead = 0; 
            while ((bytesNumRead = ins.read(buffer)) != -1) { 
                baos.write(buffer, 0, bytesNumRead); 
            } 
            return baos.toByteArray(); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
        return null; 
    } 

    private String classNameToPath(String path, String className) { 
        return path + File.separatorChar 
                + className.replace('.', File.separatorChar) + ".class"; 
    } 
 }
 //加载完后,若加载成功,拿到Class对象,可以用newInstance()方法将其实例化后就可使用。

之所以继承ClassLoader是因为我们要自己实现在加载一个类的话,需要调用ClassLoader中的一些函数,而这些函数是protected的。就比如上面的defineClass()。
这样,我们就简单的实现了一个自定义的类加载器。
这里先提个问题:
我们用以上方式得到的Class对象进行实例化后,如果对其进行例如A a = (A)o;这样的类型转换,会抛出异常么?


3.加载过程中可能会出现的异常

1.ClassNotFoundException

无法找到目标类。
通常加载类的方式:

Class 类中的 forName 方法。
ClassLoader 类中的 findSystemClass 方法。
ClassLoader 类中的 loadClass 方法。
ClassLoader 类中的 defineClass 方法

导致该异常的原因通常有以下几种:

1.类名拼写错误或者没有拼写完整类名(含包名)
2.没有导入相应的jar包

例:

public class BeanLoadDemo {
    public static void main(String[] args) {
        try {
        //该文件不存在,或者不在此包下。
            Class c = Class.forName("com.service.util.BeanTest");
            c.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

2.ClassNotFoundError

我们知道一个类在被加载的过程中要经历三个阶段:

读取:找到.class文件,读取
链接:校验读取到的.class文件是否符合规范
初始化:载入静态资源,静态块,产生一个Class对象

ClassNotFoundException发生在“读取”阶段。
ClassNotFoundError发生在“链接”阶段。
两者区别是ClassNotFoundException发生时,可以认为是没有分配内存的,至多是一个byte[]的内存(存放.class字节码)。
而ClassNotFoundError会为Class对象准备好内存。

3.NoClassDefFoundError

当目前执行的类已经编译,但是找不到它的定义时。
也就是说你如果编译了一个类B,在类A中调用,编译完成以后,你又删除掉B,运行A的时候那么就会出现这个错误。
通常发生在“链接”阶段。

4.关于涉及到类型转换的部分

这部分应该不属于类加载时可能会发生的异常范畴,不过还是觉得可以说一下,因为它归根结底还是由于类加载引起的。
看代码:

public class ClassLoaderDemo extends ClassLoader {
    public Class define(byte[] buff) {
        return defineClass(null, buff, 0, buff.length);
    }

    public void read() throws IOException, IllegalAccessException, InstantiationException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException {
        int n = 0;
        BufferedInputStream br = new BufferedInputStream(
                new FileInputStream(
                        new File("/Users/admin/OpenService/target/classes/com/service/util/beanfactory/BeanTest.class")));
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        while ((n = br.read()) != -1) {
            bos.write(n);
        }
        br.close();
        byte[] buff = bos.toByteArray();

        Class clazz = define(buff);
        Object o = clazz.newInstance();
        BeanTest test = (BeanTest)o;
    }

    public static void main(String[] args) {
        try {
//            new ClassLoaderDemo().read();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

output:

init
java.lang.ClassCastException: com.service.util.beanfactory.BeanTest cannot be cast to com.service.util.beanfactory.BeanTest
    at com.service.util.beanfactory.ClassLoaderDemo.read(ClassLoaderDemo.java:39)
    at com.service.util.beanfactory.ClassLoaderDemo.main(ClassLoaderDemo.java:53)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:497)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)

稍微说明一下,BeanTest这个类中只有一个显示的构造函数(实例化时输出一个“init”),以及一个info 函数(也是简单的输出一个字符串)。
这里加载类的方式是直接读取字节码,然后用ClassLoader的 defineClass 方法来加载。

结果抛出的异常时类型转换失败,而且从描述上看,还是自己转换为自己出现异常。
为什么会出现这样的异常?
由于我们使用的是自定义类加载器直接继承ClassLoader,同时也使用了这个加载器去加载BeanTest 。
可以理解为工人A生产了一个产品P。
但是在默认情况下,我们自己写的类都是有AppClassLoade来加载的。
同样类比于工人头子S生产产品P。
在进行类型转换时,这里用的是: BeanTest b = (BeanTest)o;
这里出现的“BeanTest” ,又是用AppClassLoader来加载的。
那就出现了一个问题,虽然看上去都是产品P,但是是由不同的人生产的。那自然就无法进行转换。于是抛出类型转换异常。
在启动参数内加入 -XX:+TraceClassLoading,可以发现:

[Loaded com.service.util.beanfactory.BeanTest from JVM_DefineClass]
[Loaded com.service.util.beanfactory.BeanTest from file:/Users/admin51/OpenService/target/classes/]

也可以证明,该类在不同加载器内分别被加载。

根据Class加载的文档资料:

1.跨ClassLoader访问一些数据是比较麻烦的,但是并不是不能做到,比如JMX2。
2.同一个类在同一个ClassLoader中只能加载一次,言下之意就是在不同ClassLoader种可以加载多次。也说明由不同ClassLoader产生的同一个类的Class对象,JVM认识是不同的东西。

以上两点就能说明这个出现类型转换异常的部分原因。

4.Class的加载过程

这里从源码调度浅要分析一下一个类的加载过程。

以下是ClassLoader中的一个方法:

protected Class loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 确认该类是否已经被加载,调用的是一个本地方法。
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                //parent表示当前加载器的父加载器
                    if (parent != null) {
                    //先由父加载器去尝试加载,同样会进入父加载器的该函数内,继续尝试用其父加载器去加载
                        c = parent.loadClass(name, false);
                    } else {
                    //如果父加载器不存在,就用根加载器去加载它
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //抛出异常,找不到该类。
                }

                if (c == null) {
                    //如果还是无法加载Class,则会去调用一个本地方发findClass去加载这个类。
                    long t1 = System.nanoTime();
                    c = findClass(name);
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

过程:
1.负责加载的加载器先去搜寻其已经加载过的类,是否包含目标类。
2.若未被加载,询问其父加载器是否加载过
3.若父加载器不存在,则有当前加载器尝试进行加载。
4.若父加载器加载不了,则用子加载器尝试进行加载。
5.若加载成功,返回Class对象,否则抛出异常。

Created with Raphaël 2.1.0 开始加载 准备工作 是否已经被加载 返回Class对象 End 父加载器是否存在 尝试加载目标类 能否加载 进入子加载器 yes no yes no yes no

大致流程图如上,画的不好请见谅。

5.总结

1.ClassLoader的层级结构
2.类加载时出现的异常及其原因
3.Class加载时的执行流程
4.可以在启动参数内加上-XX:+TraceClassLoading来观察有哪些类在启动时被加载
5.defineClass()方法更多的是用来加载不再classes下的文件,或者是在AOP时覆盖原来类的字节码,需要注意的是,对于同名类使用2次及以上defineClass()回抛出异常。

你可能感兴趣的:(Java,java,class)