一、为什么需要 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);
}
}
这里有两点值得注意:
- 热路径循环里每轮都重新取 fd 。这样即使写的过程中流被 close,也会立刻发现
fd == -1并报 "Stream Closed"。 - 部分写处理 :
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) |
把握住两个关键点,这条链路就不再神秘:
jfieldID不是对象,也不是值,而是"字段偏移量的编码包" 。fos_fd、IO_fd_fdID都是这种包。- 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);
}