赞
踩
虚拟机 把 描述类的数据 从Class文件 加载到内存,并对数据进行 校验、转换解析、初始化,最终形成 可以被虚拟机直接使用的Java类型,这就是虚拟机的 类加载机制
类型的加载、连接、初始化3个过程 都是在 程序运行期间完成的
JVM的类加载分为 5个阶段:加载、连接(验证、准备、解析)、卸载、使用、初始化
接口在初始化时,并不要求其父接口全部都完成了初始化,只有在真正用到父接口的时候(例如引用父接口定义的常量)才会初始化
指的是JVM读取Class文件,并且根据Class文件描述 创建 java.lang.Class对象的过程
类加载过程 主要包含 将Class文件 读取到 运行时区域的方法区内,在堆中创建java.lang.Class对象,并 封装(动词) 类在方法区的数据结构 的过程
类加载的最终产物就是堆内存中的class对象,对同一个ClassLoader来讲,不管一个类被加载了多少次,对应到堆内存中的Class对象始终是同一个。虚拟机规范中指出了类的加载是通过一个全限定名(包名+类名)来获取二进制数据流,但是并没有限定必须通过某种方式去获得。在读取Class文件时既可以通过文件的形式读取,也可以通过jar包、war包读取,还可以通过代理自动生成Class或其他方式读取
类加载过程如下:
主要用于确保Class文件 符合 当前虚拟机的要求,保障虚拟机自身的安全,只有通过验证的Class文件才能被JVM加载
当字节流的信息不符合要求时,则会抛出VerifyError这样的异常或者是其子异常
符号引用的验证 目的是 为了保证 解析动作 的顺利执行,如果某个类的字段不存在,则会抛出NoSuchFieldError,若方法不存在时则抛出NoSuchMethodError等,在使用反射的时候会遇到这样的异常信息
在类接口的解析完成之后,还需要进行符号引用的验证
所谓字段的解析,就是解析 所访问的类或者接口中的字段,在解析类或者变量的时候,如果该字段不存在,或者出现错误,就会抛出异常,不再进行下面的解析
类方法和接口方法有所不同,类方法可以直接使用该类进行调用,而接口方法必须要有相应的实现类继承才能够进行调用
类加载过程的最后一步,执行 类 的 构造器方法<clinit>
的过程,clinit是class initialize前面几个字母的简写
<clinit>()
不是自己定义的,是javac编译器 自动收集 类中的所有类变量(指的是静态成员变量)的赋值动作 和 静态代码块中的语句 合并而来。(也就是 将 类变量的初始化 以及 静态代码块中的语句集合在<clinit>()
方法中)。如果不存在 static成员变量 以及 静态代码块,就不会生成<clinit>()
方法<clinit>()
方法中 所有的 类变量 都会被赋予正确的值,也就是在程序编写的时候指定的值<clinit>()
方法是在编译阶段生成的,也就是说它已经包含在了class文件中了<clinit>()
初始化的顺序是根据语句在源文件中出现的顺序决定的<clinit>()
不同于类的构造器,类的构造器 是 虚拟机视角下的<init>()
方法。(java类默认都存在默认的构造方法,所以在虚拟机视角下,对于任何一个java类都会生成一个<init>()
方法)<clinit()
>方法执行前先执行父类的<clinit>()
方法,因此父类的静态变量总是能够得到优先赋值<clinit>()
方法是被同步加锁的。(一个类只会被加载一次)<clinit>()
方法的必要了,接口中同样也是如此<clinit>()
方法JVM对类的初始化是一个延迟的机制,即使用的是lazy的方式,当一个类在首次使用的时候才会被初始化,在同一个运行时包下,一个Class只会被初始化一次
虚拟机严格规定了有且只有6中情况必须要对类进行初始化
JVM同时规范了以下6种主动使用类的场景,具体如下:(同上)
除了上述6种情况,其余的都称为被动使用,不会导致类的加载和初始化
在发生以下几种情况时,JVM不会执行类的初始化流程
ClassLoader-类加载器的主要职责 就是 负责加载各种class文件 到JVM中
ClassLoader是一个抽象的class,给定一个class的二进制文件名,ClassLoader会尝试加载 并且 在JVM中生成构成这个类的各个数据结构,然后使其分布在JVM对应的内存区域中
JVM支持两种类型的类加载器,分别为 引导类加载器(Bootstrap ClassLoader)和 自定义类加载器(User-Defined ClassLoader)
这里的自定义加载器 指的不是开发人员自己定义的类加载器,而是指的 所有 继承自ClassLoader的类加载器。包括扩展类加载器、应用程序类加载器、用户自定义类加载器(程序员自己写的)三种
阅读参考类加载器与类的加载过程概述(认真阅读)
负责加载Java_HOME/lib目录中的类库,或 通过-Xbootclasspath
参数 指定 路径中 被虚拟机认可的类库
System.getProperty("sun.boot.class.path")
来得知当前JVM的根加载器都加载了哪些资源负责加载Java_HOME/lib/ext目录中的类库,或通过java.ext.dirs系统变量加载指定路径中的类库
java.ext.dirs
获得负责加载 用户路径(classpath)上的类库
ClassLoader.getSystemClassLoader()
方法获取该类加载器java.class.path
进行获取所有的自定义类加载器都是ClassLoader的直接子类或者间接子类
继承ClassLoader抽象类,覆写findClass方法,java.lang.ClassLoader是一个抽象类,它里面并没有抽象方法,但是有findClass方法,务必实现该方法,否则将会抛出Class找不到的异常
调用ClassLoader#defineClass方法 将 二进制数据 转换成 Class对象
Thread.currentThread().getContextClassLoader()
获取 线程上下文 的 类加载器,简单来说 就是这个线程 是由哪个类加器 加载的Thread#setContextClassLoader(ClassLoader cl)
设置该线程的类加载器。这个方法 可以打破 JAVA类加载器的父委托机制,有时候该方法也被称为JAVA类加载器的后门问题:为什么setContextClassLoader可以打破双亲委派机制?
每一个 类加载器,都拥有一个 独立的 类命名空间。
比较 两个类 是否相等,只有在 这两个类 是由同一个类加载器 加载的前提下才有意义,然后才是完整类名必须一致,包括包名
JVM必须知道 一个类 是由 启动类加载器加载 还是由 用户类加载器加载(除了启动类加载器之外的加载器)
如果 一个类 是由 用户类加载器加载的,那么 JVM 会将 这个类加载器的一个引用 作为类信息的一部分 保存在 方法区中
当 解析一个类型 到 另一个类型的引用 的时候,JVM 需要保证 这两个类型 的 类加载器 是相同的
每一个类加载器实例都有各自的命名空间,命名空间 是由 该加载器及其所有父加载器所构成的,因此在每个类加载器中同一个class都是独一无二的
使用不同的类加载器,或者 同一个类加载器的不同实例,去加载同一个class,则会在堆内存和方法区产生多个class的对象
在JVM运行时 class会有一个 运行时包,运行时的包 是由 类加载器的命名空间 和 类的全限定名称 共同组成的
这样做的好处是出于安全和封装的考虑
JVM规定了 不同的运行时包下的类 彼此之间是不可以进行访问的,那为何我们可以访问java.lang.String,毕竟它是 由启动类加载器 加载的
如果某个类C 被 类加载器CL加载,那么CL 就被称为 C 的 初始类加载器
JVM为每一个类加载器维护了一个列表,该 列表中 记录了 将该类加载器 作为 初始类加载器 的 所有class
在加载一个类时,JVM使用这些列表来判断该类是否已经被加载过了,是否需要首次加载
根据JVM规范的规定,在类的加载过程中,所有参与的类加载器,即使没有亲自加载过该类,也都会被标识为该类的初始类加载器
关于JVM在运行期间到底加载了多少class,可以在启动JVM时指定-verbose:class
参数观察得到
JVM规定了一个Class只有在满足下面三个条件的时候才会被GC回收,也就是类被卸载
JVM通过双亲委派机制对类进行加载
双亲委派机制 指 一个类 在收到 类加载请求后 不会尝试自己加载这个类,而是把该类加载请求 向上委派给 其父类去完成,其父类在接收到该类加载请求后又会将其委派给自己的父类,以此类推,这样所有的类加载请求都被向上委派到启动类加载器中
若父类加载器 在接收到 类加载请求后 发现自己也无法加载该类(通常原因是该类的Class文件在父类的类加载路径中不存在),则 父类 会将该信息 反馈给子类 并向下委派 子类加载器 加载该类,直到该类被成功加载,若找不到该类,则JVM会抛出ClassNotFoud异常。
由于loadClass指定了resolve为false,所以不会进行 连接阶段 的执行,这也就解释了为什么通过类加载器加载类并不会导致类的初始化
双亲委派机制的核心是保障类的 唯一性 和 安全性(防止核心API被篡改)。例如在加载rt.jar包中的java.lang.Object类时,无论是哪个类加载器加载这个类,最终都将类加载请求委托给启动类加载器加载,这样就保证了类加载的唯一性。如果在JVM中存在包名和类名相同的两个类,则该类将无法被加载,JVM也无法完成类加载流程
阅读参考JVM双亲委派机制
JNDI是Java的标准服务,它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar)
但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码
但启动类加载器不可能“认识”之些代码,该怎么办?
为了解决这个困境,只好引入了一个不太优雅的设计:线程上下文件类加载器(Thread Context ClassLoader)
这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置
如果创建线程时 还未设置,它将会从父线程中继承一个
如果在应用程序的全局范围内都没有设置过,那么这个类加载器 默认就是 应用程序类加载器
有了线程上下文类加载器,JNDI服务 使用 这个线程上下文类加载器 去加载 所需要的SPI代码,也就是 父类加载器 请求 子类加载器 去完成类加载动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,已经违背了双亲委派模型
Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI,JDBC,JCE,JAXB和JBI等
Open Service Gateway Initiative
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。