Unsafe类不提供对象复制功能,本质是按字段逐个读写偏移量定位的值;偏移量是字段距对象头的字节距离,非内存地址,需配合对应类型get/put方法使用,不可硬编码或误作指针。

Unsafe 类不提供“对象复制”功能,所谓“通过偏移量实现对象复制”,本质是**按字段逐个读取源对象的字段值、再逐个写入目标对象对应偏移位置**,属于手动内存拷贝逻辑,不是 JVM 层面的对象克隆。关键在于理解偏移量是字段在对象内存布局中的**相对位置标识**,而非地址指针。
偏移量不是地址,而是字段距对象头的字节距离
Unsafe.objectFieldOffset() 返回的是 long 类型数值,表示该字段从对象起始地址(即对象头开始处)向后偏移的字节数。这个值由 JVM 在类加载阶段根据字段类型、顺序、对齐规则和 JVM 参数(如 -XX:+UseCompressedOops)动态计算得出。它不指向物理内存,也不能做加减运算生成新地址;只用于后续 getInt、putObject 等方法定位字段。
- JDK 8 中 String 的 value 字段偏移可能是 16,JDK 17 可能变成 24,硬编码会崩溃
- 静态字段要用 staticFieldOffset(),实例字段用 objectFieldOffset(),混用会导致读错内存区域
- 数组元素访问需组合使用 ARRAY_INT_BASE_OFFSET + index × ARRAY_INT_INDEX_SCALE,不能直接用 objectFieldOffset()
模拟“对象复制”必须字段级遍历+动态获取偏移
要安全地把 A 对象字段值复制到 B 对象,不能靠反射 set/get,也不能整体 memcpy(Java 对象含隐藏头信息,结构不连续)。正确做法是:对每个需复制的字段,先调用 objectFieldOffset() 获取其在类中的偏移,再用对应类型 get 方法从 A 读出、put 方法写入 B。
- 必须为每个字段单独调用 offset 方法——一个类有 name、age、score 三个字段,就要调三次 objectFieldOffset
- 字段类型决定用哪个 get/put 方法:int 用 getInt/putInt,引用类型用 getObject/putObject,volatile 写用 putIntVolatile
- 若目标对象 B 是 allocateInstance() 创建的,它跳过构造函数,所有字段为默认值(0/null),此时复制才真正“生效”
常见误用:把偏移当地址、跳过字段类型校验
很多尝试“高效复制”的代码失败,是因为混淆了偏移量语义。例如将 long 偏移值强制转成 longAddress 当作指针传给 copyMemory,或对 String 对象直接 copyMemory(objA, 0, objB, 0, size),这会把对象头、klass 指针、mark word 全部覆盖,导致 GC 无法识别对象,下次回收直接 crash。
- copyMemory(srcBase, srcOffset, destBase, destOffset, bytes) 的前两个参数是 base address + offset,但 Unsafe 不提供获取对象 base address 的公开方法
- 试图用 getAddress() 读取对象地址?该方法已被移除或仅限内部使用,外部调用抛 UnsupportedOperationException
- 用 putAddress 写偏移量数值到某处?那是在乱写内存,极大概率破坏 JVM 运行时数据结构
真正可控的“复制”场景其实很窄
目前较稳妥的应用集中在两类:一是序列化框架(如 Kryo、FST)绕过反射加速字段读写;二是高性能缓存中对简单 DTO 做浅拷贝。它们都严格遵循“先查偏移、再按类型读写、不跨对象头操作”原则,并针对不同 JDK 版本做偏移量缓存与校验。
- 生产环境禁用 allocateInstance + 手动填充字段的方式构建核心业务对象——构造函数逻辑(如校验、初始化)被跳过,状态不可信
- 不要尝试复制含有 final 字段的对象——Unsafe 可强行修改,但会破坏 happens-before,多线程下值不可见
- 涉及 String、ArrayList 等 JDK 内置类时,字段名和布局属实现细节,不同版本差异大,应优先走标准 API

















