从 FileOutputStream 到 int fd:一次 JNI 字段读写全链路源码剖析

一、为什么需要 initIDs:先把字段句柄缓存起来

JNI 代码要访问 Java 字段,必须先用 GetFieldID 拿到一个 jfieldID。这个操作需要在符号表里查找字段名和签名,相对耗时。因此 OpenJDK 的做法是:在 Java 类初始化时,通过 <clinit> 调用 native 方法 initIDs(),把字段句柄一次性缓存到全局变量。

FileDescriptor 侧:缓存两个字段句柄

c

scss 复制代码
JNIEXPORT void JNICALL
Java_java_io_FileDescriptor_initIDs(JNIEnv *env, jclass fdClass) {
    // fd 是 int 字段,签名 "I"
    CHECK_NULL(IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "fd", "I"));
    // append 是 boolean 字段,签名 "Z"
    CHECK_NULL(IO_append_fdID = (*env)->GetFieldID(env, fdClass, "append", "Z"));
}

FileOutputStream 侧:缓存一个对象字段句柄

c

bash 复制代码
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_initIDs(JNIEnv *env, jclass fdClass) {
    // fd 是 FileDescriptor 类型,签名 "Ljava/io/FileDescriptor;"
    fos_fd = (*env)->GetFieldID(env, fdClass, "fd", "Ljava/io/FileDescriptor;");
}

到这里要建立的核心认知是:fos_fd、IO_fd_fdID、IO_append_fdID 都不是对象、不是 int、不是布尔值 ,它们只是"定位字段"的句柄。同一个 fos_fd,作用在不同 FileOutputStream 实例上,会取出各自不同的 FileDescriptor 对象。


二、jni_GetFieldID 内部:jfieldID 到底是什么

GetFieldID 进入 HotSpot 后执行的是:

c

scss 复制代码
JNI_ENTRY(jfieldID, jni_GetFieldID(JNIEnv *env, jclass clazz,
          const char *name, const char *sig))
  jfieldID ret = 0;
  // 从 jclass 拿到对应的 Klass 元数据
  Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz));

  // 字段名和签名必须已经在符号表里;否则字段不存在
  TempNewSymbol fieldname = SymbolTable::probe(name, (int)strlen(name));
  TempNewSymbol signame  = SymbolTable::probe(sig,  (int)strlen(sig));
  if (fieldname == NULL || signame == NULL) {
    THROW_MSG_0(vmSymbols::java_lang_NoSuchFieldError(), ...);
  }

  // 关键:对外发放字段 ID 之前,先确保类已经初始化
  k->initialize(CHECK_NULL);

  // 在类元数据里真正查找该字段
  fieldDescriptor fd;
  if (!k->is_instance_klass() ||
      !InstanceKlass::cast(k)->find_field(fieldname, signame, false, &fd)) {
    THROW_MSG_0(vmSymbols::java_lang_NoSuchFieldError(), ...);
  }

  // 把字段偏移编码成 jfieldID 返回
  ret = jfieldIDWorkaround::to_instance_jfieldID(k, fd.offset());
  return ret;
JNI_END

注意注释里那句关键说明: "A jfieldID for a non-static field is simply the offset of the field within the instanceOop" ------ 非静态字段的 jfieldID 本质上就是字段在实例对象里的偏移量。Klass* k 只在构造时用于查字段、算偏移、算哈希,返回的 jfieldID 里并不保存 Klass 指针。


三、编码与解码:jfieldID 是一对互逆位运算

编码:offset → jfieldID

c

scss 复制代码
static jfieldID to_instance_jfieldID(Klass* k, int offset) {
    // 把 offset 左移,低位腾出来放"实例字段"标志位
    intptr_t as_uint = ((offset & large_offset_mask) << offset_shift)
                       | instance_mask_in_place;
    if (VerifyJNIFields) {
        // 校验模式下额外把类的哈希编进高位,用于后续归属校验
        as_uint |= encode_klass_hash(k, offset);
    }
    // 强转成指针类型返回(本质上是个被伪装成指针的整数)
    jfieldID result = (jfieldID) as_uint;
#ifndef ASSERT
    if (VerifyJNIFields)
#endif
    {
        verify_instance_jfieldID(k, result);
    }
    assert(raw_instance_offset(result) == (offset & large_offset_mask),
           "extract right offset");
    return result;
}

解码:jfieldID → offset

c

arduino 复制代码
static intptr_t raw_instance_offset(jfieldID id) {
    // 右移,丢掉低位标志位,把 offset 移回原位
    uintptr_t result = (uintptr_t) id >> address_shift;
    if (VerifyJNIFields && is_checked_jfieldID(id)) {
        // 切掉高位编进去的 klass hash,只留 offset
        result &= small_offset_mask;
    }
    return (intptr_t)result;
}

static intptr_t from_instance_jfieldID(Klass* k, jfieldID id) {
#ifndef ASSERT
    if (VerifyJNIFields)
#endif
    {
        // 校验这个 id 是否真的属于类 k
        verify_instance_jfieldID(k, id);
    }
    return raw_instance_offset(id);
}

