虚拟机类加载机制
《深入理解Java虚拟机 第2版》
1 机制
1.1 类的加载
1.1.1 概述
Java语言中,类的加载、连接和初始化都是在程序运行期间完成的,这种策略使得java语言具备在运行时动态扩展功能的特性,动态扩展的来源可以是从网络端下载字节码,也可以是用户手动拷贝进jvm等。JSP、OSGi等技术,都是利用了在运行期动态加载的特性。类从被加载到虚拟机内存,到被卸载,主要经历如下声明周期,其中初始化这一步是我们比较关心的,因为它会影响代码的执行结果:
只有5种情况下会进行初始化,这些又称为主动引用:
1、 遇到new命令,或者操作static属性和方法(常量除外)
2、 通过反射实例化一个类
3、 当子类初始化时,会优先触发父类进行初始化
4、 Main方法会默认被初始化
5、 Jdk1.7中的一个特性(忘记了)
除了主动引用外,也有很多被动引用,比如操作静态变量只会触发变量所属类的初始化,如父类有一个static的String v,子类直接调用这个v(SubClass.value),会触发父类的初始化,但不会触发子类的初始化,因为这个value是父类的静态属性,当然还有一些其他的被动引用,还有数组的new命令、直接引用常量等1.1.2 加载过程
第一步:加载。根据一个类的全限定名来获取定义此类的二进制字节流,并将字节流加载进jvm,字节码的来源可以是zip包、网络、运行时计算、jsp文件和数据库中读取等。最后一步:初始化。根据程序员通过程序制定的主管计划去初始化类变量和其他资源。1.2 类加载器
1.2.1 什么是类加载器?
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其再Java虚拟机中的唯一性,每一个类加载器,都拥有一个自己独立的类名称空间。通俗的讲,只有在同一个类加载器中加载的类,才有可比性,否则,比较2个类是否相等是毫无意义的,比较可以通过obj instance of ClassA实现。Java虚拟机只有2种类加载器,一种是启动类加载器,是虚拟机自带的,另一种就是所有其他的加载器。其他的加载器,全部都由java语言实现,并且均继承自抽象类
java.lang.ClassLoader。再细分一下,可以将类加载器细分为:启动类加载器、扩展类加载器、应用程序类加载器和用户自定义类加载器,应用程序类加载器是用户classpath的默认类加载器。
1.2.2 如何选择类加载器?
既然一个jvm可能存在多个类加载器,那么加载一个类,该用什么加载器呢?是由用户指定的?还是系统默认的?还是?答案是:通过双亲委派机制,得到一个类对应的加载器,然后加载它。
1.2.3 什么是双亲委派机制?
上图中的这种层次关系,称为双亲委派模型。它要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系,不以继承的方式来实现,而是组合的方式来复用父加载器的代码。组合的意思是子的代码中调用父的代码,或者包含父对象,通过父对象调用父的接口。 双亲委派机制的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父加载器去完成,每一个层次的家再次都是如此,因此,所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己加载,类似递归。那么,什么时候才会反馈自己无法加载这个类呢?很简单,就是加载器指定的路径下,搜不到这个类。
1.2.4 双亲委派机制的优势
双亲委派模型的优势是,java类随着它所属的类加载器一起具备了一种带有优先级的层次关系。对于一个等待被加载的类,它终将被上送到最顶层的类加载器进行加载,对于这个类加载器而言,它搜索类的路径是固定的,所以,不管类的实现的jar包在哪里,只要它能被顶层类搜索到,那就默认会加载顶层类搜索到的jar包里的类,而不会使用用户自定义的路径下的jar包里的class,这样做的好处在于对于java系统的类,如java.lang.Object,即时被用户覆盖了,也不会生效,因为rt.jar会被启动类加载器搜索到并加载其中的Object类,从而使用户自定义的Object类不生效。但是,用户自定义的java.lang.Object是能够编译通过的。
1.2.5 双亲委派模型的实现
实现双亲委派模型非常简单,代码都集中在java.lang.ClassLoader的loadClass()方法中,这个代码中首先会执行parent.loadClass(),依次递归,从而实现双亲委派的特性。代码类似如下:Class loadClass()
{
Class c = findLoadedClass(name);
If(c == null)//如果没有加载过该类
{
Try{
If(parent != null)
{
C = parent.laodClass()//递归调用loadClass
}
Else
{
C = findBootstrapClassOrNull(name)//直到递归到顶层
}
}catch (e)// //如果抛异常,所以父类加载不到
{}
If(c == null)//递归退出的条件是异常,一旦异常,说明父一层找不到类加载器
{
C = findClass(name) //findClass默认获取当前层的类加载器
//findClass方法运行用户重写,实现自己的逻辑,指定对应的类加载器,但其依然只会在父类加载器无法加载的情况下执行。不建议重写loadClass()方法
}
}
1.2.6 双亲委派模型的破坏案例
双亲委派模型并不是强制的规范,故可以人为的打破!!!子加载器加载类的范围是父的范围+自身的范围,也就是说,对于子加载器而言,只要父类加载器可以加载的类,子类加载器一定可以加载,因为它默认会先调用父类加载器。但是反过来,对于父类加载器而言,它无法加载那些子类加载器特有的类。无法加载类,就意味着无法调用这些类的任何方法,通俗的说,父类能够加载的往往只是基础API,而子类加载器可以加载的还包括自身特有的类。这种模式下,子类加载器所在的模块可以轻松的调用父类加载器所加载的API,而父类加载器所在的模块无法访问子类特
有的类的信息,因为它连这些类都无法加载,更别提访问了。这样也是双亲委派模型自身设计的缺陷,但是很多情况下,父模块通常会需要调用子模块的API,比如OSGi,此时就只能依靠线程上下文类加载器了。
像上面这种情况,就需要人为的打破双亲委派模型。双亲委派模型的打破,包括3个阶段,第一阶段是JDK1.2之前,人们为重写loadClass方法,从代码上破坏了双亲委派模型。第二阶段是父类需要调用SPI提供的接口的时候,由于接口所在的jar包是在服务自身的classpath下的,所以需要打破,比如OSGi。第三阶段是动态代理功能的引入。
1.3 经典面试题:java.lang.String类
这里介绍一个有意思的面试题,关于string这个类的。首先面试官会问,我能不能定义一个类,继承自java.lang.String?然后接着问,能不能定义一个类,也叫java.lang.String?对于第一问,一般都能回答,答案是不能,因为String是final类,不能被继承。
对于第二问,首先是定义这样的类后,编译能不能通过?答案是能。因为语法上没有任何错误。然后面试官会问,那我能使用这个类吗?最差的程序员会回答,既然能编译,就肯定能使用,这个回答当然是错的。好一点的程序员会说,一个默认加载器不能加载2个相同名称的类,如果通过自定义类加载器进行,那允许存在2个String,所以如果是通过自定义类加载器加载的java.lang.String应该是可以使用的,但是这个回答也是错的。正确的答案应该是无法使用,为什么呢?因为双亲委派模型,即使你自定义类加载器,根据双亲委派原则,系统还是默认会加载rt.jar下的java.lang.String,所以你这个类是不会被加载的。但是,如果你能够告诉面试官,我的自定义类加载器重写loadClass()方法,打破双亲委派模型,所以可以被加载,这样是OK的。
2 优秀案例
本节对应书本的第9章的学习,主要介绍tomcat、OSGi和动态代理3个案例分别是如何使用累加器的。
2.1 Tomcat
Tomcat采用了正统的双亲委派模型的类加载器架构。一个web服务器,面临很多问题(具体如下),因此,注定了Tomcat服务器必须有多个类加载器。1. 同一tomcat下的不同web服务间的lib需要互相隔离
2. 同一tomcat下的不同web服务间的lib要能够共享
3. 服务器自身的lib与web服务的lib独立
4. Jsp文件要能热交换。每个web服务都有自己的webApp类加载器,一个web服务内部,每个jsp页面都有自己的类加载器。JSP的hotswap就是通过删除和新增jsp页面对应的加载器来实现的。
2.2 OSGi
OSGi是把类加载机制玩的最厉害的。它的类加载机制不再是简单的双亲委派模型,而是在这基础上做了改进,只有部分规则遵循双亲委派,其他规则基本都是扁平状的依赖关系。同时,它的import和export列表,也使得依赖之间的界限更加的精细化。例如:Bundle A:声明发布了packageA,依赖了java.*包
Bundle B:声明发布了packageB和packageC,同时也依赖了java.*包
Bundle C:声明发布了packageC,依赖了packageA
类加载架构如下:
图3
2.3 字节码生成技术与动态代理
动态代理的本质是动态生成一个新的类,把被代理的类的所有方法都生成一份,并且每个方法内部都调用invoke(functionName)方法,这样,用户调用方法时,由于使用的是代理对象,所以就调用了同名的代理方法,而代理方法会在通过invoke(真是方法名)实现对被代理类的对应方法的调用,但是在这之间,代理类通常会做一些额外的处理,这种处理就是代理自身的目的。Class Hello implements IHello{ //Hello为被代理的类
@Override
Public void sayHello() //sayHello是被代理的方法
{System.out.println("Hello world!")}
}Main(){
IHello hello = (IHello) new DynamicProxy().bind(new Hello());
hello.sayHello(); //此时由于hello是代理对象,所以sayHello其实是代理包装的方法
}
Public final void sayHello()
{
This.h.invoke(this,m3,null) //invoke才会触发真正sayHello的调用,m3是指函数名
//sayHello Method对象,null入参
}
动态代理依赖的技术是字节码动态加载。通俗的说,就是把被代理类的class文件的字节码解析出来,按照一定的规则,比如在它的sayHello方法外面包裹一些代码对应的字节码,重新组装出一个合法的类的字节码,然后让类加载器加载这个字节码进jvm,就变成了一个新的类。只是这种字节码的拼装一般是很死板的,没有灵活性,还在动态代理其实也是一套比较固定的套路。如果对字节码要有比较复杂的操作,可以学习cglib。