我是一个坑。
Tag: 语言基础
Java包、类的加载机制(理论)
本人Java菜鸡一枚,如果下面的内容有误,请各位拼命留言,我秒秒钟改!
做项目的同时努力补全知识点或许是当务之急?
问题
此面试问题暴露了我对Java内部原理的无知,哈哈,赶紧学。题外话:换做面C++的话,我估计面试官会用一个多继承里头的问题来考。
有a.jar和b.jar,里面都有某个类MyClass且他们的包名完全相同,但是有不同的foo()方法,在一个项目中同时引入这两个jar会出现什么奇妙的事情呢?
这个问题的答案是,要根据类加载器的加载顺序来决定我们构建MyClass对象的时候究竟会先找到哪个jar里头的文件,a.jar/b.jar哪个运气好拔得头筹了,剩下的就被忽视了。
如果我们构建了不同的类加载器对象的话,可以通过自定义的类加载器来同时加载这两个长得看起来一模一样的类,并且可以各自调用其方法属性什么的。
更详尽的回答可以看这里:http://stackoverflow.com/questions/6879652/possible-to-use-two-java-classes-with-same-name-and-same-package
包(Package)、编译单元
C/C++里面用#include来表示引用的类(文件),并且用过C++的盆友肯定知道namespace 这个关键字可以用来隔离名字相同东西,最常见的莫过于using namespace std; 了。以上这些与所谓“访问权限控制”有关(详见《Java编程思想》第6章)。
而Java以及诸如Python这种语言采用包(package)作为库单元,一个包内包含一组类,比如ArrayList在Java标准库中被放置在java.util包(名字空间)下。包的作用有点像地址,在寻找一个类的时候就像快递分拣一样,根据包的名字能找到最终的目标,一个不恰当的例子就是,package 浙江.杭州.西湖 底下可能会有 同仁医院 这个类,而 package 福建.福州.鼓楼(这个例子不恰当,因为按照Java的原意,包名应该是“从后往前”写的,如cn.com.ibm) 底下也可以有 同仁医院 这个类,在Java代码中使用类的内容的时候,Java虚拟机就得根据包的名称来确定你要引用的到底是哪个医院类了。
而所谓编译单元,大多是指Java源代码文件。一个编译单元的后缀就是.java,文件里只能有一个public的类且该类的名称与文件名相同,否则就会导致编译错误。Java解释器的运行过程大致是:找出环境变量CLASSPATH(包含一个或多个目录)用作查找.class文件的根目录。从根目录开始,解释器获取包的名称并将每个句点替换为反斜杠\,比如 浙江.杭州.西湖 就变成了目录 浙江\杭州\西湖(根据操作系统可能略有不同,变为正斜杠/也是有可能的)。随后解释器就在这么个目录中查找对应你给的类名称的.class文件,比如上面的 同仁医院.class。
编译执行流程
估计大家都知道,Java中的类可以被动态加载到Java虚拟机中执行。下面这张图(推荐https://www.processon.com/,在线画简单流程图的好东西)可以大致说明:
Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance() 方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
基本上所有的类加载器都是 java.lang.ClassLoader 类的一个实例。上图可以看出一点,就是通过不同的类加载器,可以加载出不同的Class类的实例。
java.lang.ClassLoader
之前说过类加载器是干啥使的,除了加载类以外,其实诸如图像文件什么的资源也是它加载的,不过这里暂且不谈。
java.lang.ClassLoader其实是一个抽象类,有几个比较重要的方法:
- getParent() 用于返回该类加载器的父类加载器;这个方法与经常能看到的“双亲委派”这个关键词有关:)
- loadClass(String name) 加载名称为name的类,返回java.lang.Class实例。
该方法有三步:
1.调用findLoadedClass方法看看是不是已经加载过这个类了;
2.如果没有,在父类加载器上调用loadClass方法。如果父类加载器是null(啊..到底了…),则用虚拟机的内置类加载器(下面讲到的引导类加载器);
3.还是不行,调用findClass方法查找类 - findClass(String name) 查找名称为name的类,返回java.lang.Class实例
- findLoadedClass(String name) 查找一个名为name的已经加载过的类,返回java.lang.Class实例
- defineClass(String name, byte[] b, int off, int len) 把字节数组b中的内容转换为Java类,返回java.lang.Class实例,这是个final方法
- resolveClass(Class<?> c) 链接指定的Java类
注意:里面的参数name都是二进制名称。
系统提供的类加载器主要有三类:
- Bootstrap class loader: 引导类加载器,用原生代码写的,用途就是bootstrap Java的核心库。
- Extensions class loader: 扩展类加载器,用来加载扩展库。
- System class loader: 系统类加载器,根据Java应用类的路径(CLASSPATH)加载Java类,一般我们写的东西就是它来加载的。
除了写好的类加载器外,我们可以自己继承java.lang.ClassLoader来实现自己的类加载器,满足一些奇奇怪怪的需求,比如问题1…
getParent()——找“爹”方法
在参考文献中是这么写的“对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器”,是不是很拗口?比如我们自己继承了个ClassLoader类的话(假设他叫MyClassLoader),一般会在这个方法里面返回System class loader,为啥呢?因为我们自己写的MyClassLoader也是个类(先有鸡还是先有蛋,哈哈),也需要一个加载器把MyClassLoader加载了,我们才能用它去加载别的东西。
有了这个方法,就可以在加载类的时候有一个向上追溯的找类过程,这个感觉就有点像JavaScript里面的XX链(原型链、作用域链……哦,扯远了)。类加载器之间可以形成树形结构,比如下面这样子(图版权归IBM所有):
一路都可以getParent()上去,有些JDK可能输出不到最初的那个引导类加载器(AppClassLoader的getParent()返回了null)。
类加载器的代理模式——”我”是”我”?
Java虚拟机怎么判断两个类相同呢?通过包名+类名?不是的,还得看加载这个类的类加载器是不是一样。本文的第一张图可以看出,相同的字节码,被不同的类加载器加载之后,得到的Class类实例是不同的。
不同的类加载器使得包、类名称完全相同的两个类可以并存在Java虚拟机中,且不同加载器加载的类之间是不兼容的。代理模式的设计动机是保证Java核心库的类型安全。我们知道java.lang.Object类是”万物的基础“,如过这个东西能够用自己的类加载器来先加载的话,那就可能存在多个版本的java.lang.Object类并且他们还互不兼容,这就懵逼了。
加载类的过程
首先明确一点,由于类的加载是自顶向下尝试的,真正完成类加载工作的加载器和启动加载过程的加载器有可能不是同一个。
defineClass方法的作用是”真正的“完成类的加载过程(你看它的传入参数,字节数组什么的,多么朴实),执行这个方法的加载器被称作定义加载器;启动类的加载过程是调用loadClass方法实现的(它只要给个名字就行),执行这个方法的加载器叫做初始加载器,它要找不到类的话,就抛出个ClassNotFoundException异常。成功加载一个类后,会把得到的Class类实例缓存起来,下次请求时就不会重复加载了,即对于一个类(全名表示)来说,loadClass方法不会在一个classLoader里头重复调用。
额,如果有空的话,下一篇会实作一个自定义的ClassLoader。
参考文献
- 彻底搞懂Java Classloader: http://weli.iteye.com/blog/1682625
- 深入探讨Java类加载器:https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
- 《Java编程思想(第四版)》