约束条件很明确:offset_shift 必须等于 address_shift ,这样"左移编码、右移解码"才互为逆操作。VerifyJNIFields 打开时多编了哈希位,解码时用 small_offset_mask 把它切掉即可。

一句话总结:to_instance_jfieldID 是编码器,raw_instance_offset 是解码器,from_instance_jfieldID 是带归属校验的解码入口。


四、读取字段:jni_GetObjectField 怎么用 jfieldID

getFD 里的第一步 GetObjectField(env, obj, fos_fd) 进入 HotSpot:

c

ini 复制代码
JNI_ENTRY(jobject, jni_GetObjectField(JNIEnv *env, jobject obj, jfieldID fieldID))
  // 把 jobject 还原成堆里的 oop
  oop o = JNIHandles::resolve_non_null(obj);
  Klass* k = o->klass();
  // 关键:解码 jfieldID,得到字段偏移
  int offset = jfieldIDWorkaround::from_instance_jfieldID(k, fieldID);
  if (JvmtiExport::should_post_field_access()) {
    o = JvmtiExport::jni_GetField_probe(thread, obj, o, k, fieldID, false);
  }
  // 按 oop + offset 直接读字段,无需再回查类元数据
  oop loaded_obj = HeapAccess<ON_UNKNOWN_OOP_REF>::oop_load_at(o, offset);
  jobject ret = JNIHandles::make_local(THREAD, loaded_obj);
  return ret;
JNI_END

整条路径非常清晰:jobject → oop → 解码 jfieldID 得 offset → oop_load_at(o, offset) 取值。


五、写回字段:jni_SetIntField 用同样的方式定位

真正把 OS fd 写回 Java 对象,靠的是 SetIntField:

c

ini 复制代码
void jni_SetIntField(JNIEnv *env, jobject obj, jfieldID fieldID, jint value) {
    JavaThread* thread = JavaThread::thread_from_jni_environment(env);
    ThreadInVMfromNative __tiv(thread);
    HandleMarkCleaner __hm(thread);
    oop o = JNIHandles::resolve_non_null(obj);
    Klass* k = o->klass();
    // 同样先解码 jfieldID → offset
    int offset = jfieldIDWorkaround::from_instance_jfieldID(k, fieldID);
    if (JvmtiExport::should_post_field_modification()) {
        jvalue field_value;
        field_value.i = value;
        o = JvmtiExport::jni_SetField_probe(thread, obj, o, k, fieldID, false,
                                             JVM_SIGNATURE_INT, &field_value);
    }
    // 按 oop + offset 直接写
    o->int_field_put(offset, value);
}

可以看到读和写走的是同一套"解码 jfieldID → 得到 offset → 按偏移访问"的机制。


六、把 fd 真正写进去的入口:fileOpen

FileOutputStream 构造时会调用 native 的 open0:

c

arduino 复制代码
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_open0(JNIEnv *env, jobject this,
                                    jstring path, jboolean append) {
    // 直接调用 fileOpen,并把 fos_fd 作为参数传进去
    fileOpen(env, this, path, fos_fd,
             O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC));
}

fileOpen 是真正"开文件、拿 fd、写回 Java 对象"的地方:

c

scss 复制代码
void fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
    WITH_PLATFORM_STRING(env, path, ps) {
        FD fd;

#if defined(__linux__) || defined(_ALLBSD_SOURCE)
        // Linux 下内核不接受结尾的斜杠,需要手动裁掉
        char *p = (char *)ps + strlen(ps) - 1;
        while ((p > ps) && (*p == '/'))
            *p-- = '\0';
#endif
        // 真正调用 OS open(),拿到文件描述符
        fd = handleOpen(ps, flags, 0666);
        if (fd != -1) {
            jobject fdobj;
            jboolean append;
            // 通过 fos_fd 从 this(FileOutputStream 实例)里取出 FileDescriptor 对象
            fdobj = (*env)->GetObjectField(env, this, fid);
            if (fdobj != NULL) {
                // ★ 关键:把真实 fd 写回 FileDescriptor.fd 字段
                (*env)->SetIntField(env, fdobj, IO_fd_fdID, fd);
                // 同时记录是否为 append 模式
                append = (flags & O_APPEND) == 0 ? JNI_FALSE : JNI_TRUE;
                (*env)->SetBooleanField(env, fdobj, IO_append_fdID, append);
            }
        } else {
            throwFileNotFoundException(env, path);
        }
    } END_PLATFORM_STRING(env, ps);
}

这就是前文反复强调的答案:FileDescriptor() 构造时那个 fd = -1 只是占位,真正的 OS fd 是在 handleOpen 成功之后,通过 SetIntField(env, fdobj, IO_fd_fdID, fd) 写进去的。


七、完整调用链一图流

text

