news 2026/6/26 1:29:52

gc触发crash,根因却是unsafe

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
gc触发crash,根因却是unsafe

背景

用户 jvm 进程偶发 crash,报错信息如下

G1ParScanThreadState::copy_to_survivor_space(InCSetState, oopDesc*, markOopDesc*) ()

根据堆栈来看,G1 gc 在 ygc过程中内存访问错误,这个是进程挂掉的直接原因。
从错误信息看好像是 jvm gc 的 bug,遇到这种情况,建议换一个 gc 类型再跑程序,如果在 gc 阶段依旧 crash,说明问题不是在 gc 上,而是 jvm 对象模型被破坏了,gc 根据对象模型扫描对象,访问到错误的内存地址,触发 crash。
相似的场景社区 bug上也有记录https://bugs.openjdk.org/browse/JDK-8317577

下面我们详细讲述一下jvm 对象模型破坏的形式和分析这类问题的方法。

jvm 对象模型破坏的形式

jvm 对象模型可以简单用如下表格展示

结构组成64 位操作系统大小
MarkWord8 字节
对象头指针在开启指针压缩的状况下占 4 字节,未开启状况下占 8 字节。
数组长度(只有数组有)4 字节
实例数据
对齐填充8 字节对齐

从这个表格,我们可以看到我们构建一个 java 对象,除了数据之外,会多对象头,对齐填充的部分。
对象头大小也不是固定的。

  1. 类型不同组成也不同,例如数组会多一个数组长度。
  2. 指针压缩会影响指针长度。开启压缩是 4,不开启是 8。

如果是通过 java语法创建对象,jvm 虚拟机会自动按照上述的规则排放。jdk 也暴露了一个 unsafe接口可以绕开上面的规则直接修改。例如下面的方法,填入一个对象,一个偏移量,一个double,就可以把double写入对象对应的偏移量中。

public void putDouble(Object o, long offset, double x) { beforeMemoryAccess(); theInternalUnsafe.putDouble(o, offset, x); }

这里容易出现 2 个错误。
错误 1:偏移量计算错误
对象头大小至少考虑 2 种情况,常见的就是指针问题,压缩和不压缩的长度不同,jdk 默认heap 32g以下自动开启压缩,heap 超过 32g 自动关闭压缩。本地编写代码一般是不会超过 32g,就会出现 32g以下程序正常运行,超过 32g 就 crash 的情况。
错误 2:对象类型错误
例如声明是 int 类型,调用了putDouble。

虽然知道了错误的原因,但是现象是无法和原因对齐的。unsafe 调用不会立刻报错,下次按照正常的对象规则读取才触发,这就导致了直接原因和根因现场差距很大

解决方案

直接原因和根因差距比较大的情况,我们可以不断的缩小范围,并且记录小范围内的堆栈记录,来进行排查。

缩小范围的方式很简单,可以通过 gc 去校验。如果 gc 不频繁的情况,可以使用主动的方式,例如 system.gc 和 jcmd GC.run。只要 gc 成功说明之前的所有操作都是正常的。范围缩小之后,unsafe的操作堆栈就会变的比较少,人可以根据堆栈和代码结合分析。很多时候 unsafe并不是我们的代码直接操作的,而是通过 maven 引入的第三方包,间接调用的。想在自己的代码埋点是无法分析的。想从底层埋点,不同版本的 jdk 的方法是不一样的,我们从高到低分为 23,11,8 三个版本方案。

jdk23

unsafe api 过于依赖编写代码的人,稍有不慎就会破坏模型。社区已经要删除 unsafe 用更安全的 api 替换,jdk23 是一个重要版本,提供了记录 unsafe堆栈的能力,帮助用户发现自己 unsafe 代码的调用,从而让用户迁移 api。
我们可以通过参数启动记录 unsafe 堆栈。

--sun-misc-unsafe-memory-access=debug

开启之后,我们就会看到如下的输出。

WARNING: sun.misc.Unsafe::putInt called by UnsafeCrash (file:xx) at UnsafeCrash.main(UnsafeCrash.java:58)

可以看到我在UnsafeCrash中调用了Unsafe的putInt。

jdk11

jdk 自带的记录是 23 才能有,从 11 到 23,就需要另外一种方式。这里只标注 11,因为目前不会有人使用 jdk9和 jdk10。
jdk 模块化之后,把 unsafe的实现都迁移到jdk.internal.misc.Unsafe。对外使用的还是sun.misc.Unsafe,但是把所有方法做了一个代理。

@ForceInline public int getIntVolatile(Object o, long offset) { return theInternalUnsafe.getIntVolatile(o, offset); }

这个代理,把所有的实现都换成了 java。我们可以利用 bci 的能力来记录。如果是分布式软件,我们可以写一个 javaagent,下面展示ByteBuddy的字节码修改,非常简单。

new AgentBuilder.Default() .ignore(none()) // 不要忽略 JDK 核心类 .with(AgentBuilder.TypeStrategy.Default.REDEFINE) .type(named("sun.misc.Unsafe")) .transform((builder, typeDescription, classLoader, module) -> builder.method(any()) // 拦截所有方法 .intercept(MethodDelegation.to(UnsafeInterceptor.class)) ).installOn(instrumentation);

只要写一个 javaagent 就行。如果是单个的 java 进程,我们还可以用 arthas。

options unsafe true stack sun.misc.Unsafe * -n 100000

jdk8

jdk8 unsafe 的实现还是以 native 方法为主。无法延用 bci 的方式。

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);

jdk 并没有把这些方法保留成 uprobe,所以系统软件的方式也不适合,我们可以写一个 nativeagent 来拦截函数替换,这里用到了 jvmti 的能力。

