HotSpot JVM

Loading / Linking /Initializing

01-Loading | 查找并创建

加载指查找二进制表示并据此创建类或接口的过程。

注:这里查找的是 .class 文件的二进制表示(可以在文件系统、也可以通过网络等任何方式,具体由类加载器实现决定);创建的是 OOP/Klass 模型(即 Java 类在内存中的表示,实际上是 C++ 中的结构体)。

什么时候会触发类或接口的 Loading ?

  1. 被另一个类的运行时常量池引用。
  2. 使用反射等特殊的方法,显式地加载,例如 Class.forName 、classLoader.loadClass 、JNI_FindClass等。
  3. JVM 启动时,会加载一些特定的类,例如 Object 、Thread 类等。

注:这里说的类或接口指的是普通的类或接口,而不是数组类。前者有外部的二进制表示,通过类加载器加载到 JVM 的方法区;而后者没有外部的二进制表示,它是完全由 JVM 创建的,而不通过类加载器。

类加载器分为两种(从 JVM 角度):

  1. bootstrap
  2. user-defined,所有此类的类加载器都是ClassLoader的子类。注:但并非通过继承实现父子关系,而是通过组合。

谈到 JVM 中类加载器的分类,不得不谈一下所谓的双亲委派模型:

  • 在 HotSpot JVM 实现中,所有的类加载器之间是有层级关系的,即除 bootstrap 外,其他的类加载器都是有一个委派双亲(delegation parent)。(注:实现父子关系并非通过继承)
  • 当向某个类加载器请求加载一个类时,该类加载器会先委派其双亲帮其加载。若双亲不能加载,则自己尝试加载。若自己仍不能加载,则抛异常。
  • bootstrap 负责加载 $JAVA_HOME/lib 、-Xbootclasspath 路径下的类
  • user-defined 可进一步细分为:extension 类加载器和 system 类加载器
    • extension 负责加载 $JAVA_HOME/ext/lib 、java.ext.dir 路径下的类
    • system 负责加载 main 方法所在类、classpath 路径下的类(注:默认的应用类加载器)
1.  SPI 服务实现加载
2.  Tomcat WebAppClassLoader

找到类的二进制表示后,接下来就是要在内存中(Java 7 及之前是在 PermGen 区,Java 8 之后,是在 Metaspace 中)创建instanceKlass或arrayKlass。

HotSpot 中维护了三个哈希表,来追踪类加载过程,表由 SystemDictionary_lock 锁保护。

  1. SystemDictionary,记录已加载的类,<name, initiating loader> → klassOop 和 <name, defining loader> → klassOop
  2. PlaceholderTable,记录当前正在被加载的类,用来做ClassCircularityError检查和多线程parallel加载
  3. LoaderConstraintTable,记录类型安全性检查的约束

[1] VM Class Loading
[2] 类加载器]

02-Linking | 使能执行

Linking 将加载的类或接口与 JVM 的运行时状态相结合,使类或接口能执行。此外,链接也涉及类或接口中符号引用的解析。

链接包括几个步骤:

  1. 验证,主要确保载入的二进制表示结构上的正确性,可导致其他类或接口的加载。

    • 在验证阶段,JVM需要完成如下工作:
      • 文件格式验证。经过了这个阶段的验证,字节流才会加载到方法区(Metaspace)。
      • 元数据验证。对方法区(Metaspace)中的结构进行语义分析,确保符合Java语言规范。
      • 字节码验证。进行数据流、控制流分析,确保被校验的类的方法在执行时不会危害JVM安全。
      • 符号引用验证。发生在JVM将符号引用转化为直接引用的时候,发生在解析阶段。可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验的过程。
  2. 预处理,创建类或接口中的 static fields,并初始化为默认值(例如0),此阶段不会执行任何 JVM 指令。

    • 示例

      public static int value = 123; // 预处理阶段,value对应的内存空间值为0,而非123, // 值在初始化阶段才会被置为123 public static final int VALUE = 123; // 对于常量,预处理阶段,VALUE对应的值为123,而非默认值0 
  3. (symbolic reference)解析,为运行时常量池中的符号引用动态地确定具体值。

    • 符号引用可能指向几种类型:
      • 类或接口
      • a method
      • a method type
      • a method handle
      • 动态计算的常量
    • method 、method handle 和 method type 有什么区别?更具体参考[1]
      • method 是常规意义上理解的方法
      • method handle 是 java 7 引入的概念,它是对可执行的方法(method)的引用,或者说,它是一个有能力安全调用方法的对象。可以通过句柄直接调用句柄引用(或指向)的底层方法。
      • method type 是表示方法签名类型的不可变对象。每个方法句柄都有一个 method type 实例,用来指示方法的返回值类型和参数类型。

