JVM如何加载Java类

一. 基本类型

  在Java中类型可分为两大类:基本类型和引用类型,我们先来看看基本类型。

  Java中提供了八种预先定义好的基本类型,来支持数值计算:byte short int long char boolean float double

  使用基本类型主要是基于工程上的考虑,可以在执行效率和内存使用两方面提升软件性能。基本类型没有引用对象的对象头等信息,占用内存很少,且不需要逃逸分析可直接分配到栈上。

  基本类型在虚拟机中都是以数字存储的,包括 boolean 和 char。boolean 类型的 false 在虚拟机中为 0,boolean 类型的 true 在虚拟机中为 1,判断 boolean 时其实就是判断是否为 0 或者 是否为 1。

  基本类型都有自己的取值范围:

  在写Java代码时,如果我们超出了基本类型的取值范围,编译器会抛出异常,不过我们可以通过修改字节码为基本类型对象赋其他值。

 

二. 引用类型

  在Java中引用类型还可再细分为四种:类、接口、数组类和泛型参数。

  由于泛型参数会在编译期擦除,替换为指定的类型,所以JVM中只有前三种。在类、接口和数组类中,数组类是由 Java 虚拟机直接生成的,其他两种则有对应的字节流。

  字节流可以是从任何形式获取的,最常见的就是Java编译期生成的class文件,除此之外还有在程序内自动生成的,或者在网络(例如网页中内嵌的小程序 Java applet)甚至是数据库中获取。这些不同形式的字节流,都会被加载到 Java 虚拟机中,成为类或接口。为了叙述方便,下面我就用“类”来统称它们。

  无论是直接生成的数组类,还是加载的类,Java 虚拟机都需要对其进行链接和初始化。接下来,我会详细给你介绍一下每个步骤具体都在干些什么。

类生命周期的7个阶段

  1. loading 加载

  2. vertifaction 验证

  3. preparation 准备

  4. resolution 解析

  5. initialization 初始化

  6. using 使用

  7. unloading 卸载

  其中 vertifaction、preparation、resolution 统称为 linking。

 

1. 加载

  加载,是指查找字节流,并且据此创建类的过程。在此间一共做了三件事:

    1. 通过一个类的全限定名来获取定义此类的二进制字节流。(ClassLoader#defineClass)

    2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口。  

  类加载时机:

    值得一提的是,虚拟机规范中并未约束类何时加载,由各自的虚拟机实现自行决定。

  类加载器:

    Java虚拟机需要类加载器(ClassLoader)来完成字节流的查找,虚拟机内置了三种类加载器:启动类加载器(BootstrapClasLoader)、扩展类加载器(ExtendClassLoader)、应用类加载器(AppClassLoader)

    1. 启动类加载器

      启动类加载器是由c++实现的,没有对应的Java对象,在Java中用null代替,负责加载最为基础、最为重要的类,比如存放在 JRE 的 lib 目录下 jar 包中的类(以及由虚拟机参数 -Xbootclasspath 指定的类)。

    2. 扩展类加载器

      除了启动类加载器外,其他的类加载器都是ClassLoader的子类。扩展类加载器的父类加载器是启动类加载器(通过组合形式实现)。它负责加载相对次要、但又通用的类,比如存放在 JRE 的 lib/ext 目录下 jar 包中的类(以及由系统变量 java.ext.dirs 指定的类)。

    3. 应用类加载器

      应用类加载器的父类加载器则是扩展类加载器。它负责加载应用程序路径下的类。(这里的应用程序路径,便是指虚拟机参数 -cp/-classpath、系统变量 java.class.path 或环境变量 CLASSPATH 所指定的路径。)默认情况下,应用程序中包含的类便是由应用类加载器加载的(如通过Class.forName("name")加载的类)。

  双亲委派:

    既然每个类加载器都有自己的加载范围,那么它们的加载顺序就至关重要。想象这样一个情景,有人在开源项目中写了一个Object类,如果你用了这个开源项目,那么他写的Object类会替换掉JRE的Object类吗?如果能替换的话,其后果不堪设想。

    JVM为了保护自己的核心类库不被人为的篡改,提供了双亲委派机制。双亲委派的源码在ClassLoader#loadClass方法中。具体的逻辑是,首先由AppClassLoader从缓存中查找是否已经被加载过。如果缓存中没有就由其父加载器也就是ExtentionClassLoader查询缓存。如果ExtentionClassLoader的缓存中也没有,就交给BootstrapClassLoader查询缓存,如果缓存还没有,BootstrapClassLoader会尝试在自己的范围内加载这个类,如果加载成功直接返回,如果加载失败再交给子类加载器去它的加载范围内加载。

2. 验证

  验证的目的是检验字节流是否符合虚拟机的约束条件,一般来说编译生成的字节码都能符合要求,字节码注入时再详解。

  验证又可分为几个阶段:文件格式验证、元数据验证、字节码验证、符号引用验证。

    1. 文件格式验证:验证字节流是否符合 class 文件格式

    2. 元数据验证:对字节码描述的信息进行验证

    3. 字节码验证:进一步验证 class 的方法体是否是合法的、符合逻辑的、安全的

    4. 引用符号验证:验证该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字 段等资源。

3. 准备

  为被加载类的静态字段分配内存,此时静态字段是零值。注意这里只是分配了静态字段的内存,实例字段是随着实例对象保存在堆中的。

4. 解析

  解析阶段是 JVM 将常量池内的符号引用替换为直接引用的过程。

5. 初始化(重要)

  初始化阶段的作用是为静态字段赋值。

  在 Java 代码中,如果要初始化一个静态字段,我们可以在声明时直接赋值,也可以在静态代码块中对其赋值。

  如果直接赋值的静态字段被 final 所修饰,并且它的类型是基本类型或字符串时,那么该字段便会被 Java 编译器标记成常量值(ConstantValue),其初始化直接由 Java 虚拟机完成。除此之外的直接赋值操作,以及所有静态代码块中的代码,则会被 Java 编译器置于同一方法中,并把它命名为 < clinit >。

  类加载的最后一步是初始化,便是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。Java 虚拟机会通过加锁来确保类的 < clinit > 方法仅被执行一次。

 

  类初始化时机

    1. 当虚拟机启动时,初始化用户指定的主类。

    2. 当遇到用以新建目标类实例的 new 指令时,初始化 new 指令的目标类;

    3. 当遇到调用静态方法的指令时,初始化该静态方法所在的类;

    4. 当遇到访问静态字段的指令时,初始化该静态字段所在的类;

    5. 子类的初始化会触发父类的初始化;

    6. 如果一个接口定义了 default 方法,那么直接实现或者间接实现该接口的类的初始化,会触发该接口的初始化;

    7. 使用反射 API 对某个类进行反射调用时,初始化这个类;

    8. 当初次调用 MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。

6. 使用

  new xxx(); 等使用此类

7. 卸载

  类的卸载可以清理方法区内存,但是类卸载条件非常苛刻

posted @ 2020-09-27 11:29  Super-Yan  阅读(204)  评论(0编辑  收藏  举报