css 复制代码
Java: new FileOutputStream(file)
   │
   ├─ this.fd = new FileDescriptor()          // 此时 fd = -1
   │
   ├─ open0(name, append) ──► Java_java_io_FileOutputStream_open0
   │                              │
   │                              └─ fileOpen(env, this, path, fos_fd, flags)
   │                                    │
   │                                    ├─ handleOpen(ps, flags, 0666) → real_fd
   │                                    │
   │                                    ├─ GetObjectField(this, fos_fd) → FileDescriptor
   │                                    │
   │                                    └─ SetIntField(fdobj, IO_fd_fdID, real_fd)
   │                                          │
   │                                          └─ jni_SetIntField
   │                                                │
   │                                                ├─ from_instance_jfieldID → offset
   │                                                │
   │                                                └─ o->int_field_put(offset, value)
   │
   └─ 此后 fd 已不再是 -1

之后 Java: fos.write(bytes)
   │
   └─ writeBytes(...) ──► Java_java_io_FileOutputStream_writeBytes
                              │
                              └─ getFD(env, this, fos_fd)
                                    │
                                    ├─ GetObjectField(this, fos_fd) → FileDescriptor
                                    │       │
                                    │       └─ jni_GetObjectField
                                    │             ├─ from_instance_jfieldID → offset
                                    │             └─ oop_load_at(o, offset)
                                    │
                                    └─ GetIntField(fdo, IO_fd_fdID) → int fd
                                            │
                                            └─ 供 write(2) 系统调用使用

八、总结

概念 本质
fos_fd jfieldID,标识 FileOutputStream.fd(对象字段,签名 Ljava/io/FileDescriptor;)
IO_fd_fdID jfieldID,标识 FileDescriptor.fd(int 字段,签名 I)
IO_append_fdID jfieldID,标识 FileDescriptor.append(boolean 字段,签名 Z)
jfieldID 在 HotSpot 一个被伪装成指针的整数,编码了字段偏移量(+可选 klass hash)
编码 / 解码 to_instance_jfieldID ↔ raw_instance_offset / from_instance_jfieldID
fd 赋值点 fileOpen 成功 handleOpen 后,通过 SetIntField(..., IO_fd_fdID, fd) 写回
字段读写机制 解码 jfieldID 得 offset,再对 oop 做 oop_load_at / int_field_put

把握住"jfieldID 只是偏移量的编码包"这一点,再回头看 FileOutputStream → FileDescriptor → int fd 这条链路,就不会再被"它到底是个什么值"困惑了。

根据提供的源码和对话内容写一篇相关博客,不少于900字,根据用户提供的源码讲解,上下文贴合用户源码讲解, 方法调用链连贯。重要代码要添加注释

用户想要一篇博客文章(中文,≥900 字),以所提供的源码为中心。我来写一篇连贯的博客,覆盖完整链路,并给关键代码加注释。

从 write 到系统调用:FileOutputStream 底层 fd 的获取全链路解析

一、前言

在使用 Java 的 FileOutputStream 写文件时,真正执行 I/O 的其实是操作系统的 write(2) 系统调用,而它的第一个参数是一个 int 类型的文件描述符(fd)。问题是:这个 fd 存放在哪里?Java 层怎么把它交给 native 层?native 层又是通过什么机制从 Java 对象里把它"挖"出来的?

本文以 OpenJDK 源码为主线,把 FileOutputStream.writeBytes → getFD → GetObjectField / GetIntField 这条链路完整串起来,并顺带解释 jfieldID 在 HotSpot 里到底是个什么东西,以及 fd 是在哪里被写回 Java 对象的。


二、入口:writeBytes 做了哪些事

Java 层调用 fos.write(bytes) 时,会进入 native 方法:

c

arduino 复制代码
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_writeBytes(JNIEnv *env,
    jobject this, jbyteArray bytes, jint off, jint len, jboolean append) {
    // this 是当前 FileOutputStream 实例
    // 直接把缓存的字段句柄 fos_fd 传下去,避免在热路径里重复查字段
    writeBytes(env, this, bytes, off, len, append, fos_fd);
}

真正的逻辑在 writeBytes:

c

