Java与C/C++能够相互调用的本质原因是什么?

3 阅读6分钟

问题:

Java与C/C++能够相互调用的本质原因是因为JVM是由C/C++写的吗?

回答:

Java 与 C/C++ 能够相互调用 并不是因为 JVM 是用 C/C++ 写的。

JVM 用什么语言实现,只是“实现层面的便利”,但绝不是本质原因。

下面从本质原因、实现原因、可能的误解三方面解释清楚。

一、Java 与 C/C++ 能互相调用的本质原因

本质原因只有一个:Java 语言规范定义了一套跨语言接口——JNI(Java Native Interface)。

换句话说:

Java ↔ Native 的互操作能力来自 JNI 这个标准,而不是来自 JVM 是用 C/C++ 写的

只要某种 JVM 实现了 JNI 规范,那么:

  • Java 代码就可以按规范调用 native 函数;
  • Native 代码也可以按规范回调 Java 方法;

至于 JVM 是用 C/C++、Rust、Go、汇编还是别的语言实现,都不影响 JNI 的存在。

二、JVM 用 C/C++ 实现只是“实现层面的便利条件”

虽然 JVM 常用 C/C++ 实现(如 HotSpot、ART),但:

  • 即便 JVM 用 Rust 写,也必须实现 JNI;
  • 即便 JVM 用 Go 写,也一样可以与 C/C++ 交互;
  • JVM 内部如何实现,与 Java 能否调用 C/C++ 没有直接必然关系。

JVM 用 C/C++ 实现的原因包括:

  • 更容易与底层 OS、CPU、内存模型打交道;
  • 性能好、可移植性强;
  • 历史原因(JVM 最早诞生于 1990s,当年系统级开发主要语言就是 C++)。

但这只是“实现选择”,不是“语言互调的根因”。

三、如果 JVM 用别的语言实现,Java 与 C/C++ 还能互调吗?

完全能。

例如:

  • 有些 hobby JVM 是用 Python 写的,仍然实现 JNI;
  • 有些研究型 JVM 是用 Rust 写的,也通过 FFI 实现 JNI;
  • 甚至可以用 C# 来写 JVM,同样可以实现 JNI。

也就是说:

只要 JVM 实现 JNI 规范,Java 就能调用 C/C++。

跟 JVM 自己用什么语言写毫无必然关系。

四、为什么很多人误以为“因为 JVM 用 C/C++ 写,所以能互调”?

这是因为两件事情同时为真,但因果关系并不存在:

  1. JVM 大多数确实用 C/C++ 写的。
  2. Java 能通过 JNI 调用 C/C++。

于是很多人“误把相关性当成因果性”。

真实关系是:

  • JVM 用 C/C++ 编写 → JNI 的实现更容易
  • 但 JNI 的设计 → 才是 Java ↔ C/C++ 互调的根本

五、一句话总结(重点)

Java 与 C/C++ 能互相调用的根本原因是“JNI 规范”,

而不是“JVM 是用 C/C++ 写的”。

JVM 的实现语言只是让“实现 JNI”更方便,但不决定是否能互调。

那么JNI规范到底定义了什么?是如何实现互相调用的?

一、JNI 规范究竟定义了什么?

JNI(Java Native Interface)是 Java 语言规范的一部分,不是 JVM 的内部实现细节。

它明确规定了:

  1. Java 如何声明一个 native 方法

public native int add(int a, int b);

JNI 明确约定:

  • 这个方法不会由 Java 实现,而是由 外部语言(C/C++)实现。
  • JVM 必须提供机制让 Java 能找到并调用这个 native 函数。
  1. C/C++ 函数应该是什么样子的

JNI 定义了函数签名规则,例如静态注册:

JNIEXPORT jint JNICALL
Java_com_example_Test_add(JNIEnv* env, jobject thiz, jint a, jint b)

JNI 规范决定:

  • 怎么命名;
  • 参数如何传递;
  • 类型如何映射;
  • 如何通过 JNIEnv* 访问 Java 对象。

这不是 JVM 的“选择”,而是规范要求。

  1. JVM 必须暴露哪些 API 给 native 使用

