深入理解Java虚拟机笔记0x02

虚拟机类加载机制

类加载时机

类的生命周期

  • 包括:加载、验证、准备、解析、初始化、使用、卸载。其中,验证、准备、解析统称为链接。

类初始化条件(当且仅当)

  1. 遇到new、getstatic、putstatic、invokestatic字节码指令时,如果类没有初始化则触发其初始化。使用场景:
    • 使用new实例化对象
    • 读取或设置一个类的静态字段(不含被final修饰、已在编译期把结果放入常量池的静态字段)
    • 调用一个类的静态方法
  2. 对类进行反射调用的时候,如果类没有初始化则触发其初始化。
  3. 初始化一个类的时候,如果发现其父类没有初始化,则触发其父类的初始化。
  4. 虚拟机启动时会先初始化用户指定的主类(包含main()方法)。
  5. 使用动态语言支持时*。

类加载过程

加载

主要步骤

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

加载方式

  • 对于非数组类的加载,可以使用系统提供的引导类加载器来完成,也可以由用户自定义的类加载器完成(重写loadClass())
  • 对于数组类的加载,主要遵循以下规则:
    • 如果数组的组件类型(去掉一个维度)是引用类型,那就递归进行组件类型的加载,数组类将在加载该组件类型的类加载器的类名称空间上被标识。
    • 如果数组的组件类型不是引用类型,虚拟机将会把数组类标记为与引导类加载器关联。
    • 数组类的可见性与它的组件类型一致,如果组件类型不是引用类型,那数组的可见性将默认为public。

验证

  • 主要为了确保Class文件的字节流中包含的信息符合当前虚拟机要求,并且不会危害虚拟机自身的安全。大致包括4个方面:文件格式验证、元数据验证、字节码验证、符号引用验证。

文件格式验证

  • 文件是否以魔数开头
  • 主、次版本号是否在当前虚拟机的处理范围内
  • 常量池中的常量是否有不支持的常量类型

元数据验证

  • 是否有父类(除java.lang.Object以外,所有类都应当有父类)
  • 父类是否继承类不允许被继承的类(被final修饰的类)
  • 非抽象类是否实现了其父类以及接口中所有要求实现的方法
  • 类中的字段、方法是否与父类产生矛盾(覆盖了父类中的final字段,重载方法参数一致但返回类型不同等不合规则的重载)

字节码验证

  • 确保操作数栈的数据类型与指令代码操作匹配
  • 确保跳转指令不会跳转到方法体以外的字节码指令上
  • 确保方法体的类型转换是有效的(父类转换为子类、或者不相干的两种类型的数据互相转换是危险的)

符号引用验证

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

准备

  • 为类变量分配内存并设置初始值,这些变量所使用的内存将在方法区中分配。
    • 分配内存的变量仅包括被static修饰的变量,不包括实例变量。
    • 初始值为数据类型的零值,而如果变量的字段属表中存在ConstantValue属性(被final修饰)则初始值为定义的值。

解析

  • 将常量池内的符号引用替换为直接引用的过程
  • 符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位带目标即可。
  • 直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。

初始化

  • 根据用户的程序制定的计划来初始化类变量和其他资源

类加载器

  • 类加载器是在虚拟机外实现的、通过类的全限定名来获取描述此类的二进制字节流的模块,可以让应用程序自行决定如何去获取所需要的类。
  • 对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。即使两个类来源于同一个Class文件、同时处于同一个虚拟机中,它们的类加载器不同,那么它们就不属于同一个类。

类加载器分类

对虚拟机而言

  • 启动类加载器,是虚拟机的一部分
  • 其他类加载器,独立于虚拟机外部

对开发人员而言

  • 启动类加载器,加载<JAVA_HOME>/lib目录下,或者是虚拟机配置中指定路径的,并且被虚拟机识别的(按文件名识别)的类库。
  • 扩展类加载器,加载<JAVA_HOME>/lib/ext目录下,或被java.ext.dirs系统变量指定路径下的所有类库。
  • 应用程序类加载器,加载用户类路径下所指定的类库,如果应用程序没有指定自己的类加载器,将默认使用该类加载器。
  • 自定义类加载器。

双亲委派模型

类加载器的双亲委派

  • 如上图所示,这种类加载器的层次关系即类加载器的双亲委派模型。
  • 工作过程:如果一个类加载器收到了类加载请求,它不会先去尝试加载该类,而是把该请求委派给父类加载器去加载,每一层都是如此,即所有类加载请求都会到顶层的启动类加载器中。只有当父加载器反馈在其搜索范围内没有搜索到所需的类,无法完成加载请求时,子加载器才会尝试自己去加载。
  • 优点:使用双亲委派模型后,Java类随着它的类加载器一起具备了一种带有优先级的层级关系。例如类java.lang.Object,无论是哪一个类加载器需要加载这个类,最终都是交给启动类加载器来加载,保证了Object类在不同的类加载器环境中都是同一个类。

程序编译与代码优化

泛型

  • 真实泛型:存在于任意阶段的代码,在系统运行期间生成,有自己的虚方法和类型数据。这种现象称为类型膨胀,基于这种方法实现的泛型称为真实泛型,例如C#。
  • 伪泛型:泛型仅存在于源码,编译后仅剩原生类型(又称裸类型),并加入强制类型转换。这种方法称为类型擦除,基于这种方法实现的泛型称为伪泛型。Java泛型是伪泛型。

即时编译

  • 在程序运行过程中,“热点代码”会被即时编译为机器码以提高运行速度,包括被多次调用的方法和被多次调用的循环体。
  • 热点探测:用于判断一段代码是否为热点代码,是否需要触发即时编译,主要有两种技术:
    • 基于采样的热点探测:周期性检查各个线程的栈顶,如果发现某个或者某些方法经常在栈顶出现,则认为这个或这些方法为热点方法。优点是实现简单,高效,容易获得方法调用关系(展开调用堆栈),缺点是难以精确的判定方法的热度,易受线程阻塞等因素影响。
    • 基于计数器的热点探测:设置计数器,统计方法调用次数;设置阈值,如果调用次数超过该阈值则认为其是热点方法。
Author: SinLapis
Link: http://sinlapis.github.io/2019/06/23/深入理解Java虚拟机笔记0x02/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.