ini 复制代码
void
writeBytes(JNIEnv *env, jobject this, jbyteArray bytes,
           jint off, jint len, jboolean append, jfieldID fid)
{
    jint n;
    char stackBuf[BUF_SIZE];   // 小数据走栈,避免频繁 malloc
    char *buf = NULL;
    FD fd;

    // 参数校验
    if (IS_NULL(bytes)) {
        JNU_ThrowNullPointerException(env, NULL);
        return;
    }
    if (outOfBounds(env, off, len, bytes)) {
        JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
        return;
    }

    // 选择缓冲区:<= BUF_SIZE 用栈,否则堆分配
    if (len == 0) {
        return;
    } else if (len > BUF_SIZE) {
        buf = malloc(len);
        if (buf == NULL) {
            JNU_ThrowOutOfMemoryError(env, NULL);
            return;
        }
    } else {
        buf = stackBuf;
    }

    // 一次性把 Java byte[] 拷进本地缓冲区,避免重复跨边界
    (*env)->GetByteArrayRegion(env, bytes, off, len, (jbyte *)buf);

    if (!(*env)->ExceptionOccurred(env)) {
        off = 0;
        while (len > 0) {
            // ★ 每轮循环都重新取一次 fd
            fd = getFD(env, this, fid);
            if (fd == -1) {
                JNU_ThrowIOException(env, "Stream Closed");
                break;
            }
            // 根据 append 标志决定是追加写还是普通写
            if (append == JNI_TRUE) {
                n = IO_Append(fd, buf+off, len);
            } else {
                n = IO_Write(fd, buf+off, len);
            }
            if (n == -1) {
                JNU_ThrowIOExceptionWithLastError(env, "Write error");
                break;
            }
            off += n;   // 部分写时继续推进
            len -= n;
        }
    }
    if (buf != stackBuf) {
        free(buf);
    }
}

这里有两点值得注意:

  1. 热路径循环里每轮都重新取 fd 。这样即使写的过程中流被 close,也会立刻发现 fd == -1 并报 "Stream Closed"。
  2. 部分写处理 :n 可能小于 len,所以要 off += n; len -= n; 循环直到写完。

三、核心:getFD 如何从对象里挖出 int fd

真正把 fd 取出来的是这三行:

c

scss 复制代码
FD getFD(JNIEnv *env, jobject obj, jfieldID fid) {
    // 第一步:obj 是 FileOutputStream,fid 是 fos_fd
    // 取 obj.fd,得到 FileDescriptor 对象
    jobject fdo = (*env)->GetObjectField(env, obj, fid);
    if (fdo == NULL) {
        return -1;
    }
    // 第二步:fdo 是 FileDescriptor 对象,IO_fd_fdID 指向它的 int fd
    return (*env)->GetIntField(env, fdo, IO_fd_fdID);
}

用 Java 伪代码等价表达就是:

java

ini 复制代码
FileDescriptor fdo = this.fd;   // 第一次字段访问:对象字段
int fd = fdo.fd;                // 第二次字段访问:int 字段

这里的两个 jfieldID 分别是:

  • fos_fd:标识 FileOutputStream.fd,签名 Ljava/io/FileDescriptor;
  • IO_fd_fdID:标识 FileDescriptor.fd,签名 I

它们都在类初始化时缓存好:

c

scss 复制代码
JNIEXPORT void JNICALL
Java_java_io_FileDescriptor_initIDs(JNIEnv *env, jclass fdClass) {
    // 缓存 FileDescriptor.fd(int)和 FileDescriptor.append(boolean)
    CHECK_NULL(IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "fd", "I"));
    CHECK_NULL(IO_append_fdID = (*env)->GetFieldID(env, fdClass, "append", "Z"));
}

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_initIDs(JNIEnv *env, jclass fdClass) {
    // 缓存 FileOutputStream.fd(对象)
    fos_fd = (*env)->GetFieldID(env, fdClass, "fd", "Ljava/io/FileDescriptor;");
}

四、HotSpot 内部:GetFieldID 返回的到底是什么

GetFieldID 进入 HotSpot 后执行的是 jni_GetFieldID:

c

scss 复制代码
JNI_ENTRY(jfieldID, jni_GetFieldID(JNIEnv *env, jclass clazz,
          const char *name, const char *sig))
  jfieldID ret = 0;

  // 从 jclass 拿到对应的 Klass 元数据
  Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz));

  // 字段名和签名必须已经在符号表里,否则字段不存在
  TempNewSymbol fieldname = SymbolTable::probe(name, (int)strlen(name));
  TempNewSymbol signame = SymbolTable::probe(sig, (int)strlen(sig));
  if (fieldname == NULL || signame == NULL) {
    THROW_MSG_0(vmSymbols::java_lang_NoSuchFieldError(), ...);
  }

  // 关键:对外发放字段 ID 之前,确保类已经初始化
  k->initialize(CHECK_NULL);

  fieldDescriptor fd;
  if (!k->is_instance_klass() ||
      !InstanceKlass::cast(k)->find_field(fieldname, signame, false, &fd)) {
    THROW_MSG_0(vmSymbols::java_lang_NoSuchFieldError(), ...);
  }

  // A jfieldID for a non-static field is simply the offset of the field
  // within the instanceOop
  ret = jfieldIDWorkaround::to_instance_jfieldID(k, fd.offset());
  return ret;
JNI_END

关键结论 :注释里说得很清楚------非静态字段的 jfieldID 本质上就是字段在实例对象(oop)里的偏移量 。Klass* k 只在构造时用于查字段、算偏移,返回的 jfieldID 里不保存 Klass 指针。

具体编码方式:

c

