打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
通过实例Java?ClassLoader原理

通过实例Java ClassLoader原理

(2010-09-02 15:50:50)
标签:

java

classloader

it

分类: Java技术
  注:本文是个人对java虚拟机规范提到的知识的一点总结。

      在Java中,类必须经过jvm使用类装载器(class loader)装载(load)之后才能使用。以主程序(Class A)为例,当jvm调用程序的main方法时,在没有装载A.class文件时,这是不可能的事情。

     装载class文件是程序执行的第一步,这跟linux下的程序执行器(execve)装载目标文件的原理是一样的,jvm充当execve的角色,读取 class文件的二进制数据,转换成jvm内部能够识别的数据结构。在这个过程中生成了一个A类关联的Class对象,该Class对象记录了A类相关的数据。

      一个类真正能使用要经过装载、连接、初始化三个步骤。连接又可以分为“验证”、”准备“、”解析 “三个步骤。总体说来,由于class文件本身记录了很多数据结构(常量池、字段信息、方法信息、引用信息等),必须要转换成内部形式,这个过程就通过装载来实现,但是,class文件自身并没有完成链接,这跟C的目标文件有很大差别——其实也就是解析执行和编译执行的区别了,装载之后形成的内部结构还存在很多符号引用,需要resolve引用,这就是连接过程,原理跟C的链接是一样——解决内部和外部符号引用。

     在连接过程,jvm试图解析类A中出现的符号引用,比如A中定义了:

private B b=new B();

符号引用b是一个字段引用,B()是一个方法引用,并且B是定义在别的class文件的(一个类只能对应一个class文件),所以jvm试图解析 b,这个过程同时要对B进行装载(如果B并没有被当前装载器装载),装载过程一般是递归进行的,一直到达最高类层次(Object)。

    关于JVM是如何装载、连接、初始化的,内容太多,详细的信息要参考Java虚拟机规范。

    Java中(jdk  1.6版)有三种加载器,启动加载器→扩展加载器(ExtClassLoader)→用户程序加载器(AppClassLoader)。箭头左边的类是右边类的父类。其中启动类加载器对于我们是不可见的,用于加载java源码包的类以及jdk安装路径下的类(rt.jar etc),而扩展类加载器用于加载ext目录下的类,用户类加载器则是加载普通的class文件。

   Java给我们提供了使用自定义类装载器来装载程序的可能,这有利于程序的扩展。想想看applet 的运行,JDBC驱动的装载(典型的JDBC访问语句Class,forName()),我们在编译的时候能知道它们要装载什么类型的类吗?

以下仅通过一个示例来说明自定义类装载器的原理以及使用自定义装载实现动态类型转换:

package greeter;


public interface Greeter {

    public void sayHello();
}

我在包greeter下定义了一个接口Greeter。

然后我实现一个自定义类装载器GreeterClassLoader(如果是没有特殊目的的加载,直接继承ClassLoader就可以了,根本不用覆盖任何方法):

//注:该实现是稍微有点复杂的,JDK文档鼓励使用另一种方法,稍后提供这种方法并说明两者之间的差异。

public class GreeterClassLoader extends ClassLoader{

    private  String basePath; //自定义装载作用的根目录,装载器将在这个目录下查找class文件

    public GreeterClassLoader(String path){
        this.basePath=path;
    }

    //覆盖loadClass方法

    @Override
    protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class result=null;

        //看看这个类是不是已经加载过了,如果是直接返回缓存中的Class对象(放在JVM的堆中)
        result=super.findLoadedClass(name);
        if(result!=null){
            System.out.println("class "+name+" has been loaded before.");
            return result;
        }

       //上一步没找到,看看是不是系统类,委托给启动类装载器去装载
        result=super.findSystemClass(name);
        if(result!=null){
            System.out.println("found by system classloader.");
            return result;
        }else{
            System.out.println("system loader can not find it.");
        }

        //都找不到,直接读取源文件
        byte classdata[]=null;
        //do not try to load system class
        if(name.startsWith("java")){
            System.out.println("i encountered a system class:"+name);
            throw new ClassNotFoundException();
        }
        classdata=this.getClassData(name);
        if(classdata==null){
            System.err.println("can't load "+name);
        }
        System.out.println(name);

        //从字节码中解析出一个Class对象
        result=defineClass(name, classdata, 0, classdata.length);
        if(result==null){
            System.out.println("Class format error.");
            throw new ClassFormatError();
        }

        //是否需要解析
        if(resolve){
           this.resolveClass(result);
        }
        return result;
//        return super.loadClass(name, resolve);
    }

     //从文件中读取class文件的二进制数据

     private byte[]  getClassData(String name){
        byte[] retArr=null;
        //read the byte data of the class file
        name=name.replace('.', '/');
        String path=this.basePath+"/"+name+".class";
        System.out.println(path);
        try {
            FileInputStream fin = new FileInputStream(path);
            BufferedInputStream bis=new BufferedInputStream(fin);
            ByteArrayOutputStream baos=new ByteArrayOutputStream();
            int c=bis.read();
            while(c!=-1){
                baos.write(c);
                c=bis.read();
            }
            bis.close();
            System.out.println("read finished.");
            retArr=baos.toByteArray();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
            return null;
        }catch(IOException ex){
            ex.printStackTrace();
            return null;
        }
        return retArr;
    }


}

 

