前 言 翻 译 初 衷, 记 录 JNI 编 程 经 验 以 备 后 查, 并 奢 望 以 JNI 为 蓝 本, 写 一 本 更 深 入 的 关 于 虚 拟 机 的 书 真 做 起 来, 才 发 现 以 现 有 水 平 只 能 仰 望 这 个 目 标, 要 达 到 它, 还 需 要 几 年 积 累 本 书 没 有 采 用 逐 字 逐 句 的 翻 译, 更 多 采 用 意 译, 请 大 家 在 阅 读 时 多 参 考 原 著 ; 对 于 书 中 夹 杂 的 评 论, 如 有 伤 观 感, 请 大 家 见 谅 现 在 有 无 数 优 秀 的 开 源 项 目, 以 前 高 深 莫 测 的 技 术 ( 虚 拟 机 编 译 器 操 作 系 统 协 议 栈 和 IDE...), 我 们 终 于 有 机 会 一 探 究 竟 了, 真 令 人 兴 奋 我 们 学 习, 我 们 参 与, 希 望 有 一 天 我 们 中 国 人 也 能 创 一 门 牛 技 术 感 谢 Die...ken 的 审 稿, 他 严 谨 和 认 真 的 态 度, 深 感 敬 佩 ; 哥 们 儿 祝 你 : 天 天 开 心, 早 结 连 理 感 谢 老 婆 老 婆 读 书 时, 看 见 别 人 写 的 书 总 会 感 谢 太 太 云 云, 煞 是 羡 慕, 总 追 问 : 你 什 么 时 候 写 书 感 谢 我? 难! 翻 译 都 这 么 费 劲, 写 书 就 不 知 猴 年 马 月 了, 在 这 儿 感 谢 一 下, 糊 弄 糊 弄 得 了 do.chuan@gmail.com
Preface 本 书 涵 盖 了 Java Native Interface(JNI) 的 内 容, 将 探 讨 以 下 问 题 : 在 一 个 Java 项 目 中 集 成 一 个 C/C++ 库 在 一 个 用 C/C++ 开 发 的 项 目 中, 嵌 入 JavaVM 实 现 Java VM 语 言 互 操 作 性 问 题, 特 别 是 互 操 作 过 程 中 的 垃 圾 回 收 (GC, garbage collection) 和 并 发 编 程 (multithreading) 译 注 : JNI(Java Native Interface) 是 SUN 定 义 的 一 套 标 准 接 口, 如 Dalvik, Apache Harmony 项 目... 等 Java 虚 拟 机, 都 会 实 现 JNI 接 口, 供 本 地 (C/C++) 应 用 与 Java VM 互 调 JNI: 可 以 供 Java 代 码 调 用 本 地 代 码, 本 地 代 码 也 可 以 调 用 Java 代 码, 即 上 文 列 出 的 第 4 条 内 容 : 语 言 互 操 作 ; 所 以, 这 是 一 套 完 善 而 功 能 强 大 的 接 口 可 能 有 朋 友 听 说 过 KNI, 那 是 J2ME VM(CLDC) 中 定 义 的 一 套 东 西, 不 如 JNI 强 大 此 外, 因 为 C/C++ 在 系 统 编 程 领 域 的 地 位, 只 要 打 通 了 与 C/C++ 的 接 口, 就 等 于 是 天 堑 变 通 途 首 先, 通 过 本 书, 你 会 很 容 易 的 掌 握 JNI 开 发, 并 能 了 解 到 方 方 面 面 的 关 于 JNI 的 知 识 本 书 详 尽 的 叙 述, 会 带 给 你 你 很 多 如 何 高 效 使 用 JNI 的 启 示 JNI 自 1997 年 初 发 布 以 来, Sun 的 工 程 师 们 和 Java 社 区 使 用 JNI 的 经 验 造 就 了 本 书 第 二, 本 书 介 绍 了 JNI 的 设 计 原 理 这 些 原 理, 不 仅 会 使 学 术 界 感 兴 趣, 也 是 高 效 使 用 JNI 的 前 提 第 三, 本 书 的 某 些 部 分 是 Java 2 平 台 规 范 的 最 终 版 本 JNI 程 序 员 可 以 此 书 作 为 规 范 的 参 考 手 册,Java 虚 拟 机 实 现 者 必 须 遵 循 规 范, 以 保 证 各 平 台 实 现 的 一 致 性 (... 几 段 不 重 要, 未 翻 译...)
CHAPTER 1 Introduction JNI 是 Java 平 台 中 的 一 个 强 大 特 性 应 用 程 序 可 以 通 过 JNI 把 C/C++ 代 码 集 成 进 Java 程 序 中 通 过 JNI, 开 发 者 在 利 用 Java 平 台 强 大 功 能 的 同 时, 又 不 必 放 弃 对 原 有 代 码 的 投 资 ; 因 为 JNI 是 Java 平 台 定 义 的 规 范 接 口, 当 程 序 员 向 Java 代 码 集 成 本 地 库 时, 只 要 在 一 个 平 台 中 解 决 了 语 言 互 操 作 问 题, 就 可 以 把 该 解 决 方 案 比 较 容 易 的 移 植 到 其 他 Java 平 台 中 译 注 : 比 如 为 Dalvik 添 加 了 一 个 本 地 库, 也 可 以 把 这 个 本 地 库 很 容 易 的 移 植 到 J2SE 和 Apache Harmony 中, 因 为 在 Java 与 C/C++ 互 操 作 方 面, 大 家 都 遵 循 一 套 API 接 口, 即 JNI 本 书 由 下 列 三 个 部 分 组 成 : Chapter 2 通 过 简 单 示 例 介 绍 了 JNI 编 程 Chapter 3-10, 对 JNI 各 方 面 特 性 和 功 能 做 介 绍, 并 给 出 示 例 ( 译 者 : 重 要 ) Chapters 11-13, 罗 列 JNI 所 有 的 数 据 类 型 的 定 义 (... 几 段 不 重 要, 未 翻 译...) 1.1 The Java Platform and Host Environment 因 本 书 覆 盖 了 Java 和 本 地 (C, C++, etc...) 编 程 语 言, 让 我 们 首 先 理 一 理 这 些 编 程 语 言 的 适 用 领 域 Java 平 台 (Java Platform) 的 组 成 :Java VM 和 Java API. Java 应 用 程 序 使 用 Java 语 言 开 发, 然 后 编 译 成 与 平 台 无 关 的 字 节 码 (.class 文 件 ) Java API 由 一 组 预 定 义 的 类 组 成 任 何 组 织 实 现 的 Java 平 台 都 要 支 持 :Java 编 程 语 言, 虚 拟 机, 和 API( 译 者 :Sun 对 Java 语 言 虚 拟 机 和 API 有 明 确 规 范 ) 平 台 环 境 : 操 作 系 统, 一 组 本 机 库, 和 CPU 指 令 集 本 地 应 用 程 序, 通 常 依 赖 于 一 个 特 定 的 平 台 环 境, 用 C C++ 等 语 言 开 发, 并 被 编 译 成 平 台 相 关 的 二 进 制 指 令, 目 标 二 进 制 代 码 在 不 同 OS 间 一 般 不 具 有 可 移 植 性
Java 平 台 (Java VM 和 Java API) 一 般 在 某 个 平 台 下 开 发 比 如,Sun 的 Java Runtime Environment(JRE) 支 持 类 Unix 和 Windows 平 台. Java 平 台 做 的 所 有 努 力, 都 为 了 使 程 序 更 具 可 移 植 性 1.2 Role of the JNI 当 Java 平 台 部 署 到 本 地 系 统 中, 有 必 要 做 到 让 Java 程 序 与 本 地 代 码 协 同 工 作 部 分 是 由 于 遗 留 代 码 ( 保 护 原 有 的 投 资 ) 的 问 题 ( 一 些 效 率 敏 感 的 代 码 用 C 实 现, 但 现 在 JavaVM 的 执 行 效 率 完 全 可 信 赖 ), 工 程 师 们 很 早 就 开 始 以 C/C++ 为 基 础 构 建 Java 应 用, 所 以,C/C++ 代 码 将 长 时 间 的 与 Java 应 用 共 存 JNI 让 你 在 利 用 强 大 Java 平 台 的 同 时, 使 你 仍 然 可 以 用 其 他 语 言 写 程 序 作 为 JavaVM 的 一 部 分,JNI 是 一 套 双 向 的 接 口, 允 许 Java 与 本 地 代 码 间 的 互 操 作 如 图 1.1 所 示 作 为 双 向 接 口,JNI 支 持 两 种 类 型 本 地 代 码 : 本 地 库 和 本 地 应 用 用 本 地 代 码 实 现 Java 中 定 义 的 native method 接 口, 使 Java 调 用 本 地 代 码 通 过 JNI 你 可 以 把 Java VM 嵌 到 一 个 应 用 程 序 中, 此 时 Java 平 台 作 为 应 用 程 序 的 增 强, 使 其 可 以 调 用 Java 类 库 比 如, 在 浏 览 器 中 运 行 Applet, 当 浏 览 器 遇 到 "Applet" 标 签, 浏 览 器 就 会 把 标 签 中 的 内 容 交 给 Java VM 解 释 执 行, 这 个 实 现, 就 是 典 型 的 把 JavaVM 嵌 入 Browser 中 译 注 : JNI 不 只 是 一 套 接 口, 还 是 一 套 使 用 规 则 Java 语 言 有 "native" 关 键 字, 声 明 哪 些 方 法
是 用 本 地 代 码 实 现 的. 翻 译 的 时 候, 对 于 "native method", 根 据 上 下 文 意 思 做 了 不 同 处 理, 当 native method 指 代 Java 中 用 "native" 关 键 字 修 饰 的 那 些 方 法 时, 不 翻 译 ; 而 当 代 码 用 C/C++ 实 现 的 部 分 翻 译 成 了 本 地 代 码 上 述, 在 应 用 中 嵌 入 Java VM 的 方 法, 是 用 最 少 的 力 量, 为 应 用 做 最 强 扩 展 的 不 二 选 择, 这 时 你 的 应 用 程 序 可 以 自 由 使 用 Java API 的 所 有 功 能 ; 大 家 有 兴 趣 可 以 读 一 读 浏 览 器 是 怎 么 扩 展 Applet 的, 或 者 读 一 读 Palm WebOS 的 东 西 译 者 最 近 一 年 都 在 做 这 件 事, 对 这 个 强 大 的 功 能, 印 象 特 别 深 刻. 我 们 整 个 小 组 做 了 两 个 平 台 的 扩 展, 设 计 编 码 测 试 和 debug 用 了 近 一 年 半 时 间, 代 码 量 在 14000 行 左 右, 做 完 扩 展 后, 平 台 功 能 空 前 增 强 我 感 觉 做 软 件, 难 得 不 在 编 码, 难 在 开 始 的 设 计 和 后 期 的 测 试 调 试 和 优 化, 并 最 终 商 用, 这 就 要 求 最 终 产 品 是 一 个 强 大 而 稳 定 的 平 台, 达 到 此 目 标 是 个 旷 日 持 久 的 事. 看 看 Java,Windows,Linux,Qt,WebKit 发 展 了 多 少 年? 向 所 有 软 件 工 程 师 致 敬! 1.3 Implications of Using the JNI 请 记 住, 当 Java 程 序 集 成 了 本 地 代 码, 它 将 丢 掉 Java 的 一 些 好 处 首 先, 脱 离 Java 后, 可 移 植 性 问 题 你 要 自 己 解 决, 且 需 重 新 在 其 他 平 台 编 译 链 接 本 地 库 第 二, 要 小 心 处 理 JNI 编 程 中 各 方 面 问 题 和 来 自 C/C++ 语 言 本 身 的 细 节 性 问 题, 处 理 不 当, 应 用 将 崩 溃 一 般 性 原 则 : 做 好 应 用 程 序 架 构, 使 native methods 定 义 在 尽 可 能 少 的 几 个 类 里 译 注 : 学 习 JNI 编 程 是 个 漫 长 的 实 践 过 程, 会 碰 到 无 数 问 题 用 C/C++ 编 程, 常 见 问 题 有 内 存 泄 露, 指 针 越 界..., 此 外 使 用 了 JNI, 还 要 面 对 JavaVM 的 问 题 : 在 本 地 代 码 中 new 一 个 Java 对 象 后 期 望 在 本 地 代 码 中 维 护 此 对 象 的 引 用, 如 何 避 免 被 GC? Java 面 向 对 象 语 言 的 封 装 性 被 破 坏 了,Java 类 中 任 何 方 法 和 属 性 对 JNI 都 是 可 见 的, 不 管 它 是 public 的, 还 是 private/protected/package 的 对 LocalRef/GlobalRef 管 理 不 善, 会 引 发 Table Overflow Exception, 导 致 应 用 崩 溃 从 JNI 调 用 Java 的 过 程 不 是 很 直 观, 往 往 几 行 Java 代 码 能 搞 定 的 事 情, 用 JNI 实 现 却 要 几 百 行 虽 然, 有 这 样 多 问 题, 逃 避 不 了, 你 就 认 了 吧 经 过 一 段 时 间 的 实 践, 当 你 能 熟 练 处 理 这 些 问 题 时, 就 会, 眉 头 一 皱, 文 思 泉 涌, 指 尖 飞 舞, 瞬 间 几 百 行 代 码 诞 生 了, 一 个 make 全 部 编 译 通 过, 这 时 的 你 肯 定 已 经 对 JNI 上 瘾 了... 1.4 When to Use the JNI 当 你 准 备 在 项 目 中 使 用 JNI 之 前, 请 先 考 虑 一 下 是 否 有 其 他 更 合 适 的 方 案 上 节 有 关 JNI 缺 点 的 介 绍, 应 该 引 起 你 足 够 的 重 视 这 里 介 绍 几 个 不 通 过 JNI 与 其 他 语 言 交 互 的 技 术 :
IPC 或 者 通 过 TCP/IP 网 络 方 案 ( Android ASE) 数 据 库 方 面, 可 以 使 用 JDBC 使 用 Java 的 分 布 式 对 象 技 术 : Java IDL API 译 注 : IPC 与 TCP/IP 是 常 用 的 基 于 协 议 的 信 息 交 换 方 案. 可 以 参 考 Android 上 的 Binder 和 ASE(Android Script Environment) 一 典 型 的 解 决 方 案 是,Java 程 序 与 本 地 代 码 分 别 运 行 在 不 同 的 进 程 中. 采 用 进 程 分 置 最 大 的 好 处 是 : 一 个 进 程 的 崩 溃, 不 会 立 即 影 响 到 另 一 个 进 程 但 是, 把 Java 代 码 与 本 地 代 码 置 于 一 个 进 程 有 时 是 必 要 的 如 下 : Java API 可 能 不 支 某 些 平 台 相 关 的 功 能 比 如, 应 用 程 序 执 行 中 要 使 用 Java API 不 支 持 的 文 件 类 型, 而 如 果 使 用 跨 进 程 操 作 方 式, 即 繁 琐 又 低 效 避 免 进 程 间 低 效 的 数 据 拷 贝 操 作 多 进 程 的 派 生 : 耗 时 耗 资 源 ( 内 存 ) 用 本 地 代 码 或 汇 编 代 码 重 写 Java 中 低 效 方 法 总 之, 如 果 Java 必 须 与 驻 留 同 进 程 的 本 地 代 码 交 互, 请 使 用 JNI 译 注 : 写 代 码 是 技 巧 和 艺 术, 看 你 想 在 设 计 上 下 多 大 功 夫. 比 如 : Chrome, 是 多 进 程 的 典 范, 她 的 简 洁 高 效, 令 人 叹 服 1.5 Evolution of the JNI 关 于 Java 应 用 程 序 如 何 与 本 地 代 码 互 操 作 的 问 题, 在 Java 平 台 早 期 就 被 提 了 出 来. JDK1.0 包 括 了 一 套 与 本 地 代 码 交 互 的 接 口 当 时 许 多 Java 方 法 和 库 都 依 赖 本 地 方 法 实 现 ( 如 java.io, java.net) 但 是,JDK release 1.0 有 两 个 主 要 问 题 : Java 虚 拟 机 规 范 未 定 义 对 象 布 局, 本 地 代 码 访 问 对 象 的 成 员 是 通 过 访 问 C 结 构 的 成 员 实 现 的 本 地 代 码 可 以 得 到 对 象 在 内 存 中 的 地 址, 所 以, 本 地 方 法 是 GC 相 关 的 为 解 决 上 述 问 题 对 JNI 做 了 重 新 设 计, 让 这 套 接 口 在 所 有 平 台 都 容 易 得 到 支 持 虚 拟 机 实 现 者 通 过 JNI 支 持 大 量 的 本 地 代 码 工 具 开 发 商 不 用 处 理 不 同 种 类 的 本 地 接 口 所 有 JNI 开 发 者 面 对 的 是 操 作 JavaVM 的 规 范 API
JNI 的 首 次 支 持 是 在 JDK release 1.1, 但 1.1 内 部 Java 与 本 地 代 码 的 交 互 仍 然 使 用 原 始 方 式 (JDK 1.0). 但 这 种 局 面, 没 有 持 续 很 久, 在 Java 2 SDK release 1.2 中 Java 层 与 本 地 代 码 的 交 互 部 分 用 JNI 重 写 了 作 为 JavaVM 规 范 的 一 部 分,Java 层 与 本 地 代 码 的 交 互, 都 应 通 过 JNI 实 现 1.6 Example Programs 本 书 注 重 JNI 编 程, 不 涉 及 如 何 通 过 第 三 方 工 具 简 化 该 过 程 ( 译 者 : 不 重 要, 未 翻 译 ) 请 从 官 网 下 载 本 书 的 示 例 代 码 :http://java.sun.com/docs/books/jni/
CHAPTER 2 Getting Started 本 章 用 Hello World 示 例 带 你 领 略 JNI 编 程 2.1 Overview 准 备 过 程 : 1. 创 建 一 个 类 (HelloWorld.java) 2. 使 用 javac 编 译 该 类 3. 利 用 javah -jni 产 生 头 文 件 4. 用 本 地 代 码 实 现 头 文 件 中 定 义 的 方 法 5. Run
译 注 : 在 一 个 特 定 环 境 中, 写 本 地 实 现 的 过 程 是 不 同 的 ( 如 Android) javah 主 要 是 生 成 头 文 件 和 函 数 签 名 ( 每 个 方 法 和 成 员 都 有 签 名, 后 有 详 细 介 绍 ), 通 过 javah 学 习 如 何 正 确 的 写 法 注 意 : 如 上 述 HelloWorld.java, 编 译 后 的 文 件 为 HelloWorld.class, 用 $javah HelloWorld 来 产 生 头 文 件, 不 要 带 末 尾 的 ".class" 2.2 Declare the Native Method HelloWorld.java class HelloWorld private native void print(); public static void main(string[] args) new HelloWorld().print(); static System.loadLibrary("HelloWorld"); HelloWrold 类 首 先 声 明 了 一 个 private native print 方 法. static 那 几 行 是 本 地 库 在 Java 代 码 中 声 明 本 地 方 法 必 须 有 "native" 标 识 符,native 修 饰 的 方 法, 在 Java 代 码 中 只 作 为 声 明 存 在 在 调 用 本 地 方 法 前, 必 须 首 先 装 载 含 有 该 方 法 的 本 地 库. 如 HelloWorld.java 中 所 示, 置 于 static 块 中, 在 Java VM 初 始 化 一 个 类 时, 首 先 执 行 这 部 分 代 码, 这 可 保 证 调 用 本 地 方 法 前, 装 载 了 本 地 库 装 载 库 的 机 制, 后 有 介 绍 2.3 Compile the HelloWorld Class $javac HelloWorld.java 2.4 Create the Native Method Header File $javah -jni HelloWorld 译 者 :"-jni" 为 默 认 参 数, 可 有 可 无. 上 述 命 令, 生 成 HelloWorld.h 文 件. 关 键 部 分 如 下 :
JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject); 现 在, 请 先 忽 略 两 个 宏 :JNIEXPORT 和 JNICALL 你 会 发 现, 该 函 数 声 明, 接 受 两 个 参 数, 而 对 应 的 Java 代 码 对 该 函 数 的 声 明 没 有 参 数 第 一 个 参 数 是 指 向 JNIEnv 结 构 的 指 针 ; 第 二 个 参 数, 为 HelloWorld 对 象 自 身, 即 this 指 针 译 注 : JNIEnv 是 JNI 核 心 数 据 之 一, 地 位 非 常 崇 高, 所 有 对 JNI 的 调 用 都 要 通 过 此 结 构 2.5 Write the Native Method Implementation 必 须 根 据 javah 生 成 的 本 地 函 数 声 明 实 现 函 数, 如 下 : #include <jni.h> #include <stdio.h> #include "HelloWorld.h" JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jobject obj) printf("hello World!\n"); return; 请 注 意 :"jni.h" 文 件 必 须 被 包 含, 该 文 件 定 义 了 JNI 所 有 的 函 数 声 明 和 数 据 类 型 2.6 Compile the C Source and Create a Native Library 请 注 意, 生 成 的 本 地 库 的 名 字, 必 须 与 System.loadLibrary("HelloWorld"); 待 装 载 库 的 名 字 相 同 Solaris: $cc -G -I/java/include -I/java/include/solaris HelloWorld.c -o libhelloworld.so -G: 生 成 共 享 库 Win: $cl -Ic:\java\include -Ic:\java\include\win32 -MD -LD HelloWorld.c -FeHelloWorld.dll -MD: 保 证 与 Win32 多 线 程 C 库 连 接 ( 译 者 :Win 上 分 静 态 动 态 动 态 多 线 程...C 库 ) -LD: 生 成 动 态 链 接 库
2.7 Run the Program Solaris or Win: $java HelloWorld 输 出 : Hello World! 运 行 前, 必 须 保 证 连 接 器, 能 找 到 待 装 载 的 库, 不 然, 将 抛 如 下 异 常 : java.lang.unsatisfiedlinkerror: no HelloWorld in library path at java.lang.runtime.loadlibrary(runtime.java) at java.lang.system.loadlibrary(system.java) at HelloWorld.main(HelloWorld.java) 如,Solaris, 通 过 sh 或 ksh shell: $LD_LIBRARY_PATH=. $export LD_LIBRARY_PATH C shell: $setenv LD_LIBRARY_PATH. 在 Win 上, 请 保 证 待 装 载 库 在 当 前 位 置, 或 在 PATH 环 境 变 量 中 你 也 可 以 如 下 : java -Djava.library.path=. HelloWorld -D: 设 置 Java 平 台 的 系 统 属 性 此 时 JavaVM 可 以 在 当 前 位 置 找 到 该 库
CHAPTER 3 Basic Types, Strings, and Arrays JNI 编 程 中 常 被 提 到 的 问 题 是,Java 语 言 中 的 数 据 类 型 是 如 何 映 射 到 c/c++ 本 地 语 言 中 的 实 际 编 程 中, 向 函 数 传 参 和 函 数 返 回 值 是 很 普 遍 的 事 情 本 章 将 介 绍 这 方 面 技 术, 我 们 从 基 本 类 型 ( 如 int) 和 一 般 对 象 ( 如 String 和 Array) 开 始 介 绍. 其 他 内 容 将 放 在 下 一 章 介 绍 译 注 : JavaVM 规 范 中 称 int,char,byte 等 为 primitive types, 译 者 平 时 叫 惯 了 基 本 类 型, 所 以 翻 译 时 延 用 了 习 惯, 不 知 合 适 否 3.1 A Simple Native Method 扩 充 HelloWorld.java, 该 例 是 先 打 印 一 串 字 符, 然 后 等 待 用 户 的 输 入, 如 下 : class Prompt // native method that prints a prompt and reads a line private native String getline(string prompt); public static void main(string args[]) Prompt p = new Prompt(); String input = p.getline("type a line: "); System.out.println("User typed: " + input); static System.loadLibrary("Prompt"); Prompt.getLine 方 法 的 C 声 明 如 下 : JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject this, jstring prompt); 3.1.2 Native Method Arguments Java_Prompt_getLine 接 收 3 个 参 数 : JNIEnv 结 构 包 括 JNI 函 数 表
第 二 个 参 数 的 意 义 取 决 于 该 方 法 是 静 态 还 是 实 例 方 法 (static or an instance method) 当 本 地 方 法 作 为 一 个 实 例 方 法 时, 第 二 个 参 数 相 当 于 对 象 本 身, 即 this. 当 本 地 方 法 作 为 一 个 静 态 方 法 时, 指 向 所 在 类. 在 本 例 中,Java_Prompt_getLine 是 一 个 本 地 实 例 方 法 实 现, 所 以 jobject 指 向 对 象 本 身 译 注 : Java 语 言 中 类 与 对 象 的 联 系 与 区 别, 概 念 很 清 晰, 但 在 JNI 和 VM 中, 有 一 些 问 题 需 要 说 明, 后 有 专 门 文 章 阐 述 3.1.3 Mapping of Types 在 native method 中 声 明 的 参 数 类 型, 在 JNI 中 都 有 对 应 的 类 型. 在 Java 中 有 两 类 数 据 类 型 :primitive types, 如,int, float, char; 另 一 种 为 reference types, 如, 类, 实 例, 数 组 译 者 : 数 组, 不 管 是 对 象 数 组 还 是 基 本 类 型 数 组, 都 作 为 reference types 存 在, 并 有 专 门 的 JNI 方 法 取 数 组 中 每 个 元 素. Java 与 JNI 基 本 类 型 的 映 射 很 直 接, 如 下 : Java boolean byte char short int long float Native(jni.h) jboolean jbyte jchar jshort jint jlong jfloat
double jdouble 相 比 基 本 类 型, 对 象 类 型 的 传 递 要 复 杂 很 多 Java 层 对 象 作 为 opaque references( 指 针 ) 传 递 到 JNI 层 Opaque references 是 一 种 C 的 指 针 类 型, 它 指 向 JavaVM 内 部 数 据 结 构 使 用 这 种 指 针 的 目 的 是 : 不 希 望 JNI 用 户 了 解 JavaVM 内 部 数 据 结 构 对 Opaque reference 所 指 结 构 的 操 作, 都 要 通 过 JNI 方 法 进 行. 比 如,"java.lang.String" 对 象,JNI 层 对 应 的 类 型 为 jstring, 对 该 opaque reference 的 操 作 要 通 过 JNIEnv->GetStringUTFChars 进 行 译 注 : 一 定 要 按 这 种 原 则 编 程, 千 万 不 要 为 了 效 率 或 容 易 的 取 到 某 个 值, 绕 过 JNI, 直 接 操 作 opaque reference. JNI 是 一 套 完 善 接 口, 所 有 需 求 都 能 满 足 在 JNI 中 对 象 的 基 类 即 为 jobject. 为 方 便 起 见, 还 定 义 了 jstring,jclass, jobjectarray 等 结 构, 他 们 都 继 承 自 jobject 3.2 Accessing Strings 如 下 使 用 方 式 是 错 误 的, 因 为 jstring 不 同 于 C 中 的 char * 类 型 JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) /* ERROR: incorrect use of jstring as a char* pointer */ printf("%s", prompt);... 3.2.1 Converting to Native Strings 使 用 对 应 的 JNI 函 数 把 jstring 转 成 C/C++ 字 串 JNI 支 持 Unicode/UTF-8 字 符 编 码 互 转 Unicode 以 16-bits 值 编 码 ;UTF-8 是 一 种 以 字 节 为 单 位 变 长 格 式 的 字 符 编 码, 并 与 7-bits ASCII 码 兼 容 UTF-8 字 串 与 C 字 串 一 样, 以 NULL('\0') 做 结 束 符, 当 UTF-8 包 含 非 ASCII 码 字 符 时, 以 '\0' 做 结 束 符 的 规 则 不 变 7-bit ASCII 字 符 的 取 值 范 围 在 1-127 之 间, 这 些 字 符 的 值 域 与 UTF-8 中 相 同 当 最 高 位 被 设 置 时, 表 示 多 字 节 编 码 如 下, 调 用 GetStringUTFChars, 把 一 个 Unicode 字 串 转 成 UTF-8 格 式 字 串, 如 果 你 确 定 字 串 只 包 含 7-bit ASCII 字 符 这 个 字 串 可 以 使 用 C 库 中 的 相 关 函 数, 如 printf. 如 何 操 作 non-ascii 字 符, 后 面 有 介 绍 JNIEXPORT jstring JNICALL
Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) char buf[128]; const jbyte *str; str = (*env)->getstringutfchars(env, prompt, NULL); if (str == NULL) return NULL; /* OutOfMemoryError already thrown */ printf("%s", str); (*env)->releasestringutfchars(env, prompt, str); /* We assume here that the user does not type more than * 127 characters */ scanf("%127s", buf); return (*env)->newstringutf(env, buf); 记 得 检 测 GetStringUTFChars 的 返 回 值, 因 为 调 用 该 函 数 会 有 内 存 分 配 操 作, 失 败 后, 该 函 数 返 回 NULL, 并 抛 OutOfMemoryError 异 常 如 何 处 理 异 常, 后 面 会 有 介 绍 JNI 处 理 异 常, 不 同 于 Java 中 的 try...catch 在 JNI 中, 发 生 异 常, 不 会 改 变 代 码 执 行 轨 迹, 所 以, 当 返 回 NULL, 要 及 时 返 回, 或 马 上 处 理 异 常 3.2.2 Freeing Native String Resources 调 用 ReleaseStringUTFChars 释 放 GetStringUTFChars 中 分 配 的 内 存 (Unicode -> UTF-8 转 换 的 原 因 ) 3.2.3 Constructing New Strings 使 用 JNIEnv->NewStringUTF 构 造 java.lang.string; 如 果 此 时 没 有 足 够 的 内 存, NewStringUTF 将 抛 OutOfMemoryError 异 常, 同 时 返 回 NULL 3.2.4 Other JNI String Functions 除 了 GetStringUTFChars, ReleaseStringUTFChars, 和 NewStringUTF, JNI 还 支 持 其 他 操 作 String 的 函 数 供 使 用 GetStringChars 是 有 Java 内 部 Unicode 到 本 地 UTF-8 的 转 换 函 数, 可 以 调 用 GetStringLength, 获 得 以 Unicode 编 码 的 字 串 长 度 也 可 以 使 用 strlen 计 算 GetStringUTFChars 的 返 回 值, 得 到 字 串 长 度 const jchar * GetStringChars(JNIEnv *env, jstring str, jboolean *iscopy);
上 述 声 明 中, 有 iscopy 参 数, 当 该 值 为 JNI_TRUE, 将 返 回 str 的 一 个 拷 贝 ; 为 JNI_FALSE 将 直 接 指 向 str 的 内 容 注 意 : 当 iscopy 为 JNI_FALSE, 不 要 修 改 返 回 值, 不 然 将 改 变 java.lang.string 的 不 可 变 语 义 一 般 会 把 iscopy 设 为 NULL, 不 关 心 Java VM 对 返 回 的 指 针 是 否 直 接 指 向 java.lang.string 的 内 容 一 般 不 能 预 知 VM 是 否 会 拷 贝 java.lang.string 的 内 容, 程 序 员 应 该 假 设 GetStringChars 会 为 java.lang.string 分 配 内 存 在 JavaVM 的 实 现 中, 垃 圾 回 收 机 制 会 移 动 对 象, 并 为 对 象 重 新 配 置 内 存 一 但 java.lang.string 占 用 的 内 存 暂 时 无 法 被 GC 重 新 配 置, 将 产 生 内 存 碎 片, 过 多 的 内 存 碎 片, 会 更 频 繁 的 出 现 内 存 不 足 的 假 象 记 住 在 调 用 GetStringChars 之 后, 要 调 用 ReleaseStringChars 做 释 放, 不 管 在 调 用 GetStringChars 时 为 iscopy 赋 值 JNI_TRUE 还 是 JNI_FALSE, 因 不 同 JavaVM 实 现 的 原 因, ReleaseStringChars 可 能 释 放 内 存, 也 可 能 释 放 一 个 内 存 占 用 标 记 (iscopy 参 数 的 作 用, 从 GetStringChars 返 回 一 个 指 针, 该 指 针 直 接 指 向 String 的 内 容, 为 了 避 免 该 指 针 指 向 的 内 容 被 GC, 要 对 该 内 存 做 锁 定 标 记 ) 3.2.5 New JNI String Function in Java 2 SDK Release 1.2 为 尽 可 能 的 避 免 内 存 分 配, 返 回 指 向 java.lang.string 内 容 的 指 针,Java 2 SDK release 1.2 提 供 了 :Get/RleaseStringCritical. 这 对 函 数 有 严 格 的 使 用 原 则 当 使 用 这 对 函 数 时, 这 对 函 数 间 的 代 码 应 被 当 做 临 界 区 (critical region). 在 该 代 码 区, 不 要 调 用 任 何 会 阻 塞 当 前 线 程 和 分 配 对 象 的 JNI 函 数, 如 IO 之 类 的 操 作 上 述 原 则, 可 以 避 免 JavaVM 执 行 GC 因 为 在 执 行 Get/ReleaseStringCritical 区 的 代 码 时,GC 被 禁 用 了, 如 果 因 某 些 原 因 在 其 他 线 程 中 引 发 了 JavaVM 执 行 GC 操 作,VM 有 死 锁 的 危 险 : 当 前 线 程 A 进 入 Get/RelaseStringCritical 区, 禁 用 了 GC, 如 果 其 他 线 程 B 中 有 GC 请 求, 因 A 线 程 禁 用 了 GC, 所 以 B 线 程 被 阻 塞 了 ; 而 此 时, 如 果 B 线 程 被 阻 塞 时 已 经 获 得 了 一 个 A 线 程 执 行 后 续 工 作 时 需 要 的 锁 ; 死 锁 发 生 了 可 以 嵌 套 调 用 GetStringCritical: jchar *s1, *s2; s1 = (*env)->getstringcritical(env, jstr1); if (s1 == NULL)... /* error handling */ s2 = (*env)->getstringcritical(env, jstr2); if (s2 == NULL)
(*env)->releasestringcritical(env, jstr1, s... /* error handling */... /* use s1 and s2 */ (*env)->releasestringcritical(env, jstr1, s1); (*env)->releasestringcritical(env, jstr2, s2); GetStringCritical 因 VM 实 现 的 原 因, 会 涉 及 内 存 操 作, 所 以 我 们 需 要 检 查 返 回 指. 比 如, 对 于 java.lang.string 来 说,VM 内 部 并 不 是 连 续 存 储 的, 所 以 GetStringCritical 要 返 回 一 个 连 续 的 字 符 数 组, 必 然 要 有 内 存 操 作 为 避 免 死 锁, 此 时 应 尽 量 避 免 调 用 其 他 JNI 方 法, 只 允 许 调 用 GetStringCritical/ReleaseStringCritical,Get/ReleasePrimitiveArrayCritical 因 VM 内 部 Unicode 编 码 的 缘 故, 所 以 Get/ReleaseStringUTFCritical 这 种 涉 及 Unicode->UTF8 转 换 要 分 配 内 存 的 函 数 不 支 持 GetStringRegion/GetStringUTFRegion, 向 准 备 好 的 缓 冲 区 赋 值, 如 下 : JNIEXPORT jstring JNICALL Java_Prompt_getLine(JNIEnv *env, jobject obj, jstring prompt) /*assumethepromptstringanduserinputhaslessthan128 characters */ char outbuf[128], inbuf[128]; int len = (*env)->getstringlength(env, prompt); (*env)->getstringutfregion(env, prompt, 0, len, outbuf); printf("%s", outbuf); scanf("%s", inbuf); return (*env)->newstringutf(env, inbuf); GetStringUTFRegion 有 两 个 参 数,starting index 和 length, 这 两 个 参 数 以 Unicode 编 码 计 算. 该 函 数 会 做 边 界 检 查, 所 以 可 能 抛 出 StringIndexOutOfBoundsException 因 为 该 函 数 不 涉 及 内 存 操 作, 所 以 较 GetStringUTFChars 使 用 要 简 单 译 注 : 有 两 个 函 数 :GetStringLength/GetStringUTFLength, 前 者 是 Unicode 编 码 长 度, 后 者 是 UTF 编 码 长 度 GetStringUTFRegion 很 有 用, 因 为 你 不 能 修 改 GetStringUTFChars 返 回 值, 所 以 需 要 另 外 malloc/strcpy 之 后, 再 操 作 返 回 值, 耗 时 费 力, 不 如 直 接 使 用 GetStringUTFRegion 来 的 简 洁 高 效 3.2.6 Summary of JNI String Functions
JNI Function Description Since GetStringChars ReleaseStringChars GetStringUTFChars ReleaseStringUTFChars Obtains or releases a pointer to the contents of a string in Unicode format.may return a copy of the string. Obtains or releases a pointer to the contents of a string in UTF-8 format. May return a copy of the string. JDK1.1 JDK1.1 GetStringLength GetStringUTFLength NewString NewStringUTF GetStringCritical ReleaseStringCritical GetStringRegion SetStringRegion GetStringUTFRegion SetStringUTFRegion Returns the number of Unicode characters in the string. Returns the number of bytes needed(not including the trailing 0) to represent a string in the UTF- 8 format. Creates a java.lang.string instance that contains the same sequence of characters as the given Unicode C string. Creates a java.lang.string instance that contains the same sequence of characters as the given UTF-8 encoded C string. Obtains a pointer to the contents of a string in Unicode format. May return a copy of the string. Native code must not block between a pair of Get/ ReleaseStringCritical calls. Copies the contents of a string to or from a preallocated C buffer in the Unicode format. Copies the content of a string to or from a preallocated C buffer in the UTF-8 format. JDK1.1 JDK1.1 JDK1.1 JDK1.1 Java 2 SDK1.2 Java 2 SDK1.2 Java 2 SDK1.2 3.2.7 Choosing among the String Functions 该 表 给 出 了 选 择 字 符 串 函 数 的 策 略 :
如 果 你 使 用 JDK 1.1 或 JDK 1.2, 你 只 能 使 用 Get/ReleaseStringChars 和 Get/ReleaseStringUTFChars 对 于 小 尺 寸 字 串 的 操 作, 首 选 Get/SetStringRegion 和 Get/SetStringUTFRegion, 因 为 栈 上 空 间 分 配, 开 销 要 小 的 多 ; 而 且 没 有 内 存 分 配, 就 不 会 有 out-of-memory exception 如 果 你 要 操 作 一 个 字 串 的 子 集, 本 套 函 数 的 starting index 和 length 正 合 要 求 GetStringCritical 必 须 非 常 小 心 使 用 你 必 须 确 保 不 分 配 新 对 象 和 任 何 阻 塞 系 统 的 操 作, 以 避 免 发 生 死 锁 如 下, 因 调 用 fprintf, 该 c 函 数 要 执 行 IO 操 作, 所 以 是 不 安 全 的 /* This is not safe! */ const char *c_str = (*env)->getstringcritical(env, j_str, 0); if (c_str == NULL)... /* error handling */ fprintf(fd, "%s\n", c_str); (*env)->releasestringcritical(env, j_str, c_str); 上 述 代 码, 不 安 全 的 原 因 : 当 前 线 程 执 行 了 GetStringCritical 后 将 禁 用 GC. 假 设,T 线 程 正 等 待 从 fd 读 取 数 据. 进 一 步 假 设, 调 用 fprintf 时 使 用 的 系 统 缓 存 将 等 待 T 读 取 完 毕 后 设 置. 我 们 制 造 了 一 个 死 锁 情 景 : 如 果 T 在 读 取 数 据 时 有 内 存 分 配 需 求, 可 能 使
JavaVM 执 行 GC. 而 此 时 的 GC 请 求 将 被 阻 塞, 直 到 当 前 线 程 执 行 ReleaseStringCritical, 不 幸 的 时, 这 个 操 作 必 须 等 fprintf 调 用 完 毕 后 才 会 执 行 此 时, 死 锁 发 生 所 以, 当 你 调 用 Get/RleaseStringCritical 要 时 刻 警 惕 死 锁 3.3 Accessing Arrays JNI 对 每 种 数 据 类 型 的 数 组 都 有 对 应 的 函 数 class IntArray private native int sumarray(int[] arr); public static void main(string[] args) IntArray p = new IntArray(); int arr[] = new int[10]; for (int i = 0; i < 10; i++) arr[i] = i; int sum = p.sumarray(arr); System.out.println("sum = " + sum); static System.loadLibrary("IntArray"); 3.3.1 Accessing Arrays in C 如 下 直 接 操 作 数 组 是 错 误 的 : /* This program is illegal! */ JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintarray arr) int i, sum = 0; for (i = 0; i < 10; i++) sum += arr[i]; 如 下 操 作 正 确 : JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintarray arr) jint buf[10]; jint i, sum = 0; (*env)->getintarrayregion(env, arr, 0, 10, buf); for (i = 0; i < 10; i++) sum += buf[i]; return sum; JNI 中 数 组 的 基 类 为 jarray, 其 他 如 jintarray 都 继 承 自 jarray 3.3.2 Accessing Arrays of Primitive Types 上 节 示 例 中, 使 用 GetIntArrayRegion 拷 贝 数 组 内 容 到 buf 中, 这 里 没 有 做 越 界 异 常 检 测, 因 为 知 道 数 组 有 10 个, 参 数 3 为 待 拷 贝 数 组 的 起 始 位 置, 参 数 4 为 拷 贝 元 素 的 个 数 JNI 支 持 SetIntArrayRegion 允 许 重 新 设 置 数 组 一 个 区 域 的 值, 其 他 基 本 类 型 (boolean, short, 和 float) 也 有 对 应 的 支 持 JNI 支 持 通 过 Get/Release<Type>ArrayElemetns 返 回 Java 数 组 的 一 个 拷 贝 ( 实 现 优 良 的 VM, 会 返 回 指 向 Java 数 组 的 一 个 直 接 的 指 针, 并 标 记 该 内 存 区 域, 不 允 许 被 GC) JNIEXPORT jint JNICALL Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintarray arr) jint *carr; jint i, sum = 0; carr = (*env)->getintarrayelements(env, arr, NULL); if (carr == NULL) return 0; /* exception occurred */ for (i=0; i<10; i++) sum += carr[i]; (*env)->releaseintarrayelements(env, arr, carr, 0); return sum; GetArrayLength 返 回 数 组 元 素 个 数 Java 2 SDK release 1.2 支 持 Get/ReleasePrimitiveArrayCritical, 该 套 函 数 的 使 用 原 则 与 上 述 String 部 分 相 同
3.3.3 Summary of JNI Primitive Array Functions JNI Function Description Since Get<Type>ArrayRegion Set<Type>ArrayRegion Get<Type>ArrayElements Release<Type>ArrayElements GetArrayLength New<Type>Array GetPrimitiveArrayCritical ReleasePrimitiveArrayCritica l Copies the contents of primitive arrays to or from a preallocated C buffer. Obtains a pointer to the contents of a primitive array.may return a copy of the array. Returns the number of elements in the array. Creates an array with the given length. Obtains or releases a pointer to the contents of a primitive array. May disable garbage collection, or return a copy of the array. JDK1.1 JDK1.1 JDK1.1 JDK1.1 Java 2 SDK1.2 3.3.4 Choosing among the Primitive Array Functions 使 用 原 则, 与 上 述 String 部 分 相 同, 请 阅 读 原 文 或 回 顾 前 面 的 内 容
3.3.5 Accessing Arrays of Objects 对 于 对 象 数 组 的 访 问, 使 用 Get/SetObjectArrayElement, 对 象 数 组 只 提 供 针 对 数 组 的 每 个 元 素 的 Get/Set, 不 提 供 类 似 Region 的 区 域 性 操 作 如 下, 二 维 数 组 示 例,Java 部 分 class ObjectArrayTest private static native int[][] initint2darray(int size); public static void main(string[] args) int[][] i2arr = initint2darray(3); for (int i = 0; i < 3; i++) for (int j = 0; j < 3; j++) System.out.print(" " + i2arr[i][j]); System.out.println(); static System.loadLibrary("ObjectArrayTest"); JNI 部 分 : JNIEXPORT jobjectarray JNICALL Java_ObjectArrayTest_initInt2DArray(JNIEnv *env, jclass cls, int size) jobjectarray result; int i; jclass intarrcls = (*env)->findclass(env, "[I"); if (intarrcls == NULL) return NULL; /* exception thrown */ result = (*env)->newobjectarray(env, size, intarrcls, NULL); if (result == NULL) return NULL; /* out of memory error thrown */ for (i = 0; i < size; i++) jint tmp[256]; /* make sure it is large enough! */ int j;
jintarray iarr = (*env)->newintarray(env, size); if (iarr == NULL) return NULL; /* out of memory error thrown */ for (j = 0; j < size; j++) tmp[j] = i + j; (*env)->setintarrayregion(env, iarr, 0, size, tmp); (*env)->setobjectarrayelement(env, result, i, iarr); (*env)->deletelocalref(env, iarr); return result; newint2darray 方 法 首 先 调 用 FindClass 获 得 一 个 一 维 int 数 组. "[I" 作 为 JNI 类 描 述 符 等 价 于 Java int[] 声 明 FindClass 当 装 载 类 失 败, 返 回 NULL( 可 能 是 没 找 到 类 或 内 存 不 足 ) 译 注 : 类 描 述 符, 也 可 以 叫 做 " 类 签 名 " 签 名 的 作 用 : 为 了 准 确 描 述 一 件 事 物. Java Vm 定 义 了 类 签 名, 方 法 签 名 ; 其 中 方 法 签 名 是 为 了 支 持 方 法 重 载 FindClass 返 回 NULL 的 原 因 : 提 供 了 错 误 的 类 描 述 符 无 法 在 当 前 ClassLoader 上 下 文 中 找 到 类 解 决 办 法 : 认 真 检 查 类 描 述 符 是 否 正 确 以 "/" 作 为 包 分 隔 符, 即 类 描 述 符 的 形 式 为 "xxx/xxx/xxx", 而 非 "xxx.xxx.xxx", 也 可 简 单 记 忆 为 "/" 用 在 本 地 形 式 ( 或 虚 拟 机 ) 中 ;"." 分 隔 符, 用 在 Java(Java Programming Language) 环 境 中 ; 并 且 类 描 述 符 末 尾 没 有 ".java", 如 FindClass("java/lang/String") 而 非 FindClass("java/lang/String.java") 构 造 ClassLoader, 并 利 用 Class.forName(String name, boolean initialize, ClassLoader loader) 装 载 类 其 中 第 三 个 解 决 办 法 比 较 复 杂, 涉 及 到 Java 的 双 亲 委 派 模 型, 类 与 对 象 相 容 性 判 定 等 问 题, 将 有 专 门 文 章 阐 述 然 后 调 用 NewObjectArray 分 配 一 个 对 象 数 组 注 意," 基 本 类 型 数 组 " 这 是 个 整 体 的 概 念, 它 是 一 个 对 象 后 面 我 们 要 填 充 它 注 意,DeleteLocalRef 是 释 放 局 部 对 象 引 用 译 注 : Java 中 有 许 多 引 用 的 概 念, 我 们 只 关 心 GlobalRef 和 LocalRef 两 种 JNI 编 程 很 复 杂, 建 议 不 要 引 入 更 多 复 杂 的 东 西, 正 确 高 效 的 实 现 功 能 就 可 以 了 比 如 对 引 用 来 说, 最 好
不 要 在 JNI 中 考 虑 : 虚 引 用 和 影 子 引 用 等 复 杂 的 东 西 GlobalRef: 当 你 需 要 在 JNI 层 维 护 一 个 Java 对 象 的 引 用, 而 避 免 该 对 象 被 垃 圾 回 收 时, 使 用 NewGlobalRef 告 诉 VM 不 要 回 收 此 对 象, 当 本 地 代 码 最 终 结 束 该 对 象 的 引 用 时, 用 DeleteGlobalRef 释 放 之 LocalRef: 每 个 被 创 建 的 Java 对 象, 首 先 会 被 加 入 一 个 LocalRef Table, 这 个 Table 大 小 是 有 限 的, 当 超 出 限 制,VM 会 报 LocalRef Overflow Exception, 然 后 崩 溃. 这 个 问 题 是 JNI 编 程 中 经 常 碰 到 的 问 题, 请 引 起 高 度 警 惕, 在 JNI 中 及 时 通 过 DeleteLocalRef 释 放 对 象 的 LocalRef. 又,JNI 中 提 供 了 一 套 函 数 :Push/PopLocalFrame, 因 为 LocalRef Table 大 小 是 固 定 的, 这 套 函 数 只 是 执 行 类 似 函 数 调 用 时, 执 行 的 压 栈 操 作, 在 LocalRef Table 中 预 留 一 部 分 供 当 前 函 数 使 用, 当 你 在 JNI 中 产 生 大 量 对 象 时, 虚 拟 机 仍 然 会 因 LocalRef Overflow Exception 崩 溃, 所 以 使 用 该 套 函 数 你 要 对 LocalRef 使 用 量 有 准 确 估 计
CHAPTER 4 Fields and Methods 本 章 介 绍 如 何 访 问 对 象 成 员, 如 何 从 本 地 代 码 调 用 Java 方 法, 即 以 callback 方 式 从 本 地 代 码 调 用 Java 代 码 ; 最 后 介 绍 一 些 优 化 技 术 4.1 Accessing Fields Java 语 言 支 持 两 种 成 员 (field):(static) 静 态 成 员 和 实 例 成 员. 在 JNI 获 取 和 赋 值 成 员 的 方 法 是 不 同 的. 译 者 : Java 层 的 field 和 method, 不 管 它 是 public, 还 是 package private 和 protected, 从 JNI 都 可 以 访 问 到,Java 面 向 语 言 的 封 装 性 不 见 了 Java: class InstanceFieldAccess private String s; private native void accessfield(); public static void main(string args[]) InstanceFieldAccess c = new InstanceFieldAccess(); c.s = "abc"; c.accessfield(); System.out.println("In Java:"); System.out.println(" c.s = \"" + c.s + "\""); static System.loadLibrary("InstanceFieldAccess"); JNI: JNIEXPORT void JNICALL Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj) jfieldid fid; /* store the field ID */ jstring jstr; const char *str; /* Get a reference to obj s class */
jclass cls = (*env)->getobjectclass(env, obj); printf("in C:\n"); /* Look for the instance field s in cls */ fid = (*env)->getfieldid(env, cls, "s", "Ljava/lang/String;"); if (fid == NULL) return; /* failed to find the field */ /* Read the instance field s */ jstr = (*env)->getobjectfield(env, obj, fid); str = (*env)->getstringutfchars(env, jstr, NULL); if (str == NULL) return; /* out of memory */ printf(" c.s = \"%s\"\n", str); (*env)->releasestringutfchars(env, jstr, str); /* Create a new string and overwrite the instance field */ jstr = (*env)->newstringutf(env, "123"); if (jstr == NULL) return; /* out of memory */ (*env)->setobjectfield(env, obj, fid, jstr); 输 出 : In C: c.s = "abc" In Java: c.s = "123" 4.1.1 Procedure for Accessing an Instance Field 访 问 对 象 成 员 分 两 步, 首 先 通 过 GetFieldID 得 到 对 象 成 员 ID, 如 下 : fid = (*env)->getfieldid(env, cls, "s", "Ljava/lang/String;"); 示 例 代 码, 通 过 GetObjectClass 从 obj 对 象 得 到 cls. 这 时, 通 过 在 对 象 上 调 用 下 述 方 法 获 得 成 员 的 值 : jstr = (*env)->getobjectfield(env, obj, fid); 示 例 中 要 得 到 的 是 一 个 对 象 类 型, 所 以 用 GetObjectField. 此 外 JNI 还 提 供 Get/SetIntField,Get/SetFloatField 访 问 不 同 类 型 成 员
译 者 : 通 过 JNI 方 法 访 问 对 象 的 成 员,JNI 对 应 的 函 数 命 名 非 常 有 规 律, 即 Get/Set<Return Value Type>Field 4.1.2 Field Descriptors 此 章 主 要 讲 述 签 名 问 题, 较 繁 琐, 可 以 总 结 如 下 : Type Signature Java Type Z boolean B byte C char S short I int J long F float D double L fully-qualified-class ; fully-qualified-class [ type type[] ( arg-types ) ret-type method type 如 下 Java 方 法 : long f (int n, String s, int[] arr); signature: "(ILjava/lang/String;[I)J" 签 名 是 一 种 用 参 数 个 数 和 类 型 区 分 同 名 方 法 的 手 段, 即 解 决 方 法 重 载 问 题 其 中 要 特 别 注 意 的 是 : 1. 类 描 述 符 开 头 的 'L' 与 结 尾 的 ';' 必 须 要 有 2. 数 组 描 述 符, 开 头 的 '[' 必 须 有. 3. 方 法 描 述 符 规 则 : "( 各 参 数 描 述 符 ) 返 回 值 描 述 符 ", 其 中 参 数 描 述 符 间 没 有 任 何 分 隔 符 号 描 述 符 很 重 要, 请 烂 熟 于 心. 写 JNI, 对 于 错 误 的 签 名 一 定 要 特 别 敏 感, 此 时 编 译 器 帮 不 上 忙, 执 行 make 前 仔 细 检 查 你 的 代 码 4.1.3 Accessing Static Fields 静 态 成 员 访 问 与 实 例 成 员 类 似 Java:
class StaticFielcdAccess private static int si; private native void accessfield(); public static void main(string args[]) StaticFieldAccess c = new StaticFieldAccess(); StaticFieldAccess.si = 100; c.accessfield(); System.out.println("In Java:"); System.out.println(" StaticFieldAccess.si = " + si); static System.loadLibrary("StaticFieldAccess"); JNI: JNIEXPORT void JNICALL Java_StaticFieldAccess_accessField(JNIEnv *env, jobject obj) jfieldid fid; /* store the field ID */ jint si; /* Get a reference to obj s class */ jclass cls = (*env)->getobjectclass(env, obj); printf("in C:\n"); /* Look for the static field si in cls */ fid = (*env)->getstaticfieldid(env, cls, "si", "I"); if (fid == NULL) return; /* field not found */ /* Access the static field si */ si = (*env)->getstaticintfield(env, cls, fid); printf(" StaticFieldAccess.si = %d\n", si); (*env)->setstaticintfield(env, cls, fid, 200); 输 出 : In C: StaticFieldAccess.si = 100 In Java: StaticFieldAccess.si = 200
请 阅 读 上 述 代 码, 不 再 叙 述 4.2 Calling Methods Java 中 有 三 类 方 法 : 实 例 方 法 静 态 方 法 和 构 造 方 法 class InstanceMethodCall private native void nativemethod(); private void callback() System.out.println("In Java"); public static void main(string args[]) InstanceMethodCall c = new InstanceMethodCall(); c.nativemethod(); static System.loadLibrary("InstanceMethodCall"); JNIEXPORT void JNICALL Java_InstanceMethodCall_nativeMethod(JNIEnv *env, jobject obj) jclass cls = (*env)->getobjectclass(env, obj); jmethodid mid = (*env)->getmethodid(env, cls, "callback", "()V"); if (mid == NULL) return; /* method not found */ printf("in C\n"); (*env)->callvoidmethod(env, obj, mid); 输 出 : In C In Java 4.2.1 Calling Instance Methods 如 上 节 示 例, 回 调 Java 方 法 分 两 步 : 首 先 通 过 GetMethodID 在 给 定 类 中 查 询 方 法. 查 询 基 于 方 法 名 称 和 签 名 本 地 方 法 调 用 CallVoidMethod, 该 方 法 表 明 被 调 Java 方 法 的 返 回 值 为 void
译 者 : 从 JNI 调 用 实 例 方 法 命 名 规 则 :Call<Return Value Type>Method 4.2.2 Formaing the Method Descriptor 一 个 方 法 描 述 ( 签 名 ) 由 各 参 数 类 型 签 名 和 返 回 值 签 名 构 成. 参 数 签 名 在 前, 并 用 小 括 号 括 起. 具 体 描 述 请 参 照 上 文 4.1.2 4.2.3 Calling Static Methods 同 实 例 方 法, 回 调 Java 静 态 方 法 分 两 步 : 首 先 通 过 GetStaticMethodID 在 给 定 类 中 查 找 方 法 通 过 CallStatic<ReturnValueType>Method 调 用 静 态 方 法 与 实 例 方 法 的 不 同, 前 者 传 入 参 数 为 jclass, 后 者 为 jobject 4.2.4 Calling Instance Methods of a Superclass 调 用 被 子 类 覆 盖 的 父 类 方 法 : JNI 支 持 用 CallNonvirtual<Type>Method 满 足 这 类 需 求 : GetMethodID 获 得 method ID 调 用 CallNonvirtualVoidMethod, CallNonvirtualBooleanMethod 上 述, 等 价 于 如 下 Java 语 言 的 方 式 : super.f(); CallNonvirtualVoidMethod 可 以 调 用 构 造 函 数 4.3 Invoking Constructors 你 可 以 像 调 用 实 例 方 法 一 样, 调 用 构 造 方 法, 只 是 此 时 构 造 函 数 的 名 称 叫 做 "<init>". 如 下 构 造 java.lang.string 对 象 (JNI 为 了 方 便 有 个 对 应 的 NewString 做 下 面 所 有 工 作, 这 里 只 是 做 示 例 展 示 ): jstring MyNewString(JNIEnv *env, jchar *chars, jint len) jclass stringclass; jmethodid cid; jchararray elemarr; jstring result; stringclass = (*env)->findclass(env, "java/lang/string"); if (stringclass == NULL)
return NULL; /* exception thrown */ /* Get the method ID for the String(char[]) constructor */ cid = (*env)->getmethodid(env, stringclass, "<init>", "([C)V"); if (cid == NULL) return NULL; /* exception thrown */ /* Create a char[] that holds the string characters */ elemarr = (*env)->newchararray(env, len); if (elemarr == NULL) return NULL; /* exception thrown */ (*env)->setchararrayregion(env, elemarr, 0, len, chars); /* Construct a java.lang.string object */ result = (*env)->newobject(env, stringclass, cid, elemarr); /* Free local references */ (*env)->deletelocalref(env, elemarr); (*env)->deletelocalref(env, stringclass); return result; 首 先,FindClass 找 到 java.lang.string 的 jclass. 接 下 来, 用 GetMethodID 找 到 构 造 函 数 String(char[] chars) 的 MethodID. 此 时 用 NewCharArray 分 配 一 个 Char 数 组 对 象 NewObject 调 用 构 造 函 数 用 DeleteLocalRef 释 放 资 源 注 意 NewString 是 个 常 用 函 数, 所 以 在 JNI 中 直 接 被 支 持 了, 并 且 该 函 数 的 实 现 要 比 我 们 实 现 的 高 效 也 可 使 用 CallNonvirtualVoidMehtod 调 用 构 造 函 数. 如 下 代 码 : result = (*env)->newobject(env, stringclass, cid, elemarr); 可 被 替 换 为 : result = (*env)->allocobject(env, stringclass); if (result) (*env)->callnonvirtualvoidmethod(env, result, stringclass, cid, elemarr); /* we need to check for possible exceptions */ if ((*env)->exceptioncheck(env)) (*env)->deletelocalref(env, result); result = NULL;
AllocObject 创 建 一 个 未 初 始 化 的 对 象, 该 函 数 必 须 在 每 个 对 象 上 被 调 用 一 次 而 且 只 能 是 一 次 有 时 你 会 发 现 先 创 建 未 初 始 化 对 象 再 调 用 构 造 函 数 的 方 法 是 有 用 的 4.4 Caching Field and Method IDs 获 得 field 与 method IDs, 需 要 做 基 于 名 称 和 签 名 的 符 号 表 查 询, 此 过 程 可 以 被 优 化 基 本 想 法 是 : 只 在 第 一 次 使 用 ID 时 查 询, 然 后 缓 存 该 值. 有 两 个 缓 存 时 机 : 首 次 使 用 和 初 始 化 类 时 4.4.1 Caching at the Point of Use 如 下, 首 次 使 用 时, 缓 存 的 局 部 静 态 变 量 中, 避 免 每 次 调 用 计 算 JNIEXPORT void JNICALL Java_InstanceFieldAccess_accessField(JNIEnv *env, jobject obj) static jfieldid fid_s = NULL; /* cached field ID for s */ jclass cls = (*env)->getobjectclass(env, obj); jstring jstr; const char *str; if (fid_s == NULL) fid_s = (*env)->getfieldid(env, cls, "s", "Ljava/lang/String;"); if (fid_s == NULL) return; /* exception already thrown */ printf("in C:\n"); jstr = (*env)->getobjectfield(env, obj, fid_s); str = (*env)->getstringutfchars(env, jstr, NULL); if (str == NULL) return; /* out of memory */ printf(" c.s = \"%s\"\n", str); (*env)->releasestringutfchars(env, jstr, str); jstr = (*env)->newstringutf(env, "123"); if (jstr == NULL) return; /* out of memory */
(*env)->setobjectfield(env, obj, fid_s, jstr); 如 上, 静 态 变 量 fid_s 保 存 了 InstanceFieldAccess.s 的 filed ID 初 始 化 阶 段 静 态 变 量 被 赋 值 为 NULL 第 一 调 用 InstanceFieldAccess.accessField 时, 缓 存 fieldid 以 待 后 用 你 可 能 会 发 现 上 述 代 码 有 个 竞 争 条 件, 当 多 个 线 程 同 时 访 问 此 函 数 时, 可 能 会 同 时 计 算 一 个 field ID. 没 关 系, 此 处 的 竞 争 是 无 害 的, 因 为 即 使 在 多 个 线 程 中 同 时 计 算 该 field ID, 各 线 程 中 的 计 算 结 果 都 是 一 样 的 构 造 函 数 的 MethodID 也 可 被 缓 存, 如 下 : jstring MyNewString(JNIEnv *env, jchar *chars, jint len) jclass stringclass; jchararray elemarr; static jmethodid cid = NULL; jstring result; stringclass = (*env)->findclass(env, "java/lang/string"); if (stringclass == NULL) return NULL; /* exception thrown */ /* Note that cid is a static variable */ if (cid == NULL) /* Get the method ID for the String constructor */ cid = (*env)->getmethodid(env, stringclass, "<init>", "([C)V"); if (cid == NULL) return NULL; /* exception thrown */ /* Create a char[] that holds the string characters */ elemarr = (*env)->newchararray(env, len); if (elemarr == NULL) return NULL; /* exception thrown */ (*env)->setchararrayregion(env, elemarr, 0, len, chars); /* Construct a java.lang.string object */ result = (*env)->newobject(env, stringclass, cid, elemarr); /* Free local references */ (*env)->deletelocalref(env, elemarr); (*env)->deletelocalref(env, stringclass); return result;
4.4.2 Caching in the Defining Class's Initializer 上 述 第 一 次 使 用 缓 存 的 方 式, 每 次 都 有 与 NULL 的 判 断, 并 且 可 能 有 一 个 无 害 的 竞 争 条 件 而 初 始 化 类 时, 同 时 初 始 化 JNI 层 对 该 类 成 员 的 缓 存, 可 以 弥 补 上 述 缺 憾, 如 下 initids: Java 代 码 : class InstanceMethodCall private static native void initids(); private native void nativemethod(); private void callback() System.out.println("In Java"); public static void main(string args[]) InstanceMethodCall c = new InstanceMethodCall(); c.nativemethod(); static System.loadLibrary("InstanceMethodCall"); initids(); JNI 代 码 : jmethodid MID_InstanceMethodCall_callback; JNIEXPORT void JNICALL Java_InstanceMethodCall_initIDs(JNIEnv *env, jclass cls) MID_InstanceMethodCall_callback = (*env)->getmethodid(env, cls, "callback", "()V"); 译 注 : 还 可 以 改 进 上 述 缓 存 策 略 的 初 始 化 时 机, 第 一 种 方 法 的 缺 陷 文 中 已 经 提 了, 而 第 二 种 需 要 在 Java 代 码 主 动 调 用 JNI 作 缓 存 改 进 : 可 以 在 你 的 项 目 中 加 一 套 Hash 表, 封 装 FindClass,GetMethodID,GetFieldID 等 函 数, 查 询 的 所 有 操 作, 都 对 Hash 表 操 作, 如 首 次 FindClass 一 个 类, 这 时 可 以 把 一 个 类 的 所 有 成 员 缓 存 到 Hash 表 中, 用 名 字 + 签 名 做 键 值 译 者 所 做 项 目 引 入 了 这 个 优 化, 项 目 的 执 行 效 率 有 100 倍 的 提 高 ; 当 时 还 做 过 两 个 权 衡 : 1. 用 一 个 Hash 表, 还 是 每 个 类 一 个 Hash 表 2. 首 次 FindClass 类 时, 一 次 缓 存 所 有 的 成 员, 还 是 用 时 缓 存 最 终 做 的 选 择 是 : 为 了 降 低 冲 突, 每 个 类 一 个 Hash 表, 并 且 一 次 缓 存 一 个 类 的 所 有 成 员 当 然, 没 有 尽 善 尽 美 的 优 化 策 略, 我 们 做 到 这 个 层 次, 已 经 达 到 预 期 目 标, 没 有 继 续 深 入
4.4.3 Comparison between the Two Approaches to Caching IDs 在 对 Java 源 码 无 改 动 权 时 使 用 时 缓 存 是 一 种 合 理 的 解 决 方 案. 但 有 许 多 弊 端 : 无 害 的 竞 争 条 件 和 重 复 与 NULL 比 较 在 类 没 被 卸 载 时,MethodID 和 FieldID 一 直 有 效. 所 以 你 必 须 保 证 : 当 你 的 JNI 代 码 依 赖 这 些 缓 存 值 的 声 明 周 期 内, 该 类 不 会 被 卸 载 而 与 另 一 种 优 化 策 略, 连 同 类 的 初 始 化 缓 存 Method/Field ID, 每 当 类 再 次 被 装 载, 缓 存 值 会 被 更 新 所 以, 有 条 件 的 话, 更 安 全 的 优 化 策 略 是 : 连 同 类 的 初 始 化 缓 存 Method/Field ID 4.5 Performance of JNI Field and Method Operations 在 学 习 了 如 何 缓 存 field 和 method ID 的 优 化 技 术 后, 你 可 能 会 想 : 影 响 JNI 回 调 性 能 的 关 键 性 因 素 是 什 么? 在 效 率 方 面,JNI/Java 与 Java/JNI 和 Java/Java 间 对 比, 是 怎 样 的? 这 要 看 具 体 VM 实 现 的 JNI 效 率. 很 难 给 出 一 个 普 适 的 性 能 关 键 指 标. 取 而 代 之, 我 们 将 分 析 在 访 问 类 成 员 时 的 固 有 性 能 损 失 首 先 比 较 Java/native 和 Java/Java, 前 者 因 下 述 原 因 可 能 会 比 后 者 慢 : Java/native 与 Java/Java 的 调 用 约 定 不 同. 所 以,VM 必 须 在 调 用 前, 对 参 数 和 调 用 栈 做 特 殊 准 备 常 用 的 优 化 技 术 是 内 联. 相 比 Java/Java 调 用,Java/native 创 建 内 联 方 法 很 难 粗 略 估 计 : 执 行 一 个 Java/native 调 用 要 比 Java/Java 调 用 慢 2-3 倍. 也 可 能 有 一 些 VM 实 现,Java/native 调 用 性 能 与 Java/Java 相 当 ( 此 种 虚 拟 机,Java/native 使 用 Java/Java 相 同 的 调 用 约 定 ) native/java 调 用 效 率 可 能 与 Java/Java 有 10 倍 的 差 距, 因 为 VM 一 般 不 会 做 Callback 的 优 化 对 于 field 的 访 问, 将 没 什 么 不 同, 只 是 通 过 JNI 访 问 某 对 象 结 构 中 某 个 位 置 的 值 译 注 : 上 述 只 是 学 术 考 虑. 用 好 缓 存 的 优 化 策 略, 完 全 可 以 让 项 目 工 作 的 绝 对 出 色
CHAPTER 5 Local and Global References JNI 把 instance 和 array 类 型 的 指 针 对 外 公 布 为 opaque reference. 本 地 代 码 不 直 接 操 作 指 针, 而 是 通 过 JNI 函 数, 所 以 本 地 代 码 不 用 关 心 内 存 布 局. 关 于 reference, 这 里 还 有 更 丰 富 的 东 西 有 待 介 绍 : JNI 支 持 三 种 类 型 的 opaque reference:local references, global references, 和 weak global references Local 和 Global 引 用 有 不 同 的 生 命 周 期. Local Ref 在 native method 执 行 完 毕 后 被 JavaVM 自 动 释 放, 而 GlobalRef,WeakRef 在 程 序 员 主 动 释 放 前 一 直 有 效 各 种 引 用 都 有 使 用 范 围. 如 LocalRef 只 能 在 当 前 线 程 的 native method 中 使 用 本 章 将 详 细 讲 述 不 同 类 型 Ref 的 使 用 方 法, 正 确 管 理 JNI 引 用 是 程 序 健 壮 空 间 占 用 少 的 关 键 5.1 Local and Global References LocalRef 与 GlobalRef 的 差 异, 将 用 几 个 示 例 说 明 : 大 部 分 JNI 函 数 都 会 创 建 LocalRef, 如 NewObject 创 建 一 个 实 例, 并 返 回 一 个 指 向 该 实 例 的 LocalRef LocalRef 只 在 本 线 程 的 native method 中 有 效. 一 但 native method 返 回,LocalRef 将 被 释 放 不 要 缓 存 一 个 LocalRef, 并 企 图 在 下 次 进 入 该 JNI 方 法 时 使 用, 如 下 : /* This code is illegal */ jstring MyNewString(JNIEnv *env, jchar *chars, jint len) static jclass stringclass = NULL; jmethodid cid; jchararray elemarr; jstring result; if (stringclass == NULL) stringclass = (*env)->findclass(env, "java/lang/string"); if (stringclass == NULL) return NULL; /* exception thrown */
/* It is wrong to use the cached stringclass here, because it may be invalid. */ cid = (*env)->getmethodid(env, stringclass, "<init>", "([C)V");... elemarr = (*env)->newchararray(env, len);... result = (*env)->newobject(env, stringclass, cid, elemarr); (*env)->deletelocalref(env, elemarr); return result; 上 述 代 码, 企 图 重 复 使 用 FindClass(env, "java/lang/string") 的 返 回 值, 这 种 方 式 不 对, 因 为 FindClass 返 回 的 是 一 个 LocalRef. 请 设 想 以 下 代 码 : JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env, jobject this) char *c_str =...;... return MyNewString(c_str); 如 下, 两 次 调 用 f 这 个 本 地 方 法....... = C.f(); // The first call is perhaps OK.... = C.f(); // This would use an invalid local reference.... 第 一 次 调 用 可 能 正 确, 而 第 二 次 将 引 用 一 个 无 效 位 置, 因 为 第 二 次 企 图 使 用 存 在 静 态 变 量 中 的 LocalRef 有 两 种 方 式 让 LocalRef 无 效, 一,native method 返 回,JavaVM 自 动 释 放 LocalRef; 二, 用 DeleteLocalRef 主 动 释 放 既 然 LocalRef 会 被 JavaVM 自 动 释 放, 为 什 么 还 要 有 DeleteLocalRef? 因 为 LocalRef 是 阻 止 引 用 被 GC, 但 当 你 在 本 地 代 码 中 操 作 大 量 对 象 时, 而 LocalRefTable 又 是 有 限 的, 及 时 调 用 DeleteLocalRef, 会 释 放 LocalRef 在 LocalRefTable 中 所 占 位 置 并 使 对 象 及 时 得 到 回 收 LocalRef 只 在 创 建 该 对 象 的 线 程 中 有 效, 企 图 把 LocalRef 存 到 全 局 变 量 中 供 其 他 线 程 使 用 的 做 法 是 错 误 的 译 注 : 注 意 这 里 的 提 到 的 native method 返 回, 返 回 是 指 回 到 Java 层, 如 果 从 一 个 本 地 函 数 返
回 到 另 一 个 本 地 函 数,LocalRef 是 有 效 的 5.1.2 Global References 释 放 GlobalRef 前, 你 可 以 在 多 个 本 地 方 法 调 用 过 程 和 多 线 程 中 使 用 GlobalRef 所 引 对 象 与 LocalRef 类 似,GlobalRef 的 作 用 : 防 止 对 象 被 GC(garbage collected, 垃 圾 回 收 ) GlobalRef 与 LocalRef 不 同 的 是,LocalRef 一 般 自 动 创 建 ( 返 回 值 为 jobject/jclass 等 JNI 函 数 ), 而 GlobalRef 必 须 通 过 NewGlobalRef 由 程 序 员 主 动 创 建 如 下 : /* This code is OK */ jstring MyNewString(JNIEnv *env, jchar *chars, jint len) static jclass stringclass = NULL;... if (stringclass == NULL) jclass localrefcls = (*env)->findclass(env, "java/lang/string"); if (localrefcls == NULL) return NULL; /* exception thrown */ /* Create a global reference */ stringclass = (*env)->newglobalref(env, localrefcls); /* The local reference is no longer useful */ (*env)->deletelocalref(env, localrefcls); /* Is the global reference created successfully? */ if (stringclass == NULL) return NULL; /* out of memory exception thrown */... 该 例 做 了 修 改, 当 stringclass 为 NULL 时, 我 们 创 建 了 java.lang.string 的 GlboalRef, 并 删 除 了 对 应 的 LocalRef, 以 待 下 次 再 进 入 此 方 法 时, 使 用 stringclass 5.1.3 Weak Global References Weak Global Ref 用 NewGlobalWeakRef 于 DeleteGlobalWeakRef 进 行 创 建 和 删 除, 多 个 本 地 方 法 调 用 过 程 中 和 多 线 程 上 下 文 中 使 用 的 特 性 与 GlobalRef 相 同, 但 该 类 型 的 引 用 不 保 证 不 被 GC
前 述 示 例 MyNewString 中, 对 java.lang.string 声 明 GlobalRef 或 GlobalWeakRef 效 果 相 同, 因 为 java.lang.string 是 一 个 系 统 类 不 会 被 GC Weak Global Ref 使 用 在 允 许 被 GC 的 场 合, 如 内 存 紧 张 时 JNIEXPORT void JNICALL Java_mypkg_MyCls_f(JNIEnv *env, jobject self) static jclass mycls2 = NULL; if (mycls2 == NULL) jclass mycls2local = (*env)->findclass(env, "mypkg/mycls2"); if (mycls2local == NULL) return; /* can t find class */ mycls2 = NewWeakGlobalRef(env, mycls2local); if (mycls2 == NULL) return; /* out of memory */... /* use mycls2 */ 我 们 假 设,MyCls 与 MyCls2 有 同 样 的 生 命 周 期 ( 并 被 同 样 的 Class Loader 装 载 ), 类 似 MyCls 被 卸 载 而 MyCls2 没 被 卸 载 的 情 况 不 考 虑 如 果 发 生 这 种 情 况, 我 们 还 需 要 检 测 mycls2 是 否 还 执 行 的 对 象 仍 然 有 效 5.1.4 Comparing Reference 有 两 个 对 象, 用 如 下 方 法 比 较 相 容 性 : (*env)->issameobject(env, obj1, obj2) 如 果 相 容, 返 回 JNI_TRUE, 否 则 返 回 JNI_FALSE 与 NULL 的 比 较,LocalRef 与 GlobalRef 语 义 显 然, 前 提 是 释 放 了 两 个 引 用, 程 序 员 重 新 为 相 应 变 量 做 了 NULL 初 始 化 但 对 于 Weak Global Ref 来 说, 需 要 使 用 下 述 代 码 判 定 : (*env)->issameobject(env, wobj, NULL) 因 为, 对 于 一 个 Weak Global Ref 来 说 可 能 指 向 已 经 被 GC 的 无 效 对 象 译 注 : 上 述 的 判 断, 都 是 假 设 所 有 的 类 和 对 象 都 是 在 一 个 Class Loader 下 被 装 载 的. 关 于 ClassLoader 的 议 题 后 有 专 门 文 章.
5.2 Freeing Reference 每 个 JNI 引 用 都 会 引 用 表 中 的 一 个 位 置. 作 为 一 个 JNI 程 序 员, 你 应 该 清 楚 程 序 某 阶 段 中 使 用 的 引 用 数 量 如 LocalRef, 如 果 你 疏 于 DeleteLocalRef 的 话, 在 JavaVM 运 行 限 制 内 你 的 应 用 程 序 工 作 正 常, 在 极 端 情 况 会 崩 溃 5.2.1 Freeing Local References 译 注 : 本 章 没 有 翻 译. 本 章 讲 了 很 多 关 于 LocalRef 的 释 放 原 则, 译 者 认 为 : 考 虑 何 时 释 放 / 何 时 不 释 放 的 问 题, 不 如 认 真 审 查 代 码, 严 堵 每 个 泄 露 环 节, 尽 最 大 努 力 提 高 程 序 的 稳 定 性 就 像 内 存 分 配 一 样, 虽 然 进 程 结 束 后,OS 自 动 释 放 该 进 程 分 配 的 所 有 内 存, 但 对 于 期 望 长 期 稳 定 运 行 的 系 统 来 说, 我 们 希 望 杜 绝 内 存 泄 露 5.2.2 Managing Local References in Java 2 SDK Release 1.2 译 注 : 本 章 没 有 翻 译 由 于 LocalRef Table 大 小 是 固 定 的, 这 套 函 数 只 是 执 行 类 似 函 数 调 用 时, 执 行 的 压 栈 操 作, 并 在 执 行 PopLocalFrame 后 执 行 类 似 退 栈 操 作, 在 LocalRef Table 中 预 留 一 部 分 供 当 前 函 数 使 用, 当 你 在 JNI 中 产 生 大 量 对 象 时, 虚 拟 机 仍 然 会 因 LocalRef Overflow Exception 崩 溃 具 体 原 则 仍 如 上 述, 严 堵 每 个 泄 露 环 节 ; 如 果 你 能 准 确 估 计 LocalRef 用 量, 可 以 使 用 Push/PopLocalFrame 5.2.3 Freeing Global References 当 不 再 使 用 GlobalRef 所 指 对 象, 及 时 调 用 DeleteGlobalRef 释 放 对 象. 否 则,GC 将 不 回 收 该 对 象 对 于 DeleteWeakGlobalRef 来 说, 不 使 用 WeakGlobalRef 时, 也 要 及 时 释 放, 因 为 即 使 GC 会 回 收 该 对 象 内 容,WeakGlobalRef 在 Table 中 的 位 置 还 占 用 着, 即 和 尚 都 跑 了, 庙 还 在 译 注 : 综 上, 不 管 何 种 类 型 引 用, 在 不 使 用 所 引 用 对 象 后, 及 时 调 用 对 应 指 针 类 型 的 释 放 函 数 5.3 Rules for Managing References 现 在 我 们 归 纳 一 下 管 理 JNI 引 用 的 原 则. 看 看 如 何 减 少 内 存 使 用 有 效 使 用 对 象 有 两 类 本 地 函 数 : 功 能 函 数 和 工 具 函 数 当 写 native method 的 实 现 时, 要 认 真 处 理 循 环 中 产 生 的 LocalRef. VM 规 范 中 规 定 每 个 本 地 方 法 至 少 要 支 持 16 个 LocalRef 供 自 由 使 用 并 在 本 地 方 法 返 回 后 回 收. 本 地 方 法 绝 对 不 能 滥 用 GlobalRef 和 WeakGlobalRef, 因 为 此 类 型 引 用 不 会 被 自 动 回 收
工 具 函 数, 对 LocalRef 的 使 用 更 要 提 起 警 惕, 因 为 该 类 函 数 调 用 上 下 文 不 确 定, 而 且 会 被 重 复 调 用, 每 个 代 码 路 径 都 要 保 证 不 存 在 LocalRef 泄 露 由 于 某 些 缓 存 机 制, 可 以 在 工 具 函 数 中 创 建 GlobalRef, WeakGlobalRef 当 工 具 函 数 返 回 对 象 时, 要 严 格 遵 守 引 用 约 定, 让 调 用 者 在 决 定 是 否 释 放 时 能 作 出 准 确 判 断, 如 下 : while (JNI_TRUE) jstring infostring = GetInfoString(info);... /* process infostring */??? /* * we need to call DeleteLocalRef, DeleteGlobalRef, * or DeleteWeakGlobalRef depending on the type of * reference returned by GetInfoString. */ JNI 方 法 NewLocalRef 总 保 证 返 回 一 个 LocalRef, 如 下 : jstring MyNewString(JNIEnv *env, jchar *chars, jint len) static jstring result; /* wstrncmp compares two Unicode strings */ if (wstrncmp("commonstring", chars, len) == 0) /* refers to the global ref caching "CommonString" */ static jstring cachedstring = NULL; if (cachedstring == NULL) /* create cachedstring for the first time */ jstring cachedstringlocal =... ; /* cache the result in a global reference */ cachedstring = (*env)->newglobalref(env, cachedstringlocal); return (*env)->newlocalref(env, cachedstring);... /* create the string as a local reference and store in result as a local reference */
return result; Push/PopLocalFrame 常 被 用 来 管 理 LocalRef. 在 进 入 本 地 方 法 时, 调 用 一 次 PushLocalFrame, 并 在 本 地 方 法 结 束 时 调 用 PopLocalFrame. 此 对 方 法 执 行 效 率 非 常 高, 建 议 使 用 这 对 方 法 译 注 : 你 只 要 对 当 前 上 下 文 内 使 用 的 对 象 数 量 有 准 确 估 计, 建 议 使 用 这 对 方 法, 在 这 对 方 法 间, 不 必 调 用 DeleteLocalRef, 只 要 该 上 下 文 结 尾 处 调 用 PopLocalFrame 会 一 次 性 释 放 所 有 LocalRef 一 定 保 证 该 上 下 文 出 口 只 有 一 个, 或 每 个 return 语 句 都 做 严 格 检 查 是 否 调 用 了 PopLocalFrame jobject f(jnienv *env,...) jobject result; if ((*env)->pushlocalframe(env, 10) < 0) /* frame not pushed, no PopLocalFrame needed */ return NULL;... result =...; if (...) /* remember to pop local frame before return */ result = (*env)->poplocalframe(env, result); return result;... result = (*env)->poplocalframe(env, result); /* normal return */ return result; 忘 记 调 用 PopLocalFrame 可 能 会 使 VM 崩 溃
CHAPTER 6 Exceptions 我 们 已 经 碰 到 在 调 用 JNI 方 法 时 出 现 异 常 的 情 况. 本 章 将 介 绍 如 何 检 查 并 处 理 异 常 本 章 只 关 注 在 调 用 JNI 方 法 或 Java 方 法 时 出 现 异 常 的 处 理 办 法 (Java 异 常 ), 不 涉 及 本 地 代 码 本 身 ( 如 本 地 代 码 中 的 除 0 错 ) 或 调 用 系 统 函 数 出 现 异 常 的 处 理 方 法 6.1.1 Caching and Throwing Exceptions in Native Code 如 下 Java 代 码 展 示 如 何 声 明 JNI 可 能 抛 出 的 异 常 class CatchThrow private native void doit() throws IllegalArgumentException; private void callback() throws NullPointerException throw new NullPointerException("CatchThrow.callback"); public static void main(string args[]) CatchThrow c = new CatchThrow(); try c.doit(); catch (Exception e) System.out.println("In Java:\n\t" + e); static System.loadLibrary("CatchThrow"); JNI 代 码 : JNIEXPORT void JNICALL Java_CatchThrow_doit(JNIEnv *env, jobject obj) jthrowable exc; jclass cls = (*env)->getobjectclass(env, obj); jmethodid mid = (*env)->getmethodid(env, cls, "callback", "()V"); if (mid == NULL) return;
(*env)->callvoidmethod(env, obj, mid); exc = (*env)->exceptionoccurred(env); if (exc) /* We don't do much with the exception, except that we print a debug message for it, clear it, and throw a new exception. */ jclass newexccls; (*env)->exceptiondescribe(env); (*env)->exceptionclear(env); newexccls = (*env)->findclass(env, "java/lang/illegalargumentexception"); if (newexccls == NULL) /* Unable to find the exception class, give up. */ return; (*env)->thrownew(env, newexccls, "thrown from C code"); 输 出 : java.lang.nullpointerexception: at CatchThrow.callback(CatchThrow.java) at CatchThrow.doit(Native Method) at CatchThrow.main(CatchThrow.java) In Java: java.lang.illegalargumentexception: thrown from C code callback 方 法 抛 出 NullPointerException. 当 CallVoidMethod 把 控 制 权 返 回 给 本 地 代 码, 本 地 代 码 调 用 ExceptionOccurred 检 查 是 否 有 异 常 发 生. 我 们 的 处 理 方 式 是, 当 有 异 常 发 生, 调 用 ExceptionDescribe 打 印 调 用 堆 栈, 然 后 用 ExceptionClear 清 空 异 常, 最 后 重 新 抛 出 IllegalArgumentException 译 者 : ExceptionOccurred 返 回 一 个 jobject, 注 意 结 束 处 理 时 调 用 DeleteLocalRef 删 除 该 返 回 值 JNI 中 还 有 一 个 ExceptionCheck, 只 是 返 回 一 个 jboolean 的 布 尔 值, 更 适 合 检 查 异 常 是 否 发 生 在 JNI 中 产 生 的 异 常 ( 通 过 调 用 ThrowNew), 与 Java 语 言 中 异 常 发 生 的 行 为 不 同,JNI 中 当 前 代 码 路 径 不 会 立 即 改 变 在 Java 中 发 生 异 常,VM 自 动 把 控 制 权 转 向 try/catch 中 匹 配
的 异 常 类 型 处 理 块 VM 首 先 清 空 异 常 队 列, 然 后 执 行 异 常 处 理 块 相 反,JNI 中 必 须 显 式 处 理 VM 的 处 理 方 式 6.1.2 A Utility Function JNI 中 抛 异 常 很 经 典 : 找 异 常 类, 调 用 ThrowNew 抛 出 之 ; 所 以, 可 以 写 一 个 工 具 函 数 void JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg) jclass cls = (*env)->findclass(env, name); /* if cls is NULL, an exception has already been thrown */ if (cls!= NULL) (*env)->thrownew(env, cls, msg); /* free the local ref */ (*env)->deletelocalref(env, cls); 本 书 中,JNU 前 缀 表 示 JNI Utilities. JNU_ThrowByName 首 先 通 过 FindClass 找 到 异 常 类 如 果 FindClass 找 类 失 败, 将 返 回 NULL, 并 抛 出 NoClassDefFoundError 异 常 此 情 况, JNU_ThrowByName 将 保 留 该 异 常, 然 后 返 回. 如 果 FindClass 成 功, 将 调 用 ThrowNew 抛 出 异 常 所 以 不 管 哪 种 情 况, 调 用 该 函 数 后, 当 前 的 JNIEnv 环 境 里 总 有 个 异 常 6.2 Proper Exception Handling JNI 程 序 员 应 对 所 有 可 能 的 异 常 做 处 理, 这 个 要 求 虽 然 苛 刻, 但 这 是 健 壮 软 件 的 保 证 6.2.1 Checking for Exception 有 两 种 方 式 检 查 是 否 有 异 常 发 生 1. 大 多 数 JNI 函 数 用 显 式 方 式 表 明 当 前 线 程 是 否 有 异 常 发 生 下 述 代 码 判 断 GetFieldID 返 回 是 否 为 NULL 以 检 查 是 否 发 生 异 常 : /* a class in the Java programming language */ public class Window long handle; int length; int width; static native void initids(); static