scss 复制代码
static jfieldID to_instance_jfieldID(Klass* k, int offset) {
    // 把 offset 左移,低位腾出来放"实例字段"标志位
    intptr_t as_uint = ((offset & large_offset_mask) << offset_shift)
                       | instance_mask_in_place;
    if (VerifyJNIFields) {
        // 校验模式:把 klass 的哈希编进高位,后续用于归属校验
        as_uint |= encode_klass_hash(k, offset);
    }
    jfieldID result = (jfieldID) as_uint;   // 整数强转成指针
    ...
    assert(raw_instance_offset(result) == (offset & large_offset_mask),
           "extract right offset");
    return result;
}

解码则是它的逆操作:

c

arduino 复制代码
static intptr_t raw_instance_offset(jfieldID id) {
    // 右移,丢掉低位标志位,offset 归位
    uintptr_t result = (uintptr_t) id >> address_shift;
    if (VerifyJNIFields && is_checked_jfieldID(id)) {
        // 切掉高位编进去的 klass hash
        result &= small_offset_mask;
    }
    return (intptr_t)result;
}

static intptr_t from_instance_jfieldID(Klass* k, jfieldID id) {
#ifndef ASSERT
    if (VerifyJNIFields)
#endif
    {
        // 校验这个 id 是否真的属于类 k
        verify_instance_jfieldID(k, id);
    }
    return raw_instance_offset(id);
}

offset_shift 与 address_shift 是同一个值,这样"左移编码、右移解码"才互为逆操作。


五、GetObjectField:按偏移取值

getFD 里第一步 GetObjectField 进入 HotSpot:

c

ini 复制代码
JNI_ENTRY(jobject, jni_GetObjectField(JNIEnv *env, jobject obj, jfieldID fieldID))
  // jobject → oop(堆里的对象指针)
  oop o = JNIHandles::resolve_non_null(obj);
  Klass* k = o->klass();
  // ★ 解码 jfieldID,得到字段偏移
  int offset = jfieldIDWorkaround::from_instance_jfieldID(k, fieldID);
  if (JvmtiExport::should_post_field_access()) {
    o = JvmtiExport::jni_GetField_probe(thread, obj, o, k, fieldID, false);
  }
  // 按 oop + offset 读字段,无需再回查类元数据
  oop loaded_obj = HeapAccess<ON_UNKNOWN_OOP_REF>::oop_load_at(o, offset);
  jobject ret = JNIHandles::make_local(THREAD, loaded_obj);
  return ret;
JNI_END

路径非常直接:jobject → oop → 解码 jfieldID 得 offset → oop_load_at(o, offset) 。

读 int 字段同理,只是把最后的 oop_load_at 换成 int 类型读,并返回 jint。


六、fd 是在哪里被写进去的

前面读到 fd 的前提是它已经被赋值。FileDescriptor 构造函数里 fd = -1 只是初始占位,真正的写入点在 FileOutputStream.open0 → fileOpen:

c

scss 复制代码
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_open0(JNIEnv *env, jobject this,
                                    jstring path, jboolean append) {
    // 传入 fos_fd,让 fileOpen 知道去哪找 FileDescriptor 对象
    fileOpen(env, this, path, fos_fd,
             O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC));
}

void
fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
    WITH_PLATFORM_STRING(env, path, ps) {
        FD fd;

#if defined(__linux__) || defined(_ALLBSD_SOURCE)
        // Linux 内核不接受结尾的斜杠,先裁掉
        char *p = (char *)ps + strlen(ps) - 1;
        while ((p > ps) && (*p == '/'))
            *p-- = '\0';
#endif
        // 调用 OS open(),返回真实文件描述符
        fd = handleOpen(ps, flags, 0666);
        if (fd != -1) {
            jobject fdobj;
            jboolean append;
            // 通过 fos_fd 从 this 里取出 FileDescriptor 对象
            fdobj = (*env)->GetObjectField(env, this, fid);
            if (fdobj != NULL) {
                // ★ 关键:把真实 fd 写回 FileDescriptor.fd 字段
                (*env)->SetIntField(env, fdobj, IO_fd_fdID, fd);
                // 记录是否为追加模式
                append = (flags & O_APPEND) == 0 ? JNI_FALSE : JNI_TRUE;
                (*env)->SetBooleanField(env, fdobj, IO_append_fdID, append);
            }
        } else {
            throwFileNotFoundException(env, path);
        }
    } END_PLATFORM_STRING(env, ps);
}

SetIntField 在 HotSpot 里同样走"解码 jfieldID → 得 offset → 按偏移写"的机制:

c

ini 复制代码
void jni_SetIntField(JNIEnv *env, jobject obj, jfieldID fieldID, jint value) {
    JavaThread* thread = JavaThread::thread_from_jni_environment(env);
    ThreadInVMfromNative __tiv(thread);
    HandleMarkCleaner __hm(thread);
    oop o = JNIHandles::resolve_non_null(obj);
    Klass* k = o->klass();
    // 解码 jfieldID 得到偏移
    int offset = jfieldIDWorkaround::from_instance_jfieldID(k, fieldID);
    if (JvmtiExport::should_post_field_modification()) {
        jvalue field_value;
        field_value.i = value;
        o = JvmtiExport::jni_SetField_probe(thread, obj, o, k, fieldID, false,
                                             JVM_SIGNATURE_INT, &field_value);
    }
    // 按 oop + offset 直接写
    o->int_field_put(offset, value);
}

