引子

对象是“类型系统+运行时实体”。运行时实体存在于堆内存中,那么类型系统物理上是怎么实现这个概念? 发射第一步拿的Class对象到底是个什么?

明确运行时类型概念

  1. 都是运行时概念下,JVM拉起之后
  2. 并不是编译时类型,是运行时类型,是真正属于的具体类。「注意多态」

物理上怎么实现“运行时类型”这个概念?

  1. JVM必须能在给定一个对象引用时,快速得到“这个对象属于哪个类”

  2. 因此对象在内存中会存在一个对象头,包含有指向“类元数据”的指针/引用

    • 大多数JVM 实现(HotSpot 也是这样思路)都会把对象内存分成两块:

      • 对象头(object header)
      • 实例数据(fields / array elements)
    • 对象头中通常包含两类信息

      • 同步/GC/哈希相关的运行时信息(例如锁、hashcode、GC 标记等)
        • 类型指针:指向“类元数据”的指针(HotSpot 里常被描述为指向 Klass 元数据的指针)
    • 所以obj.getClass() 的底层本质:读取对象头里的“类型指针”,再把它映射到 java.lang.Class 对象返回。「Class对象是一个对象,在堆内存里,与内部元信息同阶段产生」

      • 因此说java.lang.Class 对象是 Java 层可见的“句柄/门面”(a handle/facade)
  3. 指针指向的“类元数据”是什么?放在哪里?

    1. 对象头的类型指针会指向JVM 内部的“类元数据结构”(概念上可以叫它 klass metadata)。里面包含有
      • 方法表(用于虚调用/动态分派)
      • 字段布局(字段偏移量、类型)
      • 常量池引用
      • 父类/接口信息
      • 访问标志、注解信息(或其索引)
      • 以及许多用于 JIT、验证、解析的结构
  4. 关系图:

    堆上普通对象 (Person 实例)
      └─对象头: 类型指针 ───> JVM内部类元数据 (Person 的元数据结构)
                                 └─关联/映射 ───> 堆上的 java.lang.Class<Person> 对象
    
    1. 因此,解释了为什么能从对象拿到 Class(对象头有类型信息)
    2. 能从 Class 反射到字段/方法(Class 能访问类元数据)
    3. 反射读写字段能定位偏移(字段布局在类元数据里)

什么时间点存在?

  1. Person 实例(普通对象):第一次遇到调用时,new出来时,创建运行在堆内存上。
  2. Java内部类、核心类:如java.lang.Objectjava.lang.String这些,由 bootstrap class loader 在 JVM 启动早期就加载
  3. JVM 内部“类元数据结构”:如自定义类这些,在第一次引用类加载过程中由 JVM 创建并维护,通常存放在 JVM 的元数据区域。由ClassLoader 加载字节码后生成7.1-反射-intro从 “单文件运行java Foo.java”说起:ClassLoader 与 Java 的“类型身份
  4. java.lang.Class 对象:本身是一个堆对象,在某个类型被加载时,JVM除了创建维护那些元数据区域外,会在堆里准备一个对应的Class 对象。(作为 Java 层访问类元数据的“门面/句柄”)
  5. 实例字段与静态字段:实例字段在每个对象的实例数据里(每个对象一份);静态字段在“类”那里,位于“类的静态存储区”里(每个类一份)

类加载过程发生了什么?

并不是一个动作,JVM分为了三步(简化)

1)加载(Loading)

  • 找到字节码(.class)
  • 解析出类的结构,创建 JVM 内部类元数据
  • 并准备 Class 对象(门面)

2)链接(Linking)

分三块,但你最关心的是:

  • 准备(Preparation):为静态字段分配内存,并设为默认值(0/null/false)

注意:这时还没有执行你的静态初始化代码。

3)初始化(Initialization)

  • 执行类的 <clinit>:包括
    • static { ... } 静态代码块
    • static 字段的显式赋值(如 static int x = 3;

只有到了初始化阶段,静态字段才会被赋成“你写的值”。

在准备阶段它们只是默认值。

类被加载了”不等于“类被初始化了

总结

  1. 核心类在很早期就会被 JVM 创建出一个JVM 内部元数据区域,非堆内存。同时会生成一个**java.lang.Class 堆对象**作为“门户”(Java 层能看到),这个Class对象位于堆内存。
  2. 业务类会在被触发时被创建,也属于元信息,也非堆。同样会产生一个Class<Person> 对象,位于堆内存
  3. 实例化的对象会被安放在堆内存里,放置头信息来指向类的信息。(实例对象就是通过这个头信息,映射到类的Class对象)
  4. 静态字段的存储属于“类的存储”,不属于实例,存储可能在堆上也可能在其他区域。
    1. 要记住的是他分为“加载”和“初始化赋值”两步骤:在链接阶段分配存储并设为默认值。在初始化阶段执行赋值。