类加载机制 读笔

类加载机制

虚拟机的类加载机制:虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
就Java语言,类型的加载,连接过程和初始化是在程序运行期间完成的;这种策略稍微增加了性能开销,但是提供了高度的灵活性。(Java动态扩展的语言特性依赖运行期动态加载和动态连接实现)

类加载的时机:
类的生命周期:开始保持着该固定顺序,这些阶段通常都是互相交叉地混合式进行,通常会在一个阶段的执行过程中调用,激活另一个阶段。

  • 加载 (loading)
  • 连接(linking):

    • 验证 (verification)
    • 准备 (preparation)
    • 解析 (resolution):某些情况下,可以在初始化阶段之后再开始;为了支持Java语言的动态绑定(继承)。
  • 初始化 (initialization)
  • 使用 (using)
  • 卸载 (unloading)
    这里写图片描述

有且只有种情况必须立即对类进行初始化:

  • 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时(使用new实例化对象的时候、读取或设置- 一个类的静态字段、调用一个类的静态方法)。
  • 使用java.lang.reflet包的方法对类进行反射调用的时候。
  • 当初始化一个类的时候,如果发现其父类没有进行过初始化,则需要先触发其父类的初始化。
  • 当虚拟机启动时,虚拟机会初始化主类(包含main方法的那个类)。
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果为REF_getStatic,REF_outStatic,REF_invokeStatic的方法句柄,并且这个句柄对应的类没有进行初始化,则先触发其初始化。

类加载的过程:

  • 加载:
    在加载阶段,虚拟机需要完成以下3件事:

    • 通过一个类的全限定名来换取此类的二进制字节流。
    • 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构。
    • 在内存中生成一直代表这个类的java.lang.Class对象(并未规定在GC堆,HotSpot将其存放在方法区),作为方法区这个类的各种数据的访问入口。
      加载阶段完成后,虚拟机外部的二进制文件流就按照虚拟机所需要的格式存放在方法区,方法区内的数据格式由虚拟机自定义。
      并没有指明二进制文件要从一个Class文件中获取,并没有要求从哪里获取,怎样获取,许多Java技术在此基础之上:

    • 从ZIP包获取,成为JAR,EAR,WAR格式基础。

    • 从网络中获取,即Applet。
    • 运行时计算生成,动态代理技术。
      ………
      加载阶段可以使用系统提供的引导类完成,也可以由用户自定义的类加载器完成,可以通过自定义加载器控制字节流的获取方式(即重写一个类加载器的loadClass()方法)。
      数组类本身不通过类加载器创建,由Java虚拟机直接创建;但是数组的元素类型是靠类加载器去创建。
  • 验证:
    目的:确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全(可能因为载入了有害的字节流而导致系统崩溃;验证阶段是否严谨决定能否承受恶意代码的攻击)。
    验证阶段是一个重要,但并不必须的阶段(反复使用和验证的代码可以关闭验证阶段)。

    验证阶段的4个阶段的校验动作:

    • 文件格式验证:校验字节流是否符合Class文件格式的规范,并且能够被当前版本的虚拟机处理。(可能的验证点:1.是否魔数开头 2.主次版本号是否在虚拟机处理范围内………)
      作用:保证输入的字节流能够被之前的解析并存储于方法区内,经过该阶段,字节流进入方法区存储。

    下面3个阶段都是基于方法区的存储结构进行,不在直接操作字节流:

    • 元数据验证:对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范的要求。(可能的验证点:1.是否有父类 2.是否继承了不允许被继承的类………)
      作用:对类的元数据进行语义校验,保证不存在不符合Java语言规范的元数据信息。

    • 字节码验证:通过对于数据流和控制流分析,确定语言是合法的,符合逻辑的。
      目的:对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事情。

    • 符号引用验证: 对类自身以外(常量池的各种符号引用)的信息进行匹配性验证。(验证:1.符号引用中通过字符串描述的全限定名是否找到对应的类…….)
      目的:确保解析动作(符号引用转化为直接引用)能够正常执行。

  • 准备:
    正式为类变量(static变量)分配内存(将在方法区内分配)并且设置类变量初始量(数据类型的零值)的阶段。
    类字段属性表中存在ConstantValue属性(只有同时被final和static修饰的字段才有ConstantValue属性,且限于基本类型和String。),则会在准备阶段就被初始化为指定的值。

  • 解析:
    将常量池内的符号引用替换为直接引用的过程。

    • 符号引用:一个出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息(必须足以唯一的识别一个类、字段、方法)的字符串。
    • 直接引用:对于指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的本地指针。
      指向实例变量、实例方法的直接引用都是偏移量
      虚拟机可以根据需要判断是在类被加载器加载时就进行解析(静态绑定),还是等到一个符号引用将被使用才解析(动态绑定)。
      对于不同的符号引用(类或接口,字段,类方法…..),具有不同的解析动作。
  • 初始化:
    开始真正执行类定义的Java程序代码(字节码),该阶段,根据程序员制定的计划去初始化类变量和其他资源(执行类构造器()方法的过程)
    < clinit>:在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行

