Java工程师的进阶之路 JVM篇(三)

756 阅读10分钟

白菜Java自习室 涵盖核心知识

Java工程师的进阶之路 JVM篇(一)
Java工程师的进阶之路 JVM篇(二)
Java工程师的进阶之路 JVM篇(三)

1. 类加载机制

类加载器把 class 文件中的二进制数据读入到内存中,存放在方法区,然后在堆区创建一个 java.lang.Class 对象,用来封装类在方法区内的数据结构。

1.1. 类的生命周期(七个阶段)

其中加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的。解析阶段可以在初始化之后再开始(运行时绑定或动态绑定或晚期绑定)。

以下五种情况必须对类进行初始化(而加载、验证、准备自然需要在此之前完成):

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

1.2. 类的加载过程

类加载过程:加载->连接->初始化。连接过程又可分为三步:验证->准备->解析。

1.2.1. 加载

加载:查找并加载类的二进制数据(把class文件里面的信息加载到内存里面)。

  1. 通过一个类的全限定名来获取定义次类的二进制流(ZIP 包、网络、运算生成、JSP 生成、数据库读取)。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法去这个类的各种数据的访问入口。

数组类的特殊性:数组类本身不通过类加载器创建,它是由 Java 虚拟机直接创建的。但数组类与类加载器仍然有很密切的关系,因为数组类的元素类型最终是要靠类加载器去创建的,数组创建过程如下:

  1. 如果数组的组件类型是引用类型,那就递归采用类加载加载。
  2. 如果数组的组件类型不是引用类型,Java 虚拟机会把数组标记为引导类加载器关联。
  3. 数组类的可见性与他的组件类型的可见性一致,如果组件类型不是引用类型,那数组类的可见性将默认为 public。

内存中实例的 java.lang.Class 对象存在方法区中。作为程序访问方法区中这些类型数据的外部接口。 加载阶段与连接阶段的部分内容是交叉进行的,但是开始时间保持先后顺序。

1.2.2. 连接

连接:把内存中类的二进制数据合并到虚拟机的运行时环境中。

(1)验证:是连接的第一步,确保 Class 文件的字节流中包含的信息符合当前虚拟机要求。

文件格式验证

  1. 是否以魔数 0xCAFEBABE 开头
  2. 主、次版本号是否在当前虚拟机处理范围之内
  3. 常量池的常量是否有不被支持常量的类型(检查常量 tag 标志)
  4. 指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量
  5. CONSTANT_Utf8_info 型的常量中是否有不符合 UTF8 编码的数据
  6. Class 文件中各个部分集文件本身是否有被删除的附加的其他信息

只有通过这个阶段的验证后,字节流才会进入内存的方法区进行存储,所以后面 3 个验证阶段全部是基于方法区的存储结构进行的,不再直接操作字节流。

元数据验证

  1. 这个类是否有父类(除 java.lang.Object 之外)
  2. 这个类的父类是否继承了不允许被继承的类(final 修饰的类)
  3. 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法
  4. 类中的字段、方法是否与父类产生矛盾(覆盖父类 final 字段、出现不符合规范的重载)

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

字节码验证

  1. 保证任意时刻操作数栈的数据类型与指令代码序列都鞥配合工作(不会出现按照 long 类型读一个 int 型数据)
  2. 保证跳转指令不会跳转到方法体以外的字节码指令上
  3. 保证方法体中的类型转换是有效的(子类对象赋值给父类数据类型是安全的,反过来不合法的)

这是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。这个阶段对类的方法体进行校验分析,保证校验类的方法在运行时不会做出危害虚拟机安全的事件。

符号引用验证

  1. 符号引用中通过字符创描述的全限定名是否能找到对应的类
  2. 在指定类中是否存在符方法的字段描述符以及简单名称所描述的方法和字段
  3. 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问

最后一个阶段的校验发生在迅疾将符号引用转化为直接引用的时候,这个转化动作将在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,还有以上提及的内容。 符号引用的目的是确保解析动作能正常执行,如果无法通过符号引用验证将抛出一个 java.lang.IncompatibleClass.ChangeError 异常的子类。如 java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError 等。

(2)准备:这个阶段正式为类分配内存并设置类变量初始值,内存在方法去中分配(含 static 修饰的变量不含实例变量)。

public static int value = 1127;