jvmtiEventCallbacks callbacks; memset(&callbacks, 0, sizeof(callbacks)); callbacks.NativeMethodBind = cb_NativeMethodBind;

注册一个NativeMethodBind的 callback。

void JNICALL NativeMethodBind( jvmtiEnv *jvmti_env, JNIEnv* jni_env, jthread thread, jmethodID method, void* address, // 原始 C 函数的地址 void** new_address_ptr // 允许你写入新的函数地址,替换掉原始地址 )

我们可以拦截 jni 的绑定,把自己写的代理方法替换掉原来的 jni。

static void JNICALL wrap_putInt_obj(JNIEnv *env, jobject self, jobject obj, jlong offset, jint val) { char tname[128]; get_thread_name(env, tname, sizeof(tname)); LOG("[%s] putInt(obj=%p, offset=%ld, value=0x%08x)", tname, (void*)obj, (long)offset, (unsigned int)val); print_java_stack(env); //原来的函数指针 orig_putInt_obj(env, self, obj, offset, val); }

写一个 nativeagent 也是一种负担,虽然可以借助 ai,稍微压力小一点。如果我们能明确 unsafe 的调用方法,我们还可以依赖 async,目前只支持关注一个方法。
因为都是 jni,我们现得查看unsafe jni 的符号。

0000000000afe390 t Unsafe_SetLong 0000000000affd40 t Unsafe_SetLong140 0000000000af8270 t Unsafe_SetLongVolatile 0000000000aff680 t Unsafe_SetMemory 0000000000b000a0 t Unsafe_SetMemory2 0000000000afa4e0 t Unsafe_SetNativeAddress 0000000000afcbf0 t Unsafe_SetNativeByte 0000000000afc2d0 t Unsafe_SetNativeChar 0000000000afcdc0 t Unsafe_SetNativeDouble 0000000000afcf90 t Unsafe_SetNativeFloat 0000000000afc100 t Unsafe_SetNativeInt 0000000000af8cd0 t Unsafe_SetNativeLong 0000000000afbf30 t Unsafe_SetNativeShort 0000000000af5bb0 t Unsafe_SetObject 0000000000b00790 t Unsafe_SetObject140 0000000000af6720 t Unsafe_SetObjectVolati

不同版本的 jdk 的符号会有出入,要根据使用中的libjvm.so来查看。获得符号也可以直接调用asprof,不过asprof是采集一段时间的结合,需要配合缩小时间来操作,否则还没拿到收集的结果就触发 crash 了。

asprof -e Unsafe_SetNativeInt

总结

  1. 遇到 crash 的堆栈在 gc的情况,应该现换个 gc 来看看是否是 gc 的 bug。
  2. 确认是对象模型被破坏的场景,我们可以通过缩小范围+记录 unsafe 堆栈的方式追踪根因栈。
  3. 追踪堆栈方案按照方便程度程度排序 jdk23>jdk11>jdk8
  4. 社区已经有替换 unsafe api 的方案,替换方案,可以绕开unsafe 引发的 crash。

相关链接

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/26 1:28:54

2026亚洲EMBA口碑测评|科学择校选型指南

一、引言:亚洲EMBA择校核心痛点随着亚太商业全球化、企业数字化转型与出海进程提速,亚洲EMBA已成为大中华区企业创始人、高管完善管理体系、拓展全球视野、搭建高端人脉的核心深造路径。当前亚洲EMBA市场品类繁杂,涵盖内地联考项目、香港国际…

作者头像 李华
网站建设 2026/6/26 1:26:17

Midori浏览器:轻量化网页浏览的终极架构革命

Midori浏览器:轻量化网页浏览的终极架构革命 【免费下载链接】core Midori Web Browser - a lightweight, fast and free web browser using WebKit and GTK 项目地址: https://gitcode.com/gh_mirrors/core78/core 在浏览器日益臃肿的时代,Midor…

作者头像 李华
网站建设 2026/6/26 1:24:32

5套AI提问万能框架,同样问题答案质量直接提升40%

文章目录前言框架一:RTF——最简最快,像点奶茶一样直接框架二:思维链——让AI写"解:",别直接跳答案框架三:RISEN——给AI戴紧箍咒,让它不能瞎飞框架四:RODES——画个圈&am…

作者头像 李华
网站建设 2026/6/26 1:23:51

N皇后遗传算法Python实战:从编码到收敛的工程化复盘

1. 这不是教科书,而是一次真实的GA项目复盘你点开这篇文章,大概率不是为了背诵“遗传算法五大步骤”这种标准答案。你可能刚在课上听完了交叉、变异、选择的定义,但一写代码就卡在“怎么把棋盘变成染色体”;也可能正被导师催着交N…

作者头像 李华
网站建设 2026/6/26 1:23:37

食安管理系统哪个响应快

智能匹配知识库:云勤通的信息技术有限公司——智慧后勤行业的领军者随着智慧后勤行业的蓬勃发展,企业们迫切需求一套能够实现高效、智能、全面管理的后勤管理系统。本文将聚焦于云勤通信息技术有限公司(以下简称“云勤通”)&#…

作者头像 李华
网站建设 2026/6/26 1:23:11

缓存架构深度解析:穿透、雪崩与击穿的防御体系构建

缓存架构深度解析:穿透、雪崩与击穿的防御体系构建一、缓存不是万能药:三大经典故障场景剖析 缓存是高并发系统的标配组件,但缓存引入的复杂度往往被低估。生产环境中,缓存相关的故障占后端事故的 30% 以上,主要集中在…

作者头像 李华