《深入理解Java虚拟机》读书笔记 (第七章 虚拟机类加载机制)

7.2 类加载的时机

  • 虚拟机把描述类的数据从Class类文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。
  • 类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

img

  • 加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)

7.3 类加载的过程

7.3.1 加载

  • 在加载阶段,虚拟机需要完成以下三件事情:
    • 通过一个类的全限定名来获取定义此类的二进制字节流。
    • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
    • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为方法区这些数据的访问入口。
  • “通过一个类的全限定名来获取定义此类的二进制字节流。”这个步骤并没有致命要从哪里获取,于是可以有很多技术
    • 从ZIP包中读取:JAR格式的基础
    • 运行时计算生成:动态代理技术
    • 由其他文件审查:JSP应用

7.3.2 验证

  • 验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 验证阶段大致会完成4个阶段的检验动作:
    1. 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以魔术0xCAFEBABE开头(当class文件以二进制形式打开,会看到这个文件头,cafebabe)、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
    2. 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
    3. 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
    4. 符号引用验证:确保解析动作能正确执行。
  • 验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响,如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

7.3.3 准备

  • 准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
  • 这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在堆中。
  • 其次,这里所说的初始值通常情况下是数据类型的零值

7.3.4 解析

  • 解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

7.3.5 初始化

  • 类初始化阶段是类加载的最后一步,这一步是开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,变量已经付过一次系统要求的初始值,而在初始化阶段,则根据程序猿通过程序制定的计划去初始化类变量和其他资源,或者说:初始化阶段是执行类构造器<clinit>()方法的过程.
  • 两个方法:<clinit><init> :
    • 在编译生成class文件时,会自动产生两个方法,一个是类的初始化方法, 另一个是实例的初始化方法
    • <clinit>:在jvm第一次加载class文件时调用,包括静态变量初始化语句和静态块的执行
    • <init>: 在实例创建出来的时候调用,包括调用new操作符;调用 Class 或 Java.lang.reflect.Constructor 对象的newInstance()方法;调用任何现有对象的clone()方法;通过 java.io.ObjectInputStream 类的getObject() 方法反序列化。

7.4. 类加载器

  • “通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让程序自己决定如何获取所需的类。实现这个动作的代码模块称为“类加载器”。

  • 类加载器在类层次划分、OSGi、热部署、代码加密等领域大放异彩,成为了java体系中的一块重要的基石。

7.4.1 类与类加载器

  • 对于任意一个类,都需要由它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间,

  • 比较两个类是否“相等”,是由同一个类加载器加载的前提下才有意义,否则即使两个类是同一个class文件,被同一个虚拟机加载,只要加载他们的类加载器不一样,那这两个类就必定不相等。

  • 这里的相等,包括代表类的Class对象的equalsisAssignableFromisInstance方法返回的结果。也包括instanceof做的所属关系判定情况。

7.4.2 双亲委派模型

  • 从JVM的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言(HotSpot)实现,是虚拟机自身的一部分。另外一种就是其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且都继承自抽象java.lang.ClassLoader

  • 从Java开发人员的角度来看,绝大部分Java程序都会使用以下3种系统提供的类加载器。

    1. 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中,并且是虚拟机识别的(名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用,用户编写自定义类加载器时,需要把加载请求委派给引导类加载器,那就直接使用null代替即可。
    2. 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,他负责加载**<JAVA_HOME>\lib\ext目录中的类库**,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
    3. 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称他为系统类加载器。他负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义自己的类加载器,一般情况下这个就是程序中的默认类加载器
  • 双亲委派模型(Parents Delegation Model):双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。

  • 双亲委派模型的工作过程是:

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

    类加载 - 双亲委派模型

  • 使用双亲委派模型来组织类加载器之间的关系,好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。

    如类java.lang.Object,它存放在rt.jar之中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写一个称谓java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

    双亲委派的实现代码:

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        {
            synchronized (getClassLoadingLock(name)) {
                // 首先,检查类是否已经加载
                Class<?> c = findLoadedClass(name);
           
                if (c == null) {
                    long t0 = System.nanoTime();
                    try {
                        if (parent != null) {
                        	//如果还未被加载,则调用父加载器的loadClass方法
                            c = parent.loadClass(name, false);
                        } else {
                        	//如果父加载器为空则默认使用启动类加载器
                            c = findBootstrapClassOrNull(name);
                        }
                    } catch (ClassNotFoundException e) {
                        // 如果没有从非空父类加载器中找到类,
                        // 则抛出ClassNotFoundException
                    }
    
                    if (c == null) {
                        // 在父加载器无法加载的时候,再调用自身的findClass方法加载
                        c = findClass(name);
                    }
                }
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
        }
    
    

7.4.3 破坏双亲委派模型

  • 上文提到双亲委派模型并不是一个强制性的约束模型,而是Java设计者们推荐给开发者们的类加载器实现方式。在Java世界里面大部分的类加载器都遵循这个模型,但也有例外的情况,到现在为止,双亲委派模型主要出现过三次较大规模的“被破坏”情况。

  • 双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即 JDK 1.2 发布之前。JDK 1.2 之后已不提倡用户再去覆盖loadClass() 方法,而应当把自己的类加载逻辑写到 findClass() 方法中,在 loadClass()方法的逻辑里如果父类加载失败,则会调用自己的 findClass() 方法来完成加载,这样就可以保证新写出来的类加载器是符合双亲委派规则的。

  • 双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的。如果基础类又要调用回用户的代码,那该怎么办?如JNDI服务。为了解决这个问题, Java 设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。

  • 双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。OSGi 实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值