美文网首页
虚拟机类加载机制

虚拟机类加载机制

作者: 简书徐小耳 | 来源:发表于2019-03-14 18:02 被阅读0次

    类加载的生命周期

    • 加载,验证,准备,初始化和卸载这五个阶段顺序是确定的,但是解析和初始化有可能是交替执行的。

    • 解析在初始化之后,是为了支持Java语言的运行时绑定。

    加载

    jvm加载说明

    • JVM没有规定了什么时候开始加载,但是规定了什么时候开始初始化。
    • 在初始化之前必须已经将class文件加载好。

    jvm加载需要做的三件事情

    • 通过一个类的全限定名来获取定义此类的二进制字节流。
    • 将class文件(二进制流)代表的静态存储结构转化为方法区的运行时数据结构--该数据结构即我们之前说的类文件包含的常量池,字段表,方法表,属性表等等。
    • 在java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。

    连接:验证,准备,解析

    验证过程

    • 文件格式验证-主要是检验class文件是否符合要求。
    • 元数据验证-java类是否由父类,是否多继承等等。
    • 字节码验证-进行数据流和控制流的分析,主要是确保类的方法不会危害虚拟机的安全(比如强转,比如break到其他方法体等等)。
    • 符号引用验证-发生在解析阶段,将符号引用变为直接饮用。需要校验通过符号引用描述的全限定名称是否能找到对应的类,指定类中
      是否存在符合方法的字段描述符及简单名称等等。

    验证总结

    • 验证非常重要,但是不是必须的,可以关闭进而提升虚拟机类加载的时间。

    准备

    • 为类变量分配内存,并赋予初始化值(初始值不是我们的代码中设置的,那个会在编译器(final),或者类初始化时候赋值)

    解析

    • 主要就是将常量池符号引用替换为直接引用。
    • 1.符号引用:字面量,可以通过该字面量找到所引用目标(类,方法,字段)
    • 2.直接引用:指针或者句柄,通过指针或者句柄找到目标的真是内存地址。
    • 3.解析的时机是可以在运行时机也可以在启动时机,具体需要在执行以下字节码指令时候
    • anewarray:创建新数组
    • getfield:获取指定类的实例域(域就是字段),并将其值压入栈顶
    • getstatic:获取指定类的静态域,并将其值压入栈顶
    • instanceof:检查对象是否是指定的类的实例。如果是,1进栈;否则,0进栈
    • invokeinterface:调用接口方法
    • invokespecial:调用超类构造方法、实例初始化方法、私有方法
    • invokestatic:调用静态方法
    • invokevirtual:调用实例方法
    • multianewarray:创建指定类型和维度的多维数组(执行该指令时,栈中必须包含各维度的长度值),并且其引用值进栈
    • new:
    • putfield:为指定的类的实例域赋值
    • putstatic:为指定的类的静态域赋值
    • 4.虚拟机会对解析结果进行缓存,运行常量池记录直接引用,并把常量标识设置为已经解析
    • 5.解析主要是针对类和接口(CONSTANT_class_info),字段(CONSTANT_Field_info),类方法(CONSTANT_methodref_info),接口方法(CONSTANT_interfaceMethodRef_info)的符号引用。
    • 类和接口(CONSTANT_class_info):如果不是数组则直接加载,如果是数组且数组元素类型为对象,则按照对象去加载。
    • 字段(CONSTANT_Field_info):先寻找这个类或者接口本身是否有字段,找不到先去父接口的字段表寻找,再去父类的字段表寻找。
    • 类方法(CONSTANT_methodref_info):先寻找类,再寻找对应的方法表,找不到则先去父类寻找,找不到再去父接口方法表寻找。
    • 接口方法(CONSTANT_interfaceMethodRef_info):先寻找接口,再寻找对应的方法表,找不到则去父接口方法表寻找。

    初始化

    下面四种情况,jvm必须对类进行主动初始化:

    • 遇到new(new一个对象),getStatic(获取静态变量),putStatic(设置静态变量),或者invokeStatic(调用static方法)4个字节指令
    • 使用reflect包里面的方法对类进行反射调用,如果类没有初始化则就开始初始化
    • 初始化一个类的时候,其父类必须完成初始化
    • 执行main方法的类必须进行了初始化

    初始化就是执行<clinit>方法。

    • 该方法包含static{}以及其他的static变量。
    • 该方法会保证是同步的,如果一个类被多线程初始化,比如Class.forName。这就意味着我们的代码块中如果有很长的阻塞方法,会导致JVM阻塞。

    拾遗

    • 1.数组对象不会导致改类进行初始化,但是 JVM会触发===[L类的全限定名称的初始化,该类是由字节码指令newarray触发。
    • 2.final修饰的类变量会在编译阶段直接存入类的常量池中(A类引用B类的常量,则常量放入了A的常量池),所以对类常量的引用不会触发类的初始化。
    • 3.接口初始化的时候不会要求父接口也完成了初始化,只有真正使用父接口的变量才会初始化

    使用和卸载

    传统的类加载机制

    类加载器特性

    • 1.比较两个类是否相等的前提是使用同一个类加载器。
    • 2.jvm默认加载class,是采用全盘机制。即Aclass中引用Bclass的时候,如果Bclass没有加载过。默认采用Aclass去加载。

    双亲委派

    • 1.即子类会先检查自己的命名空间是否已经加载了,如果有则结束,否则继续2.
    • 2.检测是否有父类,如果有则交给父类执行父类按照步骤1继续执行,如果没有则尝试bootStrap去加载
    • 3.如果最终得到class还是为null 则尝试当前的classloader自己去加载。
    • 4.根据标志准备对class进行连接,也就是验证,准备,解析。

    双亲委派总结

    • 1.先查看当前classloader是否已经加载,然后尝试交给父类。如果父类不存在则交给当前类加载器去加载。
    • 2.双亲委派的好处
    • java核心类不会被外部的类给替换掉。比如Object无论被什么类加载器加载都是同一个(因为最终交给BootStrap)。

    破坏双亲委派模型

    破坏双亲委派的目的

    • 1.如果我们在能BootStrap加载的AClass中调用我们classpath的Bclass,那么根据全盘委托机制和双亲委派机制,Aclass会使用BootStrap去加载Bclass,但是BootStrap是无法加载到Bclass的。所以我们这个时候就需要从Thread中获取可以加载子类的classloader。
    • 2.我们需要隔离类,因为我们可能不同的项目虽然使用同一个类,但是该类的版本不一样。比如tomcat自定义的classloader。
    • 3.为了达到模块化的热部署,如果为了替换代码则类加载器也需要替换,但是我们采用双亲加载机制就会导致需要替换很多类加载器,而有些类加载器无法替换比如bootstrap等。

    线程上下文

    • 1.将可以加载我们需要加载类的classloader,设置到线程上下文的classloader。
    • 2.当我们在使用当前类加载器无法加载的时候,我们就使用线程上下文classloader。

    tomcat类加载机制

    tomcat的启动加载原理(内置的tomcat的classloader采用的委托双亲加载机制)

    tomcat自定义四大classloader

    • commonLoader:双亲委派机制,该classloader集成了系统类加载,可以给tomcat本身和下面的所有项目。
    • catalinaLoader:双亲委派机制,只能给Tomcat本身使用
    • sharedLoader:双亲委派机制,只能给tomcat加载的所有项目使用。
    • webAppclassloader:每个项目使用一个新的去加载。对于JAR文件可以直接使用URLClassloader获取字节进行加载,对于class文件则是直接获取文件的流,生成字节进而得到class
      1.手下检测是否已经加载过该class(只检测当前classloader中一个map容器)加载完就返回
      2.检测当前classloader的命名空间(该命名空间包含父命名空间),如果有就返回(这确保了我们的核心类不会被我们自己新写的类给替代)
      3.使用javaseClassLoader(是扩展类加载器)去寻找我们要加载的类是否在javaseClassLoader中,如果在则javaseClassLoader去加载,这样就是进一步确保了一些核心类不会被web项目自己去重写并加载。
      4.然后根据加载的class名称去判断是否需要遵循双亲委派机制,默认是不需要的。
      5.如果是按照双亲委派机制则直接通过Class.forName将类交给parent也就是sharedLoader去加载。
      6.如果不是或者第五步骤错误则直接尝试自己加载。
      7.如果第6步骤还是失败,则在尝试一次第5步骤(因为失败有可能是webAppclassloader自己无法加载)
      8.如果第7步骤失败则抛出异常。
    命名空间及其作用
    • 1.每个类装载器有自己的命名空间,命名空间由所有以此装载器为创始类装载器的类组成。
    • 2.不同命名空间的两个类是不可见的,但只要得到类所对应的Class对象的reference,还是可以访问另一命名空间的类
    tomcat的加载流程
    • 1.首先启动的时候设置Commonloader,然后将catalianLoader和sharedLoader的父类设置为commonloader。
    • 2.这个时候通过catalinaloader初始化Catalina类,然后catalina启动tomcat。这就意味着tomcat的所有类都是依靠依靠catalinaLoader加载
    • 3.在加载context的时候创建webAppclassloader,依靠该classloader去加载我们的项目,同时webAppclassloader设置父classloader为sharedLoader。
    • 4.不同的项目采用不同的实例webAppclassloader,所以项目之间可以隔离。

    osgi的类加载机制

    OSGI的大体介绍

    • 1.每一个程序模块成为一个Bundle。
    • 2.每一个Bundle都有自己的类加载器。
    • 3.如果需要替换模块,则类加载器也需要替换。

    OSGI的加载机制

    • 1.将java.*开头的类交给父类加载器。
    • 2.否则将委派名单内的类,委派给父类加载器(应该是预留一些必须交给父类的class)
    • 3.否则将Import列表中的类,委派给Export这个类的Boundle类加载器加载(比如Abundle 需要Bbundle的类,那么委托Bbundle的类加载加载,我们只要获取Class既可以使用)
    • 4.否则查找当前bundle的classpath,使用自己的类加载器(加载该模块自己的class)
    • 5.否则查找类是否在自己的Fragment Bundle。在则委托给Fragment的classloader加载
    • 6.否则查找Dynamic Import列表的Bundle,委派给对应的Bundle的类加载器(Dynamic Import 这个列表是动态变化的每次当我们替换某个模块的时候对应的classloader变化了 就可以重新加载了)
    • 7.否则类查找失败。

    相关文章

      网友评论

          本文标题:虚拟机类加载机制

          本文链接:https://www.haomeiwen.com/subject/hnjtmqtx.html