13对象内存和反射
a对象如何存储

HotSpot是使用指针的方式来访问对象:
Java堆中会存放指向类元数据的地址
Java栈中的reference存储的是指向堆中的对象的地址
类元数据
- 对象类型数据:存储类的元数据(如类的结构、方法、字段等信息)。
简单来说:元数据是”类的说明书”,告诉JVM”这个类长什么样、有什么方法、有什么字段”。
类的元数据包含哪些内容?(核心组成)
| 元数据类型 | 说明 | 示例 |
|---|---|---|
| 类名 | 类的完整名称(包括包名) | com.example.Person |
| 父类 | 继承的父类 | java.lang.Object |
| 接口 | 实现的接口列表 | Serializable, Cloneable |
| 字段 | 成员变量信息(名称、类型、访问权限) | private String name; |
| 方法 | 方法信息(名称、参数、返回类型、访问权限) | public String getName() |
| 常量池 | 字符串常量、类常量等 | "Hello World" |
| 访问标志 | 类/方法的访问权限(public、private等) | public final class |
| 注解 | 类/方法/字段上的注解 | @Deprecated, @Override |
元数据 vs 实例数据(关键区别)
| 项目 | 元数据 | 实例数据 |
|---|---|---|
| 存储位置 | 方法区(或元空间) | Java堆 |
| 生命周期 | 类加载时创建,JVM运行期间存在 | 通过new创建,随对象销毁而回收 |
| 内容 | 类的”说明书”(静态结构) | 对象的实际数据(动态值) |
| 示例 | Person类的字段/方法定义 |
new Person("Tom", 25)中的name="Tom" |
| 数量 | 每个类只有1份 | 每个对象1份 |
💡 类比理解:
元数据 = 《房屋设计图纸》
实例数据 = 真实建造的房屋(每个房子不同)
为什么需要元数据?(为什么不是只有代码)
如果JVM只存储代码,无法实现以下功能:
instanceof检查(obj instanceof Person)- 动态加载类(
Class.forName("com.example.Person")) - 注解处理(如Spring的
@Autowired) - 类型安全检查(编译器在运行时验证)
✅ 总结:元数据是JVM理解”类”的唯一桥梁,没有它,Java的动态特性(反射、注解、动态代理等)将无法实现。
实际内存中的表现(简化版)
1 | |
💡 关键点:
元数据在方法区(类加载时创建),
实例数据在堆(运行时创建),
元数据描述了实例数据的结构。
对象内存布局


放在了栈中
64为虚拟机markword详情如下:

反射话术
4,反射原理
反射是什么
指在程序运行期间动态获取和操作类元数据的一种机制
反射为什么(原理)
与类元数据有关 类元数据保存在了方法区中(类元数据==类信息==Class)
每个对象的对象头中都有一个指向类元信息的一个类元指针
反射原理其实指的就是在运行的时候,通过获取对象头中的classPointer
(老家地址)指向方法区的类元信息instanceKlass对象
中的_java_mirror 属性来获取对应的类对象,该对象我们是可以直接操作的
(获取类各个信息的能力,比如类名,父类接口 变量方法等信息)
反射怎么用
Class.ForName("类的全类路径名")
Object.getClass()
对象.class
通过setAccessible(true)可绕过访问权限检测--暴力反射
原本要获取的对象被private修饰,可以通过该方法进行暴力获取

