objectFieldOffset仅返回字段内存偏移量,不提供读写能力;修改私有字段需配合putXxx等原子方法,且须先通过trySetAccessible()获访问权限、JVM启动参数授权模块、严格匹配字段类型,否则会抛异常或引发崩溃。

为什么 objectFieldOffset 不能直接用于修改私有字段
objectFieldOffset 返回的是某个字段在对象内存布局中的偏移量(long 类型),但它本身不提供读写能力;它只是 Unsafe 的“地址计算器”。真正要读写,必须配合 getObject/putObject、getInt/putInt 等带偏移量的原子方法。而且——关键点是:字段必须是**可访问的(accessible)**,否则 staticFieldOffset 或 objectFieldOffset 调用会直接抛 RuntimeException(不是 IllegalAccessException)。
常见错误现象:sun.misc.Unsafe.objectFieldOffset 报 java.lang.RuntimeException: field is not accessible,尤其在 JDK 9+ 模块系统下,默认禁止反射访问非开放模块的私有成员。
- 必须先用
Field.setAccessible(true)尝试绕过访问检查(JDK 8 有效;JDK 9+ 需额外 JVM 参数) - JDK 12+ 默认彻底禁用
setAccessible对关键类(如String.value)的生效,即使加了--add-opens也可能被SecurityManager或模块限制拦截 -
objectFieldOffset不接受final字段(JDK 17 后多数实现直接拒绝,返回 0 或抛异常)
如何在 JDK 17+ 安全获取私有字段偏移量(且大概率可用)
绕过反射限制不是靠“技巧”,而是靠 JVM 启动参数显式授权。没有这一步,objectFieldOffset 对大多数私有字段根本调不通。
- 启动时必须添加:
--add-opens java.base/java.lang=ALL-UNNAMED(若操作String的value)或对应目标类所在模块 - 如果类在自定义模块中,还需
--add-opens your.module/your.package=ALL-UNNAMED - 仅加
--illegal-access=permit不够,该参数在 JDK 16+ 已废弃,不影响Unsafe的字段定位逻辑 - 验证是否生效:调用前先执行
field.trySetAccessible()(JDK 12+ 推荐方式),再检查field.canAccess(null)是否为true
示例(获取 String 内部 value 字段偏移):
Field valueField = String.class.getDeclaredField("value");
valueField.trySetAccessible();
long offset = unsafe.objectFieldOffset(valueField); // 此时才可能成功
用偏移量读写时,类型必须严格匹配
Unsafe 的偏移读写方法是类型强绑定的:用 getInt 读 long 字段会字节错位,用 putObject 写基本类型字段会触发 JVM 断言失败或直接崩溃(JDK 17+ 常见 segfault)。
- 字段是
int→ 必须用unsafe.getInt(obj, offset)和unsafe.putInt(obj, offset, newValue) - 字段是
Object(如String.value是byte[])→ 必须用unsafe.getObject/putObject - 字段是
boolean或byte→ 只能用getByte/putByte,别误用getInt(大小端和对齐都不同) - 注意平台差异:
long和double在某些 JVM 实现中可能要求 8 字节对齐,偏移非法会导致IllegalArgumentException
真实场景中,比 offset 更容易出问题的是对象生命周期
通过 objectFieldOffset + putObject 替换一个引用字段(比如把 ArrayList.elementData 换成新数组),看起来可行,但极易引发 GC 问题:
- 旧数组如果仍被其他地方强引用,不会立即回收;但若已无强引用,JVM 可能在任意时刻回收它,而你的偏移写入可能还在用它的地址(悬垂指针)
-
Unsafe.putObject绕过写屏障(write barrier),JVM 的 G1/ZGC 无法感知该引用变更,导致并发标记阶段漏标,最终发生 use-after-free - 没有内存屏障保证可见性:多线程下,其他线程可能永远看不到你用
putObject写入的新值
所以,除非你在写 JIT 编译器、GC 算法或极底层的运行时库,否则不要用 objectFieldOffset 做生产环境的对象字段篡改。它不是“黑科技捷径”,而是裸金属级操作——少一个内存屏障、错一个类型、漏一个模块授权,结果都是不可预测的崩溃或静默数据损坏。

















