JVM类加载机制
先说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不但会加载类型,默认还会最终初始化类型。
说说剩下的阶段:验证、准备、解析、初始化。需要注意的是,这些阶段(解析除外)只是按照这个顺序开始,但是执行的过程中可能存在交叉。
验证:就是要对加载的二进制流文件进行各种检查,很好理解。
准备:为类变量(static)分配内存并设置初始值,即所谓的"零值",但是不包括常量(final)。
解析:将常量池的符号引用替换成直接引用,这个阶段发生时间没有明确规定,但是有具体限制:在符号引用被使用之前,必须被解析。
上述3个阶段合称连接阶段。
初始化:这里是类型初始化,不是对象初始化。
对于第一个阶段--加载,没有明确规定时机,但是初始化阶段有且仅有明确的的4种情况:
1、访问类型的静态成员(final常量除外)和使用new关键字
2、反射调用
3、一个类型的父类型先初始化
4、包含main方法的主类
初始化的过程:编译器自动按顺序收集类变量赋值语句和静态语句块(static{})生成<clinit>()方法,如果一个类型没有类变量赋值以及静态语句块,就不会自动生成。JVM需要保证调用子类的<clinit>()方法前先调用父类的<clinit>()方法(接口不必),同时保证线程安全。
最后,说一个特例,数组类。数组类由JVM自动生成,自动创建。假设自定义类com.fbi.A,A[] arrs = new A[10];语句,JVM会生成"[Lcom.fbi.A"这样的一个类型。这不是重点,真正的重点是这条语句只会去加载类A,但不会初始化类A。
类加载与动态代理
动态代理
所谓动态,就是在运行期间生成代理类。不然,有100个需要被代理的类,你就得手动写100个代理类,代码膨胀得厉害。
而我现在的目标是弄清楚jdk如何实现动态代理。
阅读Proxy类的源码能够看清大体流程:
1、我们自己提供接口和类加载器,然后jdk去通过Class.forName的方式去加载以及初始化这些接口,并生成类型信息。
2、有了这些接口的类型信息,就可以通过反射得到所有的方法的信息
3、这个时候有2种选择:通过已有的信息生成代理类的java源代码文件,然后动态编译生成class文件。
而jdk用的是另一种,将已有信息直接写入class文件。因为class文件的内容分布是固定的,所以按照class文件的格式一个一个的写二进制流就可以实现。
相比第一种,第二种的效率更高。
4、有了class文件,就可以调用defineClass方法生成代理类的类型信息
5、有了代理类的类型信息,就可以通过反射调用参构造方法,把我们自定义的InvocationHandler传进去,生成代理类的实例。
通过动态代理的实现原理,可以清楚的看到:类加载机制相对灵活,只要你能得到符合规范的class文件,就可以生成对应的类型信息,然后通过反射就可以干很多事情。
但是动态代理的唯一遗憾是必须要实现接口,而另外还有一种方式---cglib,可以更加灵活的实现动态代理。