虚拟机类加载机制

读深入理解JAVA虚拟机 第七章,记一下内容

虚拟机类加载机制

概述
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验转换解析初始化,最终形成可以被虚拟机直接使用的java类型。这就是虚拟机的类加载机制

在java语言里,类型的加载,连接和初始化过程都是在程序运行期间完成的。这种策略会令类加载时稍微增加一些性能开销,都是会为java应用程序提供高度的灵活性。java里天生可以动态扩展语言的特性就是依赖运行期动态加载和动态连接这个特点实现的。
比如编写一个面向接口的应用程序,可以到运行时再指定其实际的实现类;用户可以通过java预定义或自定义的类加载器,让一个本地应用程序可以在运行时从网络或其他地方加载二进制流/文件,制作为程序代码的一部分。(从网络或者其他地方加载二进制文件作为代码的一部分)

类加载的时机

类加载的生命周期包括 加载(Loading),验证(Verification),准备(Preparation),解析(Resolution),初始化(Initialization),使用(Using)和卸载(UNloading)七个阶段
其中验证,准备,解析三个部分统称为连接(Linking)
其中解析和初始化不一定是顺序的,可能先初始化再解析,这个是为了支持java语言的运行时绑定。
这些步骤都是交叉混合执行的,而不是按部就班的开始进行完成的