七、完整调用链一图流

text

css 复制代码
Java: new FileOutputStream(file)
   │
   ├─ this.fd = new FileDescriptor()          // fd = -1(占位)
   │
   ├─ open0(name, append) ──► Java_java_io_FileOutputStream_open0
   │                              │
   │                              └─ fileOpen(env, this, path, fos_fd, flags)
   │                                    │
   │                                    ├─ handleOpen(ps, flags, 0666) → real_fd
   │                                    ├─ GetObjectField(this, fos_fd) → FileDescriptor
   │                                    └─ SetIntField(fdobj, IO_fd_fdID, real_fd)
   │                                          └─ jni_SetIntField
   │                                                ├─ from_instance_jfieldID → offset
   │                                                └─ o->int_field_put(offset, value)
   │
   └─ 此后 fd 不再是 -1

之后 Java: fos.write(bytes)
   │
   └─ writeBytes(...) ──► Java_java_io_FileOutputStream_writeBytes
                              │
                              └─ writeBytes(..., fos_fd)
                                    │
                                    └─ 循环:
                                        getFD(env, this, fos_fd)
                                          ├─ GetObjectField(this, fos_fd) → FileDescriptor
                                          │       └─ jni_GetObjectField
                                          │             ├─ from_instance_jfieldID → offset
                                          │             └─ oop_load_at(o, offset)
                                          └─ GetIntField(fdo, IO_fd_fdID) → int fd
                                                │
                                                └─ IO_Write(fd, ...) / IO_Append(fd, ...)
                                                      └─ write(2) / pwrite(2) 系统调用

八、总结

概念 本质
fos_fd jfieldID,标识 FileOutputStream.fd(对象字段,签名 Ljava/io/FileDescriptor;)
IO_fd_fdID jfieldID,标识 FileDescriptor.fd(int 字段,签名 I)
IO_append_fdID jfieldID,标识 FileDescriptor.append(boolean 字段,签名 Z)
jfieldID 在 HotSpot 一个伪装成指针的整数,编码了字段偏移量(+可选 klass hash)
编码 / 解码 to_instance_jfieldID ↔ raw_instance_offset / from_instance_jfieldID
fd 赋值点 fileOpen 成功 handleOpen 之后,通过 SetIntField(..., IO_fd_fdID, fd) 写回
读字段机制 解码 jfieldID 得 offset → oop_load_at(o, offset)
写字段机制 解码 jfieldID 得 offset → int_field_put(offset, value)

把握住两个关键点,这条链路就不再神秘:

  1. jfieldID 不是对象,也不是值,而是"字段偏移量的编码包" 。fos_fd、IO_fd_fdID 都是这种包。
  2. fd 的生命周期 :FileDescriptor() 构造时为 -1;open0 成功后由 native 层通过 SetIntField 写回真实值;writeBytes 每轮循环都重新读一次,用于感知流是否被关闭。

理解了这套机制,再去看 SocketInputStream、RandomAccessFile 等类似的 JNI 代码,就能一眼看出它们是怎么从 Java 对象里把底层句柄"挖"出来交给系统调用的了。

##源码

scss 复制代码
JNIEXPORT void JNICALL
Java_java_io_FileDescriptor_initIDs(JNIEnv *env, jclass fdClass) {
    CHECK_NULL(IO_fd_fdID = (*env)->GetFieldID(env, fdClass, "fd", "I"));
    CHECK_NULL(IO_append_fdID = (*env)->GetFieldID(env, fdClass, "append", "Z"));
}

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_initIDs(JNIEnv *env, jclass fdClass) {
    fos_fd = (*env)->GetFieldID(env, fdClass, "fd", "Ljava/io/FileDescriptor;");
}
static jfieldID to_instance_jfieldID(Klass* k, int offset) {
    intptr_t as_uint = ((offset & large_offset_mask) << offset_shift) | instance_mask_in_place;
    if (VerifyJNIFields) {
      as_uint |= encode_klass_hash(k, offset);
    }
    jfieldID result = (jfieldID) as_uint;
#ifndef ASSERT
    // always verify in debug mode; switchable in anything else
    if (VerifyJNIFields)
#endif // ASSERT
    {
      verify_instance_jfieldID(k, result);
    }
    assert(raw_instance_offset(result) == (offset & large_offset_mask), "extract right offset");
    return result;
  }
DT_RETURN_MARK_DECL(GetFieldID, jfieldID
                    , HOTSPOT_JNI_GETFIELDID_RETURN((uintptr_t)_ret_ref));