这句代码在初始值设置之后为 0,因为这时候尚未开始执行任何 Java 方法。而把 value 赋值为 1127 的 putstatic 指令是程序被编译后,存放于 clinit() 方法中,所以初始化阶段才会对 value 进行赋值。

基本数据类型的零值:

数据类型零值数据类型零值数据类型零值
int0booleanfalselong0L
float0.0fshort(short) 0double0.0d
char'\u0000'referencenullbyte(byte) 0

特殊情况:如果类字段的字段属性表中存在 ConstantValue 属性,在准备阶段虚拟机就会根据 ConstantValue 的设置将 value 赋值为 1127。

(3)解析:这个阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。

  1. 符号引用:
    符号引用以一组符号来描述所引用的目标,符号可以使任何形式的字面量。
  2. 直接引用:
    直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和迅疾的内存布局实现有关 解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类符号引用进行,分别对应于常量池的 7 中常量类型。

1.2.3. 初始化

为类的静态变量赋予正确的初始值。当静态变量的等号右边的值是一个常量表达式时,不会调用 static 代码块进行初始化。只有等号右边的值是一个运行时运算出来的值,才会调用 static 初始化。

1.2.4. 类的卸载

  1. 有 JVM 自带的三种类加载器(根、扩展、系统)加载的类始终不会卸载。因为 JVM 始终引用这些类加载器,这些类加载器使用引用他们所加载的类,因此这些 Class 类对象始终是可到达的。
  2. 由用户自定义类加载器加载的类,是可以被卸载的。

1.3. 类的加载器

从 Java 虚拟机角度讲,只存在两种类加载器:一种是启动类加载器(C++ 实现,是虚拟机的一部分);另一种是其他所有类的加载器(Java 实现,独立于虚拟机外部且全继承自 java.lang.ClassLoader)。

1.3.1. 双亲委派模型

当一个类加载器收到类加载请求的时候,它首先不会自己去加载这个类的信息,而是把该 请求转发给父类加载器,依次向上。所以所有的类加载请求都会被传递到父类加载器中,只有当父类加载器中无法加载到所需的类,子类加载器才会自己尝试去加载该类。当当前类加载器和所有父类加载器都无法加载该类时,抛出 ClassNotFindException 异常。

  1. 启动类加载器: 加载 lib 下或被 -Xbootclasspath 路径下的类。

  2. 扩展类加载器: 加载 lib/ext 或者被 java.ext.dirs 系统变量所指定的路径下的类。

  3. 引用程序类加载器: ClassLoader负责,加载用户路径上所指定的类库。

双亲委派模型的好处:

提高系统的安全性。用户自定义的类加载器不可能加载应该由父加载器加载的可靠类。(比如用户定义了一个恶意代码,自定义的类加载器首先让系统加载器去加载,系统加载器检查该代码不符合规范,于是就不继续加载了)。

双亲委派模型的特点:

  1. 全盘负责:当一个类加载器加载一个类时,该类所依赖的其他类也会被这个类加载器加载到内存中。
  2. 缓存机制:所有的 Class 对象都会被缓存,当程序需要使用某个 Class 时,类加载器先从缓存中查找,找不到,才从 class 文件中读取数据,转化成 Class 对象,存入缓存中。

1.3.2. 用户自定义的类加载器

  1. 继承 ClassLoader
  2. 重写 findClass 方法。从特定位置加载 class 文件,得到字节数组,然后利用 defineClass 把字节数组转化为 Class 对象

为什么要自定义类加载器

  1. 可以从指定位置加载 class 文件,比如说从数据库、云端加载 class 文件
  2. 加密:Java 代码可以被轻易的反编译,因此,如果需要对代码进行加密,那么加密以后的代码,就不能使用 Java 自带的 ClassLoader 来加载这个类了,需要自定义 ClassLoader,对这个类进行解密,然后加载。

1.3.3. 类加载机制与接口

  1. 当Java虚拟机初始化一个类时,不会初始化该类实现的接口。
  2. 在初始化一个接口时,不会初始化这个接口父接口。
  3. 只有当程序首次使用该接口的静态变量时,才导致该接口的初始化。

Java工程师的进阶之路 JVM篇(一)
Java工程师的进阶之路 JVM篇(二)
Java工程师的进阶之路 JVM篇(三)