Klass概念(加载到了方法区的class对象)
Klass是c++层面类元信息的抽象,对应java层面的class
Klass 继承自 Metadata 继承自 MetaspaceObj
InstanceKlass继承自Klass,InstanceKlass是.class文件被加载到元空间中的存在形式
1.9.3.总结
反射提供了认识自身的一种途径java反射提供了在运行时获取类各个信息的能力,比如类名,父类接口变量方法等信息
反射原理其实指的就是在运行的时候,通过获取对象头中的classPointer(老家地址)指向方法区的类元信息instanceKlass对象
中的_java_mirror 属性来获取对应的类对象,该对象我们是可以直接操作的(获取类各个信息的能力,比如类名,父类接口 变量方法等信息)
重要point
引用存在了栈中,类元指针指向了堆中的klass
话术
[简述Java创建对象的过程]
检查该指令的参数能否在常量池中定位到一个类的符号引用,并检查引用代表的类是否已被加载、解析和初始化,如果没有就先执行类加载。
通过检查通过后虚拟机将为新生对象分配内存。
完成内存分配后虚拟机将成员变量设为零值
设置对象头,包括哈希码、GC 信息、锁信息、对象所属类的类元信息等。
执行 init 方法,初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
类加载器的流程
从类被加载到虚拟机内存中开始,到释放内存总共有 7 个步骤:加载,验证,准备,解析,初始化,使用,卸载。其中验证,准备,解析三个部分统称为连接
[2.1.1 加载]
- 将 class 文件加载到内存
- 将静态数据结构转化成方法区中运行时的数据结构
- 在堆中生成一个代表这个类的 java.lang.Class 对象作为数据访问的入口
[2.1.2 链接]
- 验证:确保加载的类符合 JVM 规范和安全,保证被校验类的方法在运行时不会做出危害虚拟机的事件,其实就是一个安全检查
- 准备:为 static 变量在方法区中分配内存空间,设置变量的初始值,例如 static int a = 3 (注意:准备阶段只设置类中的静态变量(方法区中),不包括实例变量(堆内存中),实例变量是对象初始化时赋值的)
- 解析:虚拟机将常量池内的符号引用替换为直接引用的过程(符号引用比如我现在 import java.util.ArrayList 这就算符号引用,直接引用就是指针或者对象地址,注意引用对象一定是在内存进行)
[2.1.3 初始化]
初始化其实就是执行类构造器方法的
<clinit>()的过程,而且要保证执行前父类的<clinit>()方法执行完毕。这个方法由编译器收集,顺序执行所有类变量(static 修饰的成员变量)显式初始化和静态代码块中语句。此时准备阶段时的那个static int a由默认初始化的 0 变成了显式初始化的 3。 由于执行顺序缘故,初始化阶段类变量如果在静态代码块中又进行了更改,会覆盖类变量的显式初始化,最终值会为静态代码块中的赋值。注意:字节码文件中初始化方法有两种,非静态资源初始化的
<init>和静态资源初始化的<clinit>,类构造器方法<clinit>()不同于类的构造器,这些方法都是字节码文件中只能给 JVM 识别的特殊方法。[2.1.4 卸载]
1 该类的所有的实例对象都已被 GC,也就是说堆不存在该类的实例对象。
2 该类没有在其他任何地方被引用
3 该类的类加载器的实例已被 GC
[简述JVM给对象分配内存的策略]
- 指针碰撞:这种方式在内存中放一个指针作为分界指示器将使用过的内存放在一边,空闲的放在另一边,通过指针挪动完成分配。
- 空闲列表:对于 Java 堆内存不规整的情况,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。
[Java对象内存分配是如何保证线程安全的]
第一种方法,采用CAS机制,配合失败重试的方式保证更新操作的原子性。该方式效率低。
第二种方法,每个线程在Java堆中预先分配一小块内存,然后再给对象分配内存的时候,直接在自己这块”私有”内存中分配。一般采用这种策略。
[简述对象的内存布局]
对象在堆内存的存储布局可分为对象头、实例数据和对齐填充。
1)对象头主要包含两部分数据: MarkWord、类型指针。
MarkWord 用于存储哈希码(HashCode)、GC分代年龄、锁状态标志位、线程持有的锁、偏向线程ID等信息。
类型指针即对象指向他的类元数据指针,如果对象是一个 Java 数组,会有一块用于记录数组长度的数据。
2)实例数据存储代码中所定义的各种类型的字段信息。
3)对齐填充起占位作用。HotSpot 虚拟机要求对象的起始地址必须是8的整数倍,因此需要对齐填充。
end?。