例如:

  • (*env)->FindClass
  • (*env)->CallVoidMethod
  • (*env)->NewStringUTF
  • (*env)->GetIntArrayElements

这些 API 全部写在 jni.h 中,是 标准化接口。

只要某个 JVM 实现了这些 API,Java 与任何 native 语言都能交互。

  1. 引用、对象、数组、异常、线程……如何跨语言管理

例如:

  • Native 如何创建 Java 对象
  • Native 如何抛出 Java 异常
  • Native 如何接管线程(AttachCurrentThread)
  • 如何避免 GC 回收 Java 对象(GlobalRef)

这些行为全部由 JNI 固定定义。

二、JVM 内部是如何实现“Java ↔ Native 打通”的?

不管 JVM 用什么语言写,它必须实现以下能力:

  1. 能加载外部动态库(.so / .dll)

就像操作系统加载普通 C/C++ 动态库一样。

内部相当于做了:

dlopen("libxxx.so");

这个动作和 JVM 是 C/C++ 实现没有直接关系,任何语言都能做:

  • Rust:libloading
  • Go:plugin
  • Python:ctypes
  • C#:DllImport

所以 JVM 用什么语言写不影响这一点。

  1. 能根据方法名找到 native 函数入口

JNI 的规则只是告诉 JVM:

当 Java 调用某个 native 方法时,你要去动态库里找对应的函数名。

不管 JVM 是 C++ 写的、Rust 写的还是 Python 写的,只要实现:

  • 符号查找
  • 函数绑定
  • 参数传递

即可,本质就是 FFI(Foreign Function Interface)。

这个能力不依赖 JVM 本身必须用 C++ 实现。

一种信息传递的方式,协议。读写可以相互遵守就好了。

  1. 能暴露 JNIEnv(操作 Java 世界的能力)

JVM 必须给 native 层传入一个结构体(JNIEnv*),里面存放:

  • 调用 Java 方法的入口
  • 创建 Java 对象的入口
  • 数组、字符串访问入口
  • 异常处理入口

这是 API 级别的约束,不是语言级约束。

一个 Rust 写的 JVM 也完全可以暴露这样的 API。

  1. 能处理不同语言之间的内存模型与线程 -- 关键!

JNI 的关键任务之一是屏蔽 Java 和 native 之间的差异:

  • Java 有 GC
  • Native 没 GC
  • Java 对象布局虚拟机内部可变(ART 和 HotSpot 都不同)
  • Native 指针是裸指针

JNI 的意义就是把所有复杂性隐藏在虚拟机内部,让开发者只需要用有限的 API 交互。

三、为什么说“任何语言都能与 Java 互操作”?

因为 JNI 是一个语言无关的规范。-- 一种规范,一种协议,并不绑定语言

只要一个语言具备 FFI 能力(可以加载动态库、调用 C 函数),它就能调用 Java。

例如:

  • Go → 使用 cgo 访问 JNI
  • Python → ctypes 调 JNI
  • Rust → bindgen + jni crate
  • C# → P/Invoke
  • Swift → SwiftFFI
  • 汇编 → 手写调用 JNI

这说明:

Java 与 native 的互调本质上是 FFI,而不是“JVM 用 C 写的”产生的。

四、那么 JVM 用 C/C++ 编写与 JNI 有什么关系?

关系只有:

C/C++ 让实现 JNI 更容易,但不决定是否能实现 JNI。

举例:

  • JVM 要加载 .so,C/C++ 有成熟 API(dlopen)
  • JVM 要管理线程,C/C++ 有 pthread
  • JVM 要处理内存,C/C++ 直接访问 OS 内存管理

换句话说:

JVM 用 C/C++ 写,让 JNI 的实现更方便,但不是 Java 能否调用 C/C++ 的根本原因。

就像:

  • 浏览器大多用 C++ 写
  • 但网页能执行 JS,不是因为浏览器是 C++ 写的,而是因为浏览器实现了 JS 引擎规范

同理:

  • JVM 恰好用 C++ 写
  • Java 能调 Native,是因为 JVM 实现了 JNI 规范,而不是因为 JVM 用 C++ 写的