好了,我在greeter包下又建立了一个新类Hello,它继承了Greeter接口:

public class Hello implements Greeter{

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

}

以下是一个测试类,我想使用GreeterClassLoader加载Hello类:

public class Greet {
   
   

    public static void main(String args[]) throws ClassNotFoundException, InstantiationException, IllegalAccessException{

        //等待命令行输入的字符串作为类的全限定名,我在这里输入greeter.Hello
        Scanner scan=new Scanner(System.in);
        String path=scan.nextLine();
        GreeterClassLoader greeterLoader=new GreeterClassLoader("/build/classes");
        Class c=greeterLoader.loadClass(path, false);
        //c.newInstance();

        //输出加载c的类加载器

        System.out.println(c.getClassLoader());

       
    }

输出结果:

greeter.Hello
greeterLoader's loader: sun.misc.Launcher$AppClassLoader@19821f
found by system classloader.
greeter.Hello@1d58aae
Class c's loader: sun.misc.Launcher$AppClassLoader@19821f

注意到"found by system classloader."这段输出,在loadclass中我们本来想直接读取了class文件并使用defineClass加载字节码,但是这段代码没有执行(没见到”read finished“信息),由此可见该类直接使用了super.findSystemClass(name)加载了,而这个方法本身调用了系统加载器(在这里是AppClassLoader),AppClassLoader是标准的用户程序加载器。

如果我们把findSystemClass部分注释掉,再重新测试,结果如下:

greeter.Hello
greeterLoader's loader: sun.misc.Launcher$AppClassLoader@19821f
E:/Project Area/NetBeans/NetBeansProjects/AlgorithmApps/build/classes/greeter/Hello.class
read finished.
greeter.Hello
E:/Project Area/NetBeans/NetBeansProjects/AlgorithmApps/build/classes/greeter/Greeter.class
read finished.
greeter.Greeter
i encountered a system class:java.lang.Object

Exception in thread "main" java.lang.NoClassDefFoundError: java/lang/Object
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:621)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:466)
        at test.GreeterClassLoader.loadClass(GreeterClassLoader.java:58)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:252)
        at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:320)
        at java.lang.ClassLoader.defineClass1(Native Method)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:621)
        at java.lang.ClassLoader.defineClass(ClassLoader.java:466)
        at test.GreeterClassLoader.loadClass(GreeterClassLoader.java:58)
        at test.Greet.main(Greet.java:40)
Caused by: java.lang.ClassNotFoundException
        at test.GreeterClassLoader.loadClass(GreeterClassLoader.java:51)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:252)
        at java.lang.ClassLoader.loadClassInternal(ClassLoader.java:320)
        ... 11 more
Java Result: 1
成功生成(总时间:4 秒)

注意到在调用defineClass后,它同时也解析了接口Greeter,读取了Greeter.class文件并解析。但是,在解析 Object(每个类的超类)时出错了,因为java.lang.Object是由启动类加载的,自定义类加载器找不到它的路径,所以加载失败。

这也是这种方式的一个不足之处。

 

现在我们看看JDK推荐的方式,覆盖ClassLoader的findClass方法:

findClass
protected Class findClass(String name)
                      throws ClassNotFoundException使用指定的二进制名称查找类。此方法应该被类加载器的实现重写,该实现按照委托模型来加载类。在通过父类加载器检查所请求的类后,此方法将被 loadClass 方法调用。默认实现抛出一个 ClassNotFoundException

我们写了一个类如下:

public class GreeterClassLoaderNew extends ClassLoader{

    private String basepath;

    public GreeterClassLoaderNew(String path){
        this.basepath=path;
    }

    public GreeterClassLoaderNew(ClassLoader loader,String path){
        super(loader);
        this.basepath=path;
    }

    @Override
    public Class loadClass(String name) throws ClassNotFoundException {
        System.out.println("i am called.");
        return super.loadClass(name);
    }

 

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        byte[] classData=null;
        classData=this.getClassData(name);
        if(classData==null){
            throw new ClassNotFoundException();
        }
        Class c=this.defineClass(name, classData, 0, classData.length);
        return c;
    }

     private byte[]  getClassData(String name){
        byte[] retArr=null;
        //read the byte data of the class file
        name=name.replace('.', '/');
        String path=this.basepath+"/"+name+".class";
        System.out.println(path);
        try {
            FileInputStream fin = new FileInputStream(path);
            BufferedInputStream bis=new BufferedInputStream(fin);
            ByteArrayOutputStream baos=new ByteArrayOutputStream();
            int c=bis.read();
            while(c!=-1){
                baos.write(c);
                c=bis.read();
            }
            bis.close();
            System.out.println("read finished.");
            retArr=baos.toByteArray();
        } catch (FileNotFoundException ex) {
            ex.printStackTrace();
            return null;
        }catch(IOException ex){
            ex.printStackTrace();
            return null;
        }
        return retArr;
    }

}

 