JNI_ENTRY(jfieldID, jni_GetFieldID(JNIEnv *env, jclass clazz,
          const char *name, const char *sig))
  HOTSPOT_JNI_GETFIELDID_ENTRY(env, clazz, (char *) name, (char *) sig);
  jfieldID ret = 0;
  DT_RETURN_MARK(GetFieldID, jfieldID, (const jfieldID&)ret);

  Klass* k = java_lang_Class::as_Klass(JNIHandles::resolve_non_null(clazz));

  // The class should have been loaded (we have an instance of the class
  // passed in) so the field and signature should already be in the symbol
  // table.  If they're not there, the field doesn't exist.
  TempNewSymbol fieldname = SymbolTable::probe(name, (int)strlen(name));
  TempNewSymbol signame = SymbolTable::probe(sig, (int)strlen(sig));
  if (fieldname == NULL || signame == NULL) {
    ResourceMark rm;
    THROW_MSG_0(vmSymbols::java_lang_NoSuchFieldError(), err_msg("%s.%s %s", k->external_name(), name, sig));
  }

  // Make sure class is initialized before handing id's out to fields
  k->initialize(CHECK_NULL);

  fieldDescriptor fd;
  if (!k->is_instance_klass() ||
      !InstanceKlass::cast(k)->find_field(fieldname, signame, false, &fd)) {
    ResourceMark rm;
    THROW_MSG_0(vmSymbols::java_lang_NoSuchFieldError(), err_msg("%s.%s %s", k->external_name(), name, sig));
  }

  // A jfieldID for a non-static field is simply the offset of the field within the instanceOop
  // It may also have hash bits for k, if VerifyJNIFields is turned on.
  ret = jfieldIDWorkaround::to_instance_jfieldID(k, fd.offset());
  return ret;
JNI_END

static jfieldID to_instance_jfieldID(Klass* k, int offset) {
    intptr_t as_uint = ((offset & large_offset_mask) << offset_shift) | instance_mask_in_place;
    if (VerifyJNIFields) {
      as_uint |= encode_klass_hash(k, offset);
    }
    jfieldID result = (jfieldID) as_uint;
#ifndef ASSERT
    // always verify in debug mode; switchable in anything else
    if (VerifyJNIFields)
#endif // ASSERT
    {
      verify_instance_jfieldID(k, result);
    }
    assert(raw_instance_offset(result) == (offset & large_offset_mask), "extract right offset");
    return result;
  }
static intptr_t from_instance_jfieldID(Klass* k, jfieldID id) {
#ifndef ASSERT
    // always verify in debug mode; switchable in anything else
    if (VerifyJNIFields)
#endif // ASSERT
    {
      verify_instance_jfieldID(k, id);
    }
    return raw_instance_offset(id);
  }
  
static intptr_t raw_instance_offset(jfieldID id) {
    uintptr_t result = (uintptr_t) id >> address_shift;
    if (VerifyJNIFields && is_checked_jfieldID(id)) {
      result &= small_offset_mask;  // cut off the hash bits
    }
    return (intptr_t)result;
  }
  
/**************************************************************
 * Output stream
 */

JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_open0(JNIEnv *env, jobject this,
                                    jstring path, jboolean append) {
    fileOpen(env, this, path, fos_fd,
             O_WRONLY | O_CREAT | (append ? O_APPEND : O_TRUNC));
}

void
fileOpen(JNIEnv *env, jobject this, jstring path, jfieldID fid, int flags)
{
    WITH_PLATFORM_STRING(env, path, ps) {
        FD fd;

#if defined(__linux__) || defined(_ALLBSD_SOURCE)
        /* Remove trailing slashes, since the kernel won't */
        char *p = (char *)ps + strlen(ps) - 1;
        while ((p > ps) && (*p == '/'))
            *p-- = '\0';
#endif
        fd = handleOpen(ps, flags, 0666);
        if (fd != -1) {
            jobject fdobj;
            jboolean append;
            fdobj = (*env)->GetObjectField(env, this, fid);
            if (fdobj != NULL) {
                // Set FD
                (*env)->SetIntField(env, fdobj, IO_fd_fdID, fd);
                append = (flags & O_APPEND) == 0 ? JNI_FALSE : JNI_TRUE;
                (*env)->SetBooleanField(env, fdobj, IO_append_fdID, append);
            }
        } else {
            throwFileNotFoundException(env, path);
        }
    } END_PLATFORM_STRING(env, ps);
}

JNI_ENTRY(jobject, jni_GetObjectField(JNIEnv *env, jobject obj, jfieldID fieldID))
  HOTSPOT_JNI_GETOBJECTFIELD_ENTRY(env, obj, (uintptr_t) fieldID);
  oop o = JNIHandles::resolve_non_null(obj);
  Klass* k = o->klass();
  int offset = jfieldIDWorkaround::from_instance_jfieldID(k, fieldID);
  // Keep JVMTI addition small and only check enabled flag here.
  // jni_GetField_probe() assumes that is okay to create handles.
  if (JvmtiExport::should_post_field_access()) {
    o = JvmtiExport::jni_GetField_probe(thread, obj, o, k, fieldID, false);
  }
  oop loaded_obj = HeapAccess<ON_UNKNOWN_OOP_REF>::oop_load_at(o, offset);
  jobject ret = JNIHandles::make_local(THREAD, loaded_obj);
  HOTSPOT_JNI_GETOBJECTFIELD_RETURN(ret);
  return ret;
