# Java Class Load

# 什么是类加载?

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

# 类加载时机

  • 生命周期

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

虚拟机规范规定有且只有5种情况必须立即对类进行初始化。

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要触发其初始化。
    生成这4条指令的最常见的Java代码场景是,使用new关键字实例化对象的时候、读取或设置一个类的静态字段,
    (被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
  4. 当虚拟机启动时候,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
  5. 当使用JDK1.7的动态语言支持时
    如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄
    并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
  • 被动引用

通过子类引用父类的静态字段,不会导致子类初始化。
通过数组定义来引用类,不会触发此类的初始化。
常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
接口的初始化,接口在初始化时,并不要求其父接口全部完成类初始化,只有在正整使用到父接口的时候(如引用接口中定义的常量)才会初始化。

# 加载的过程

  • 加载

通过一个类的全限定名类获取定义此类的二进制字节流 将这字节流所代表的静态存储结构转化为方法区运行时数据结构 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

    • 怎么获取二进制字节流?

从ZIP包中读取,这很常见,最终成为日后JAR、EAR、WAR格式的基础 从网络中获取,这种场景最典型的应用就是Applet 运行时计算生成,这种常见使用得最多的就是动态代理技术 由其他文件生成,典型场景就是JSP应用,从数据库中读取,这种场景相对少一些(中间件服务器)

    • 数组类的创建

数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。
规则:
如果数组的组件类型(指的是数组去掉一个维度的类型)是引用类型, 那就递归采用上面的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识。
如果数组的组件类型不是引用类型(列如int[]组数),Java虚拟机将会把数组C标识为与引导类加载器关联。
数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为public。

  • 验证

    • 文件格式验证 第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理 基于二进制字节流进行的,只有通过类这个阶段的验证后,字节流才会进入内存的方法区进行存储 所以后面的3个验证阶段全部是基于方法区的存储结构进行的,不会再直接操作字节流
    • 元数据验证

主要目的 对类元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息

这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类) 这个类的父类是否继承了不允许被继承的类(被final修饰的类) 如果这个类不是抽象类,是否实现类其父类或接口之中要求实现的所有方法 类中的字段、方法是否与父类产生矛盾,
(列如覆盖类父类的final字段,或者出现不符合规则的方法重载,列如方法参数都一致,但返回值类型却不同等)。

    • 字节码验证

主要目的 通过数据流和控制流分析,确定程序语言是合法的、符合逻辑的

对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。

    • 符号引用验证

发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。

对于虚拟机的类加载机制来说,验证阶段是非常重要的,但是不一定必要(因为对程序运行期没有影响)的阶段。

  • 准备
    正式为类变量分配内存并设置类变量初始值的阶段

  • 解析
    虚拟机将常量池内符号引用替换为直接引用的过

  • 初始化

前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。
到了初始化阶段,才正真开始执行类中定义的Java程序代码(或者说是字节码)。

# 类加载器

只存在两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader)

使用C++实现,是虚拟机自身的一部分。

  • 所有其他的类加载器

使用JAVA实现,独立于JVM,并且全部继承自抽象类java.lang.ClassLoader。

  • 启动类加载器(Bootstrap ClassLoader) 负责将存放在<JAVA+HOME>\lib目录中的,或者被-Xbootclasspath参数所制定的路径中的,
    并且是JVM识别的(仅按照文件名识别,如rt.jar,如果名字不符合,即使放在lib目录中也不会被加载),
    加载到虚拟机内存中,启动类加载器无法被JAVA程序直接引用。

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

  • 应用程序类加载器(Application ClassLoader)

由sun.misc.Launcher$AppClassLoader来实现。 由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般称它为系统类加载器。
负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,
如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

  • 双亲委派模型

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

    • 工作过程

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

    • 这样做的好处就是

Java类随着它的类加载器一起具备了一种带有优先级的层次关系。

例如类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,
因此Object类在程序的各种类加载器环境中都是同一个类。

相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,
如果用户自己编写了一个称为java.lang.object的类,并放在程序的ClassPath中,
那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

就是保证某个范围的类一定是被某个类加载器所加载的,这就保证在程序中同 一个类不会被不同的类加载器加载。
这样做的一个主要的考量,就是从安全层 面上,杜绝通过使用和JRE相同的类名冒充现有JRE的类达到替换的攻击方式。