java基础中一些值得聊的话题(加载篇)

   在开始java的类加载旅程之前,可以先参考这里了解一些类加载器在tomcat中的应用。

   在最初执行java这个命令时,便会调用ClassLoader的getSystemClassLoader方法去显式或者隐式的加载main方法所在的类及其所引用的类。getSystemClassLoader会返回AppClassLoader,后者是URLClassLoader的一个子类。

   所以,最初的一个问题是:先有鸡还是先有蛋的问题,因为ClassLoader的整套体系是打包在jre/lib/rt.jar中的。只有rt.jar先被加载进来,才能够加载别的类;但是rt.jar又是被谁加载的呢?自然就是大名鼎鼎的BootstrapClassLoader。它就是'鸡'。所以严格来讲,BootStrapClassLoader并不是整个体系中的一部分(可以用-Xbootclasspath指定bootstrap加载的位置)。

   当rt.jar被加载进来后,ClassLoader会调用getSystemClassLoader,其中最重要的一步就是初始化Launcher,ExtClassLoader以及AppClassLoader,另外就是将ContextClassLoader设为AppClassLoader。ExtClassLoader与AppClassLoader都是URLClassLoader的子类,分别会加载java.ext.dirs和java.class.path路径下的jar资源,前者一般指向jre/lib/ext下的所有jar,后者就是我们经常念叨的classpath。区分这两个ClassLoader的主要目的是,让他们形成层级关系,ExtClassLoader为AppClassLoader的父ClassLoader,有了层级关系,便可随意使用双亲委托模型了。

ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader");
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader");
        }

        // Also set the context class loader for the primordial thread.
        Thread.currentThread().setContextClassLoader(loader);
   接下来一个比较重要的问题是 ClassLoader究竟干了什么?通常我们只知道它加载了一个类进了jvm,但是具体做了什么呢?

   java设计者把classloader加载一个类的过程分为4步:第一步,从某个地方得到我们想要的字节码二进制流;第二步,读入字节码流并转化为Class;第三步,链接;第四步,初始化。其中,第二步一般比较固定,因此ClassLoader提供了defineClass来完成这步;

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);

        Class c = null;
        String source = defineClassSourceLocation(protectionDomain);

        try {
            c = defineClass1(name, b, off, len, protectionDomain, source);
        } catch (ClassFormatError cfe) {
            c = defineTransformedClass(name, b, off, len, protectionDomain, cfe,
                                       source);
        }

        postDefineClass(c, protectionDomain);
        return c;
    }
  而ClassLoader提供了另一个方法findClass来完成第一步及第二步,即从某个地方读入类的二进制流,然后调用defineClass返回Class

protected Class<?> findClass(final String name)
         throws ClassNotFoundException
  ClassLoader提供了resolveClass方法完成第三步链接的工作。
protected final void resolveClass(Class<?> c) 

  除非特殊需要,否则尽量重载findClass而不是loadClass

  loadClass是java1.0就存在的类,为了增强可扩展性,将findClass和resolveClass封装到了loadClass中,一般我们只需要定义类的加载路径,因此仅需覆盖findClass。

  通常我们显示加载类一般会用到ClassLoader.loadClass, Class.forName,他们的区别见 这里

  resolveClass做了什么?

  resolveClass最终调用了一个本地方法做link,这里的link主要做了这么几步事情:1. 验证Class以确保类装载器格式和行为正确;2.准备后续步骤所需的数据结构;3.解析所引用的其他类。关于这些内容的具体细节,请参考这里http://blog.csdn.net/ns_code/article/details/17881581

  ClassNotFoundException, NoClassDefFoundError, ClassCastException常见问题

  ClassNotFoundException一般发生在显式类加载;NoClassDefFoundError一般发生在隐式加载;ClassCastException一般发生在jar包冲突,比如某个jar包已经被更上层的加载器加载了,但你却要求他强制转为下层加载器加载的同名类;


  接下来讲讲链接的相关知识。

  为什么会发明链接器?

  最初程序员写程序都在一个文件里,随着程序规模的增加,逐渐发现越来越难以维护,扩展。于是,分多个文件和模块就成为必然。

  但是文件间必然相互调用,这就带来了另一个问题:文件A引用了B的某个变量或方法,但是运行时A并不知道他们在内存中的位置。于是人们发明了链接,这种方式会在编译阶段将需要引用的变量或者方法作个记号,这时形成A.o和B.o两个目标文件;通过ld链接器的链接,会将B中变量或方法的地址重新修改A的记号最终形成一个可执行文件,实际上就是把A和B合在一起工作了。

   这种方式就是静态链接

   但是静态链接有个问题,比如A需要B模块的方法,C需要B模块的变量,D需要B模块的方法……如此一来,当我们编译为A, C, D几个可执行文件时,他们都会在内存中引用B,即B在内存中有多份拷贝。这带来很多问题:首先浪费了内存;其次修改了B模块需要重新修改并发布A, C, D几个可执行文件,这是非常不方便的;于是动态链接的一个思想是在A, C, D调用时再确定记号的地址,而B则通过延迟加载的方式按需加载到内存(如果已经加载则不再重复)。这样一来,内存中总是只有一份B的拷贝,解决了上面的问题。java的类加载机制即是这种灵活的方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值