类加载器

类加载器:实现通过一个类的全限定名来换取描述该类的的二进制字节流的代码模块。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机的唯一性,每一个类加载器都拥有一个独立的类名称空间。(同一个Class文件,只有加载的类加载器不同,则两个类必然不同)。

双亲委派原则:
以JVM的角度,只存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):使用C++语言实现,是虚拟机的一部分。
  • 其他所有的类加载器: 由Java语言实现,独立于虚拟机外部,并且都继承自抽象类java.lang.ClassLoader。

以开放人员角度,存在3种类加载器:

  • 启动类加载器:引导类装入器是用本地代码实现的类装入器,它负责将 /lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

  • 扩展类加载器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

  • 应用程序加载器(Application ClassLoader 系统类加载器):由sun.misc.Lanuncher$AppClassLoader 实现。负责加载用户路径(ClassPath)所指定的类库,开发者可以直接使用。(如果没有自定义,则该为程序默认类加载器)

双亲委派原则:
除顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器(父子关系通过组合关系实现,而不是继承)。
这里写图片描述
工作过程:当一个类加载器收到了类加载的请求,将把这个请求委派给父类,所有的加载请求最终都被传送到顶层的启动类加载中;只有当父类加载器反馈自己无法完成加载请求,子加载器才会尝试加载。
好处:形成了一种带有优先级的层次关系,保证由父类加载器优先,确保了安全性。
实现方式:
先检查是否已经被加载过,没有则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器为父加载器;如果父加载器加载失败,抛出ClassNotFoundException,则调用自己的findClass()加载。

**LoadClass()方法:**
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        // First, check if the class has already been loaded
        Class c = findLoadedClass(name);
        if (c == null) {
            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.
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }

破坏双亲委派模型:
双亲委派模型并不是强制性约束模型,存在例外的方式。

  • loadClass()方法:JDK1.2之前,用户通过继承java.lang.ClassLoader从而重写loadClass()方法,因为虚拟机在进行类加载时会调用加载器的私有方法loadClassInternal()来调用自己的loadClass()方法。

    现在采用将自己的加载逻辑写入findClass()中,在loadClass()方法调用父类失败,则会自动调用findClass(),则符合了双亲委派原则。

  • 线程上下文加载器(Thread Context ClassLoader):由于自身模型的缺陷——当基础类需要调用用户代码时,存在问题。 通过该类加载器去请求子类加载器去完成类加载的动作,打破父类优先的层次结构。

  • 网络结构: 由于用户对于程序动态性的追求(代码热替换,模块热部署…),需要破坏双亲模型。
    OSGI实现模块热部署通过对于它自定义的类加载器机制的实现;每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle,将Bundle连同类加载器一同替换。
    在OSGI环境下,类加载器不再是双亲委派模型中的树桩结构,进一步发展为更复制的网状结构,当收到加载请求,将采用自己的加载顺序。而在加载顺序中,只有部分符合双亲加载原则,其余的类查找都是在平级的类加载器中进行的,

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值