初始化:有五种情况,必须立刻对类进行初始化(包含加载,验证,准备):
1,遇到new,getstatic,putstatic或者invokestaic这4条字节码指令时,如果类没有初始化过,需要先触发初始化。(new对象,读取或者设置一个static字段(不含final修饰,已经在编译器就放入常量池的静态字段),以及调用静态方法。
2,使用Java.lang.reflect包的方法对类进行反射调用的时候,如果没初始化,就先初始化。
3,初始化一个类的时候,发现父类还没初始化,就先初始化父类。
4,虚拟机启动的时候,会把包含main方法的那个类进行初始化。
5,使用JDK1.7的java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化。(说白了就是static方法或者字段)。

除此之外的引用类的方法,都不会触发初始化,称为被动引用,
比如
通过子类引用父类的静态字段,不会导致子类初始化。
常量因为在编译阶段就已经存入了常量池,没有引用到定义常量的类,所以调用常量也不会触发定义常量的类初始化。(详情见p213)
对于接口,在接口初始化的时候,唯一的区别是,不会加载父接口的初始化,而是在真正用到付借款的时候,才会初始化,比如引用父接口中定义的常量。

类加载的过程

加载

1,通过一个类的全限定名来获取定义此类的二进制流。
2,把字节流所代表的静态存储结构转化为方法区的运行时数据结构(把字节流的结构转化加载进内存,变成方法区的数据结构)
3,在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口(在方法区生成一个class类型的对象,代表这个类,给这个类的访问提供入口)

获取二进制流的方法是多样的,比如从zip,网络中获取,运行时计算生成(动态代理技术,java.lang.reflect.Proxy),由其他文件生成(比如jsp),由数据库里读取。

数组类不是由类加载器去加载的,而是由虚拟机直接创建的,只有数组的元素类型是类加载器创建的

加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在方法区之中。(虚拟机自行定义的格式,没有规范规定)
实例化的java.lang.Class对象(并没要明确规定是在java堆中,有的虚拟机放在方法区里)

加载阶段与连接(验证,准备,解析)阶段的部分内容,是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始。但是这两个阶段的开始时间仍然保持着固定的先后顺序。

验证

确保class文件的字节流中的信息符合当前虚拟机的需求,并且无害。
因为class文件不一定是。java文件编译生成的,可能是通过其他方式生成的,在里面就可能出现“不合格的代码”,比如把对象转型为并未实现的类型,跳转到不存在的代码行等等
如果验证不合格,编译器将拒绝编译。并且抛出java.lang.VerifyError异常或者其子类异常。
验证主要包括:
文件格式验证
文件格式是否符合class文件格式的规范,代码jdk版本号是不是在虚拟机处理范围内,etc,p216页
文件格式验证结束,数据通过文件格式验证,存入方法区,后面3个步骤都是基于方法区的存储结构进行的,不会再直接操作字节流

元数据验证
对字节码描述的信息进行语义分析,比如这个类是否有父类(除了java.lang.Object之外,其他的类都有父类);
这个类否继承了不允许被继承的类(final修饰类);
类中的字段,方法是否与父类产生矛盾(比如覆盖了final字段,或者不符合规则的重载,比如入参一样,返回类型不同的重载)
这个阶段主要是进行语义校验,保证不存在不符合java语言规范的元数据信息

字节码验证
对方法体进行校验,保证不会在运行时出现危害虚拟机安全的事件

就算通过校验,也不能保证绝对的安全,但是通不过肯定是不安全。

符号引用验证
在解析阶段发生,把符号引用转化为直接引用。
对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,总结为:
符号引用能不能找到对应的类,对应的方法,字段,以及他们的访问性(public,private..)是否可以被当前类访问。

准备

正式为变量分配内存,并设置类变量初始值的阶段,这时候分配内存的仅包括类变量(static变量),不包括实例变量。这里的初始值是0,而不是我们赋值的初始值。
只有常量(static final变量)才会在准备阶段被赋值用户初始化的值。

解析

在解析阶段发生,把符号引用(标记)转化为直接引用(地址)。(把引用的目标加载到内存里)
符合引用:用一组符号来描述所引用的目标,和虚拟机内存布局无关

直接引用:可以是指向目标的指针,相对偏移量或一个能间接定位到目标的句柄,和虚拟机内存布局有关

加载时间没有规定,只要在相关的使用命令执行之前就行了(p211页),所以可以在类被加载器加载的时候对常量池中的符合引用进行解析,也可以在要用到的时候再去解析它。

类或接口解析

字段解析
对字段解析的搜索过程,假定被解析的类或接口为C
1,搜索C本身,如果包含匹配的字段,返回
2,如果C实现了接口,那将会按照继承关系从下往上递归搜索各个接口和它的父接口,来查找匹配的引用,找到返回。
3, 如果C实现了父类(C本身不是java.lang.Object),按照继承关系从下往上递归搜索其父类,找到返回。
4,都找不到,抛错NoSuchFieldError。
5,找到,进行权限校验

实际应用中,如果在父类和接口中出现同名字段,虚拟机也可能拒绝编译

类方法解析
接口方法解析 略 p224

初始化

执行类构造器中方法的过程,给变量和其他资源赋用户定义的初始值。
clinit方法是由编译器自动收集类中的所有类变量的赋值当做和静态语句块(static{})中的语句合并产生的,顺序是由源文件的顺序绝对的。
静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,静态语句块可以赋值,但是不能访问。

虚拟机会保证在执行子类的clinit之前,一定会先执行父类的clinit
因此第一个被执行的方法一定是java.lang.Object的clinit。

所以父的静态代码块会先于子类执行

如果一个类没有静态代码块也没有需要赋值的变量,编译器可以不为这个类生成clinit。

接口的clinit方法不需要先执行父接口的clinit方法
只有当父接口中定义的变量使用时,父接口才会初始化。
接口的实现类在初始化时,也不会执行接口的clinit方法。

多个线程同时初始化一个类,会被加锁,只有一个线程可以执行clinit,其他的线程会阻塞等待。(clinit只需要执行一次,以后的new就不用执行了)

类加载器

类加载器用于实现类的加载动作。
并且由类加载器和加载的类本身来一同确立其在java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类名称空间。
两个类是否相等,是在两个类都由同一个类加载器加载的前提下才有意义的。
否则,即使来源于同一个class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。

双亲委派模型

从虚拟机的角度来看,只有两种类加载器:
启动类加载器(boostrap classloader),使用c++语言实现(不一定,看是什么虚拟机),是虚拟机自身的一部分
另一种是其他的类加载器,由java语言实现,独立于虚拟机外部,并且全部都继承自抽象类java.lang.ClassLoader

从java开发人员的角度来看:会使用到以下3种系统提供的类加载器

启动类加载器

负责加载\lib 和 -Xbootclasspath参数指定路径中的类库加载到虚拟机内存中,(名字要是虚拟机能识别的);
无法被java程序直接引用,用户在编写自定义加载器时,如果需要把加载请求委派给引导类加载器,那直接使用null代替即可

扩展类加载器

Extension ClassLoader,负责加载\lib\ext目录中的所有类库,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器;由sun.misc.Launcher$ExtClassLoader实现

应用程序类加载器

Application ClassLoader,由sun.misc.Launcher$AppClassLoader实现,是ClassLoader中的getSystemClassLoader()方法的返回值;
一般也称为系统类加载。负责加载用户类路径(ClassPath)上所指定的类库,开发中可以直接使用这个类加载器。

如果有需要,还可以自定义类加载器

双亲委派模型

启动类加载器<-扩展类加载器-<应用程序类加载器<-自定义加载器(多个)

类加载器之间的这种层次关系,称为类加载器的双亲委派模型(Parents Delegation Model).

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。这里的类加载器之间的父子关系一般都不会以继承(Inheritance)关系来实现,而都是使用组合(Composition)关系来复用父加载器的代码的。

于JDK1.2版本引入,不是一个强制性约束模型,但是是推荐的一种类加载器实现方式。

工作过程:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委派给父类加载器去完成,每一个层次的类加载器都是如此。因此所有的加载请求最终都应该会传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时(它的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载。

好处是让java类随着它的类加载器一起具备了一种带有优先级的层次关系。比如Java.lang.Object,无论哪个加载器去加载它,最终都会委派给顶端的启动类加载器去进行加载,就保证了Object类在程序中各个类加载器环境下,都是同一个类。

如果没有使用双亲委派模型的类加载器,用户自己定义一个类命名为java.lang.Object,放到classpath下,系统里就会出现多个不同的Object类,就乱了。 在现实中,用户尝试去编写一个和rt.jar类库中已有类重名的java类,是无法被加载运行的,虽然可以正常编译。

代码双亲委派模型的代码都几种在java.lang.ClassLoader的loadClass()方法之中。

1,检查是否已经加载过;
2,如果没有,调用父类加载器的loadClass方法去加载;
3,如果父类不存在,就用启动类加载器作为父加载器去加载;
4,如果父类加载失败,报错,就用自己的findClass(name)去加载。
5,resolve为true,本地方法解析class。(看不见源码)

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
}
破坏双亲委派模型

具体内容略
因为是jdk1.2以后才出来的双亲委派,所以有很多为了兼容,违背双亲委派原则的代码。
比如兼容1.2以前的东西
比如涉及SPI的加载动作(JNDI,JDBC,JCE,JAXB,JBI等),是需要通过父类加载器去请求子类加载器去完成类加载动作的(P.234页),打破了双亲委派的层次结构,逆向使用类加载器等。

其他内容:提倡把自己定义的类加载逻辑写到findClass()中,不提倡用户覆盖loadClass()方法。

OSGi实现模块化热部署,每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换模块的时候,把Bundle连同类加载器一起换掉以实现模块热替换。
在OSGi环境下,类加载器不是双亲委派模型中的树状结构,而是复杂的网状结构。
收到类加载请求的时候,类加载器不仅会去父类那边找加载,也会在平级的类加载器中寻找。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值