JNI_END

DEFINE_SETFIELD(jint,     int,    Int,     JVM_SIGNATURE_INT, i
                , HOTSPOT_JNI_SETINTFIELD_ENTRY(env, obj, (uintptr_t)fieldID, value),
                HOTSPOT_JNI_SETINTFIELD_RETURN())

 extern "C" { void  jni_SetIntField(JNIEnv *env, jobject obj, jfieldID fieldID, jint value) { JavaThread* thread=JavaThread::thread_from_jni_environment(env); ; ; ThreadInVMfromNative __tiv(thread);  HandleMarkCleaner __hm(thread); JavaThread* __the_thread__ = thread; os::verify_stack_alignment(); ; oop o = JNIHandles::resolve_non_null(obj); Klass* k = o->klass(); int offset = jfieldIDWorkaround::from_instance_jfieldID(k, fieldID); if (JvmtiExport::should_post_field_modification()) { jvalue field_value; field_value.i = value; o = JvmtiExport::jni_SetField_probe(thread, obj, o, k, fieldID, false, JVM_SIGNATURE_INT, (jvalue *)&field_value); } if (JVM_SIGNATURE_INT == JVM_SIGNATURE_BOOLEAN) { value = ((jboolean)value) & 1; } o->int_field_put(offset, value); ; } }                 
 
JNIEXPORT void JNICALL
Java_java_io_FileOutputStream_writeBytes(JNIEnv *env,
    jobject this, jbyteArray bytes, jint off, jint len, jboolean append) {
    writeBytes(env, this, bytes, off, len, append, fos_fd);
}

void
writeBytes(JNIEnv *env, jobject this, jbyteArray bytes,
           jint off, jint len, jboolean append, jfieldID fid)
{
    jint n;
    char stackBuf[BUF_SIZE];
    char *buf = NULL;
    FD fd;

    if (IS_NULL(bytes)) {
        JNU_ThrowNullPointerException(env, NULL);
        return;
    }

    if (outOfBounds(env, off, len, bytes)) {
        JNU_ThrowByName(env, "java/lang/IndexOutOfBoundsException", NULL);
        return;
    }

    if (len == 0) {
        return;
    } else if (len > BUF_SIZE) {
        buf = malloc(len);
        if (buf == NULL) {
            JNU_ThrowOutOfMemoryError(env, NULL);
            return;
        }
    } else {
        buf = stackBuf;
    }

    (*env)->GetByteArrayRegion(env, bytes, off, len, (jbyte *)buf);

    if (!(*env)->ExceptionOccurred(env)) {
        off = 0;
        while (len > 0) {
            fd = getFD(env, this, fid);
            if (fd == -1) {
                JNU_ThrowIOException(env, "Stream Closed");
                break;
            }
            if (append == JNI_TRUE) {
                n = IO_Append(fd, buf+off, len);
            } else {
                n = IO_Write(fd, buf+off, len);
            }
            if (n == -1) {
                JNU_ThrowIOExceptionWithLastError(env, "Write error");
                break;
            }
            off += n;
            len -= n;
        }
    }
    if (buf != stackBuf) {
        free(buf);
    }
} 

FD getFD(JNIEnv *env, jobject obj, jfieldID fid) {
  jobject fdo = (*env)->GetObjectField(env, obj, fid);
  if (fdo == NULL) {
    return -1;
  }
  return (*env)->GetIntField(env, fdo, IO_fd_fdID);
}
相关推荐
水饺编程1 小时前
第2章:Linux 基础,whoami 命令
linux·c语言·汇编
鱼很腾apoc2 小时前
【MySQL】 第2期 查询与函数系统详解:从基础 CRUD 到复合多表查询
linux·数据库·学习·mysql·dml
IMPYLH2 小时前
Linux 运维基础:从入门到实战的完整指南
linux·运维·php
Lsetea2 小时前
curl走HTTPS代理报SSL证书错误:proxy-cacert与cacert双层信任排查
linux·https·ssl证书·curl·代理服务器
星恒随风2 小时前
Linux进程间通信(二):深入理解System V共享内存、消息队列、信号量与内核IPC资源管理
linux·运维·服务器·笔记·学习·状态模式
用户094248568032 小时前
第36章:JVMTI、Java Agent与运行时扩展点
java·jvm
yunwei372 小时前
eBPF 实例教程:BPF Arena 零拷贝共享内存
linux·后端·性能优化
IMPYLH2 小时前
Linux 文件检索:find、locate、grep 与 which 实战指南
linux·运维·服务器
脚踏实地,坚持不懈!3 小时前
Linux 内存回收机制:水位线、kswapd 与 Direct Reclaim
linux·jvm·算法