03-Initialization | 调用<clinit>方法

Initialization 指执行类或接口的初始化方法的过程。导致类或接口初始化的场景有如下几种:

  1. JVM 指令 new / getstatic / putstatic / invokestatic 引用了某个类或接口,导致该类初始化。
  2. The first invocation of a java.lang.invoke.MethodHandle instance。
  3. 调用类库中的反射方法,例如 Class 类中的某些方法或 java.lang.reflect 包中某些方法。
  4. 子类被初始化,导致父类的初始化。
  5. 若接口中声明了非抽象、非静态方法,则实现该接口的类初始化将导致接口的初始化。
  6. JVM 启动时,指定实例化的类或接口。
  • 初始化过程是如何的?
    1. 对于每个类或接口C来说,存在唯一的初始化锁LC。对C进行初始化,需要首先获得LC。
    2. 如果C正在被某些其他线程初始化,则释放LC,阻塞当前线程,直到收到C初始化完成的通知,从1.重新开始。
    3. 如果C正在被当前线程初始化,必定是递归请求,释放LC,退出初始化过程。
    4. 如果C已初始化完成,释放LC,正常退出初始化过程。
    5. 如果C处于错误状态,初始化不可能完成,释放LC,抛NoClassDefFoundError。
    6. 否则,记录C正在被当前线程初始化,释放LC。
    7. 接下来,如果C是类而非接口,那么它的超类、超接口(包含至少一个非抽象、非静态方法)如果存在仍未被初始化的情况,则递归地进行初始化。如果在初始化超类的过程中抛异常了,则获得LC,标记C为错误状态,并通知其他阻塞在LC上的线程,释放LC,异常退出C的初始化过程。
    8. 接下来,determine whether assertions are enabled for C by querying its defining class loader.
    9. 接下来,执行C的初始化方法。
    10. 如果步骤9正常结束,请求获取LC,标记C为初始化完成,通知所有阻塞在LC上的线程,释放LC,正常退出C的初始化过程。
    11. 否则,即步骤9异常退出,且抛出Error或其子类的异常E,则以E为参数创建ExceptionInInitializerError。
    12. 请求获取LC,标记C为异常状态,通知所有阻塞在LC上的线程,释放LC,抛出异常。
  • 初始化阶段才真正开始执行类或接口中定义的字节码,即执行类的 方法的过程:
    • 方法由 JVM 自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的。
    • 方法与类的构造器 方法不同,不需要显示地调用父类的 方法,而由 JVM 保证执行子类 方法时,父类的已执行完毕。所以,在 JVM 中执行的第一个 方法是 java.lang.Object 的 方法。
    • 接口也可以拥有 方法,但与类不同的是,执行接口的 方法不需要先执行其父类的 方法,只有父接口中定义的变量被使用时才会对父接口初始化。同样,接口的实现类在初始化时,不会执行接口的 方法
    • 不是必须的,若类或接口中无静态变量,则不会为该类或接口生成 方法
    • JVM 会保证一个类的 方法只会有一个线程在执行,其他试图初始化该类的线程都会被阻塞

04-总结

01-03 步骤之间有一些基本的准则需要遵循,例如:

  1. 链接之前,类或接口必须保证已完全载入。
  2. 初始化之前,类或接口必须保证已完成验证、预处理(即链接)。

05-Klass and OOP

HotSpot JVM 实现中用 Klass-OOP 模型来表示 Java 中的类和对象。


图 1. Klass 的继承体系


图 2. oopDesc 的继承体系

Klass 结构是 .class 文件的运行时结构; oopDesc 结构是 Object 对象的运行时结构。

oopDesc(对象头)中包含了两部分信息

  • mark word,存储了哈希码、锁信息、GC元数据等。
  • klass word,类信息。注:应该是指针,指向 Metaspace 中的类结构。
  • possible alignment paddings,非必须

对象头后存储的是对象的实例数据。



图 3. oopDesc 布局示意图

JVM 中的普通对象表示为 instanceOopDesc,数组对象表示为 arrayOopDesc。两者在结构上的区别是,后者多了 4 个字节的长度信息。

06-Java Object Layout

在 32/64 位机器上,mark word 是有区别的:

32 bits:
-------- hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)

64 bits:
--------
unused:25 hash:31 -->| unused_gap:1   age:4    biased_lock:1 lock:2 (normal object)
JavaThread*:54 epoch:2 unused_gap:1   age:4    biased_lock:1 lock:2 (biased object)

图 4. 32 bits mark word 可能的分配情况及其含义


图 5. 64 bits mark word 可能的分配情况及其含义

  • 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 升级过程(不可降级)?

    偏向锁获取:CAS 比较线程ID,同一线程再次获得锁的效率提高。

    偏向锁获取失败时,说明有其他线程加入抢锁的队伍,在到达 safepoint 后获得偏向锁的线程被挂起,判断锁对象是否处于锁定状态,并据此决定撤销偏向锁或升级为轻量级锁。

    轻量级锁(自旋锁):

借助工具查看 Java 对象内存布局?

  • HSDB [1]
  • jol [1,
    2]
    • 示例

      public class SimpleInt { private int state;
      }
      # Running 64-bit HotSpot VM. # Objects are 8 bytes aligned. # Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes] # ClassLayout.parseClass(SimpleInt.class).toPrintable() self.samson.example.jol.SimpleInt object internals: OFFSET  SIZE   TYPE DESCRIPTION                               VALUE 0 16 (object header)                           N/A 16 4 int SimpleInt.state                           N/A 20 4 (loss due to the next object alignment)
      Instance size: 24 bytes
      Space losses: 0 bytes internal + 4 bytes external = 4 bytes total # ClassLayout.parseInstance(instance).toPrintable() self.samson.example.jol.SimpleInt object internals: OFFSET  SIZE   TYPE DESCRIPTION                               VALUE 0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) (5) 4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 8 4 (object header) 88 13 1a 1d (10001000 00010011 00011010 00011101) (488248200) 12 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0) 16 4 int SimpleInt.state 0 20 4 (loss due to the next object alignment)
      Instance size: 24 bytes
      Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

03-对象创建过程

在所有会创建对象的场景中,该类的某个特定构造器方***被调用。

  1. 创建类的实例时,会在堆上开辟空间存放所有的 instance variables,包括其超类声明的实例变量(也包括被子类 hide 的变量)。
  2. 实例变量被初始化为默认值
  3. 在新创建对象的引用被返回之前,调用特定的 constructor(先执行 instance initializers 和 instance variable initializers)
class Super {
    Super() { printThree(); } void printThree() { System.out.println("three"); }
} class Test extends Super { int three = (int)Math.PI; // That is, 3 void printThree() { System.out.println(three); } public static void main(String[] args) {
        Test t = new Test();
        t.printThree();
    }
} // 输出 // 0 // 3 
  • 内存分配方式有哪些呢?

    若堆是规整的,即使用过得放在一边,空闲未用的放在一边,在两者之间存在一个指针,称为分界指示器。分配方式就是指示器向空闲部分移动一定的大小。这种方式称为指针碰撞

    若堆不是规整的,空闲内存块列表记录在空闲列表中,分配方式为从空闲列表中取合适的大小分配给对象。

  • 访问对象的方式有哪些呢?

    通过句柄访问,栈中对象的引用指向的是句柄地址,句柄包含了实例数据与数据类型信息。对象移动时更方便。

    直接访问,栈中对象的引用指向的是实例数据的地址。访问速度更高。

  • 如何判断对象已经死亡?

    引用计数,最直接,最简单,但不能解决循环引用问题,而且算法边界很多,不容易实现

    可达性分析,从 GC Root 开始,根据引用关系向下搜索。GC Root 包括:栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象、JVM 内部的引用例如 Class 对象、所有被 synchronized 持有的对象、JMX 内部中注册的回调等等。

  • 应用类型?
    强引用

    软引用

    弱引用

    虚引用

  • 如何判断一个类不再使用?同时满足以下三个条件:

    类的所有实例已被回收

    类的加载器已被回收(通常难以达成,除非特殊设计目的存在,例如OSGI、JSP的重加载等)

    类对应的 Class 对象不被应用,也不能通过反射访问这个类

#Java##计算机##编程##Java学习#
全部评论
感谢楼主分享,狠详细狠实用
点赞 回复 分享
发布于 2022-07-29 19:06

相关推荐

11-14 16:13
已编辑
重庆科技大学 测试工程师
Amazarashi66:不进帖子我都知道🐮❤️网什么含金量
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
点赞
1
分享
牛客网
牛客企业服务