先说class文件
编写的java代码首先被编译成class二进制文件,这是实现平台无关性的关键一步。至于class文件里面的具体内容,可以用编辑器打开,结合一些教程一项一项的分析。
其实,我主要想说的是,一个class文件代表一个类型(类或者接口),也可以理解为元数据。在我们的程序中访问一个类型的元数据,并做点什么,比如反射调用等,是很有意义的。
类加载
过程:通过类(或接口)的全限定名找到对应的class文件,读取里面的内容,在方法区上分配运行时数据结构,并返回一个java.lang.Class的对象(不一定在堆中),代表这个类型和访问方法区中的类型数据。
类装载器:每个类装载器都有自己的命名空间,装载特定区域的类型,实现类型加载。系统提供了3个类加载器:启动类加载器(Bootstrap ClassLoader):由虚拟机实现,加载JAVA_HOME/lib目录下以及-Xbootclasspath参数所指定的路径下的可识别的类库;扩展类加载器(Extension ClassLoader):加载JAVA_HOME/lib/ext目录下以及java.ext.dirs系统变量所指定的路径下的类库;应用程序类加载器:(Application ClassLoader):加载classpath里面的类。当然,我们还可以自定义类加载器。
双亲委派:除了启动类加载器,其余的类加载器都必须要有父类加载器。这里的父子关系不是继承的父子关系,而是通过组合来实现复用。当一个类加载器收到类加载的请求时,它先是递归的向父类加载器委派这个请求,直至启动类加载器,只有当父类加载器无法完成加载的时候,子类加载器才会去加载这个类。思考一下,为什么要这样设计呢?所有类的老祖宗是Object,如果我们自己写了一个恶意的Object类,并且自己加载进来,那就会造成混乱。可是有了双亲委派,就有了优先级,所以最终只有一个java.lang.Object类提供服务。
自定义类加载器:通过ClassLoader的源码可以看到,loadClass方法实现了双亲委派的逻辑,并最终会调用findClass方法去完成主要工作。所以在自定义类加载器的时候,如果想突破双亲委派模型,可以重写loadClass方法,否则重写findClass方法即可。在findClass方法里面,我们可以先通过类名获得该类文件的二进制流,然后调用defineClass方法去完成类加载的工作。这里的defineClass方法由虚拟机实现,我们不用管,而且它是final和protected,所以我们只管继承,然后调用。
突破双亲委派:先回想一下jdbc的工作模式。sun公司首先制定一套标准,也就是一大推接口,然后各个数据库厂商去写自己的实现。当我们的程序要用的时候,就把相应的数据库的驱动jar包导入classpath。这里就会产生一个问题。sun公司的接口肯定放在基础类库里面,由启动类加载器加载,但是这些接口里面会用到第三方厂商提供的具体实现类,然后就需要去加载。如果按照双亲委派模型,启动类加载器是不能去classpath里面加载类的。这是一个硬性的逻辑阻碍。于是,那些Java大神设计出了一个线程上下文类加载器(Thread Context ClassLoader)。如果创建线程的时候没有设置,将会从父线程继承过来,如果整个程序都没有设置的话,默认是Application ClassLoader。所以在一个线程中,当用Bootstrap ClassLoader去加载基础类库的时候,可以用线程上下文类加载器去加载其他的类。
tips:ClassLoader的loadClass方法和Class的forName方法都能加载类,区别是,loadClass只加载类型,而forName不但会加载类型,默认还会最终初始化类型。