然后运行测试:

 GreeterClassLoaderNew greeterLoader=new GreeterClassLoaderNew(pwd);
        System.out.println("greeterLoader's loader: "+greeterLoader.getClass().getClassLoader());
        Class c=greeterLoader.findClass(path);
        System.out.println(c.newInstance().toString());

        System.out.println("Class c's loader: "+c.getClassLoader());

结果:

greeter.Hello
greeterLoader's loader: sun.misc.Launcher$AppClassLoader@19821f
E:/Project Area/NetBeans/NetBeansProjects/AlgorithmApps/build/classes/greeter/Hello.class
read finished.
i am called.
i am called.
greeter.Hello@e09713
Class c's loader: test.GreeterClassLoaderNew@8813f2

注意”i am called.“指示了loadClass被调用了。我们开始是调用GreeterClassLoaderNew的findClass方法,当它加载完字节码后,顺便解析了Greet接口和Object类,在这个过程中jvm又调用了loadclass方法(所以我们看到了i am called),并且调用了两次,loadClass是父类的方法,也就是说,它使用了父类装载器(AppClassLoader)装载了Greet接口和Object类。

从上可以看出,决定一个类的loader的是findClass方法,当一个类被加载器加载时,它隶属于这个加载器的命名空间,不同加载器加载同一个类A,会产生两个类A,这两个A的对象是不能相互转换的,它们的命名空间不一样,导致它们属于两个不同的类型。

比如下面的转换语句:

Hello h=(Hello)c.newInstance();

一眼看上去似乎这个语句可以成功执行,但是结果是抛出异常。奇怪左边跟右边都是greeter.Hello的实例啊,竟然不能转换。

——由于左边的Hello类是AppClassLoader加载(定义加载器,它只会向它的父类请求加载,而并不知道实际上 GreeterClassLoaderNew已经加载了这个类)的,c是GreeterClassLoaderNew加载的,两个类属于不同的命名空间,执行上面的语句将会产生异常信息"greeter.Hello cannnot be cast to greeter.Hello",看上去很诡异。

如果是Greeter ig=(Greeter)c.newInstance()则可以成功执行。

这又是为什么呢?我们在使用GreeterClassLoader加载Hello类时,同时也加载了Greeter接口,注意这里的Greeter接口实际是GreeterClassLoader委托它的父类AppClassLoader加载的,所以Greeter是 AppClassLoader定义并加载的,而GreeterClassLoader只是它的初始化加载器,这个结论可以通过调用API Greeter.class.getClass().getClassLoader()查看定义类加载器证实。

具体的原因我现在还没弄懂,大略分析如下:

jvm判别ig和c.newInstance()的类型,判断能否进行转换,发现c.newInstance的接口是由AppClassLoader加载的,跟左边的一致,所以执行了类型转换。

具体的原因还需要深究。

 ------刚刚看了Inside java virtual machine的关于java类型安全和装载约束的部分,感觉了解到一个比较正确的答案:ig是由AppClassLoader装载的,而Greeter 接口也是由AppClassLoader装载,并且Greeter在GreeterClassLoaderNew、AppClassLoader之间共享,虽然它们不是属于同个命名空间,这样一来,GreeterClassLoaderNew定义的Hello类就可以转换成AppClassLoader 定义的Greeter接口。

我们可以作如下的验证:

在GreeterClassLoaderNew中的loadClass截获对Greeter的解析(因为我们自行加载了Hello类之后,JVM又试图使用GreeterClassLoaderNew来加载Greeter接口,但这时它调用的方法是loadClass,而不是findClass,默认loadClass会将加载任务委派给它的父类AppClassLoader),仍然把它导向到findClass中,进行我们自己的加载:

 @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        System.out.println("i am called.");
        if(name.endsWith("Greeter")){
            return this.findClass(name);
        }
        return super.loadClass(name);
    }

好了,这样的结果造成了Greeter也是由GreeterClassLoader定义的,并且AppClassLoader并不知道,所以在执行

Greeter ig=(Greeter)c.newInstance();

时,它又加载了Greeter,并且因为此时GreeterClassLoader和AppClassLoader并没有共享Greeter接口,所以这个转换失败了。

结果:

Exception in thread "main" java.lang.ClassCastException: greeter.Hello cannot be cast to greeter.Greeter

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
分析AppClassLoader,ExtClassLoader 和URLClassLoader 的关系
一看你就懂,Java中的ClassLoader详解
classloader加载class文件的原理和机制
JVM类加载机制详解(二)类加载器与双亲委派模型
Java基础中一些值得聊的话题(加载篇)
javaEE防盗版-java之类加载
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服