Author: geneblue
Blog: https://geneblue.github.io/
在使用NDK的时候,发现自己对jNI机制并不是很了解,就把官方文档拿来看了看并翻译了一下。官方文档地址看这里。自己也是首次翻译英文资料,有翻译不妥的地方请联系我,我将及时更改。
本篇开始介绍JNI(Java Native Interface)技术。JNI是一种本地编程接口。它允许运行在java VM 中的java代码与使用C/C++或汇编等其它编程语言写成的应用或者库进行交互操作。
JNI机制能够存在的最重要一点在于利用了JVM的基础实现是没有被限制死。因此,JVM的构建者可以增添对JNI的支持而不会影响到VM的其他部分。开发人员在编写完一个版本的native程序或库后就可以在所有支持JNI技术的JVM中运行。
本篇包括以下几个话题:
- Java Native Interface Overview
- Background
- Objectives
- Java Native Interface Approach
- Programming to the JNI
- Changes
Java Native Interface Overview
当使用java语言开发应用程序时,我们常常遇到java语言无法满足程序需求的情况。所以在应用程序无法完全用java语言实现时,开发人员就会使用JNI编写java本地方法来解决程序需求。
在以下情形中可能会用到java本地方法:
- 标准的java类库不支持程序需求的平台特性
- 希望通过java语言直接使用由其它语言编写的库
- 希望通过低级语言如汇编来实现计算复杂度较高的部分代码,依次来提高计算效率
通过JNI编程,可以使用本地方法来达到以下目的:
- 创建、检查和更新java对象(包括arrays和strings)
- 调用java方法
- 捕获和抛出异常
- 加载类和获得类信息
- 执行运行时的类型检查
也可以通过JNI的Invocation API将任意一个本地程序嵌入到java VM中。这使得开发者可以很容易地将已存在的程序成为可以被java语言使用的(java-enabled)而且不需要与VM的源码相关联。
Historical Background
不同构造者的VM可能会提供不同的本地方法接口。在特定平台,这些不同的接口会造成开发人员开发、维护和分发不同版本的本地方法库。
我们简要地列举一些本地方法接口:
- JDK1.0本地方法接口
- Netscape java运行接口
- Microsoft's Raw本地接口和Java/COM接口
JDK 1.0本地方法接口
JDK 1.0中首次引入了本地方法接口。不幸的是,有两个主要问题导致该版本的接口无法对其它的java VM适用。
首先,本地代码是以C结构体成员的形式进入到java对象的字段中。但是,java语言的说明文档中没有定义对象在内存中是如何分配的。如果一个java VM以不同的方式在内存中分配对象,那么开发人员就不得不重新编译本地方法库。
其次,JDK 1.0本地方法接口依赖较为保守的垃圾收集策略。比如无节制地使用unhand macro就会导致垃圾收集器以一种较为保守的方式扫描本地方法的内存栈。
Java 运行接口
Netscape曾推荐的JRI(The Java Runtime Interface)对于JVM提供的服务来说是一种较通用的接口。本质上,JRI被设计的容易移植,它在基本的JVM的实现细节上做了些许改动。JRI机制解决了很多的问题,比如本地方法,调试,反射,嵌入(invocation)等。
Raw本地接口和Java/COM接口
Microsoft的JVM支持两种本地方法接口。在低等级中,它提供了高效的RNI(Raw Native Interface)。在JDK的本地方法接口方面,RNI机制提供了高度的源码级向后兼容性。但是它有一个重要的不同点,本地代码必须使用RNI的函数来精确地与垃圾收集器交互,而不是依赖保守的垃圾收集策略。
在高等级中,微软的Java/COM接口为JVM提供了一个不受语言约束的单独的二进制接口。java代码即使是java对象也可以使用COM对象。同样地,一个java类也会以COM类的形式对系统的剩余部分暴露。
Objectives
我们相信提供一个统一的经过深思熟虑的单独接口可以为所有人带来以下优点:
- 每一个VM的构建者都能够支持更多的本地代码
- 编译工具提供商也不必再维护针对不同本地方法接口的版本工具
- 应用程序开发者可以一次编译本地代码并在所有的JVM上运行
标准化本地方法接口的最好办法就是召集所有与JVM相关的厂商和人员。因此,在关于java授权中标准本地接口的设计方面,我们组织了一些列交流会。交流会上一致认同标准的本地方法接口必须满足以下几点要求:
- 二进制兼容性-二进制兼容性的基本目标是在一个平台上本地方法库可以适用于所有的JVM。开发人员也只需维护该平台上的一个版本的本地方法库。
- 效率-为了支持计算复杂的代码,本地方法接口必须利用小部分的上层代码。所有确保VM独立性(保证二进制兼容性)的技术都涉及了一定数量的上层代码。我们必须知道如何在效率和VM独立性之间折中。
- 泛函数-为了本地方法等顺利完成任务,接口必须能揭示足够多的JVM内部构件
Java Native Interface Approach
我们希望采用现存的接口来作为标准接口,这样可以减少大量的工作,因为不同VM里的接口是不同的。不幸的是,没有一个现存的途径可以满足标准接口的需求。
Netscape的JRI是一种轻便的本地方法接口,它起初是最靠近我们的设想,而且也在我们设计之初被采用。对JRI较熟悉的读者会觉得在API命名约定,方法和字段的使用,本地和全局引用的使用等都是非常相似的。尽管我们力推JRI,但是JRI(尽管虚拟机可以同时支持JRI和JNI)并不支持二进制兼容性。
Microsoft的RNI对JDK 1.0做了改善因为它解决了本地方法和垃圾收集器之间的问题。但是RNI不满足虚拟机的独立性,就像JDK,RNI的本地方法仍然采用C结构体的形式进入java对象。这导致两个主要问题:
- RNI对本地代码暴露了java对象的内部布局
- 以C结构体的形式直接进入java对象可能会造成大量的“写障碍”,在高版本的垃圾收集算法中尤为突出。
作为二进制独立的COM可以确保在不同虚拟机中二进制兼容性。只能通过间接方式调用COM的方法,因为COM中也设计了部分上层代码。此外,在解决版本问题方面,COM对象相比动态链接库有了极大的提升。
但是采用COM作为标准本地方法接口也存在几点障碍:
- 第一,Java/COM接口缺乏几个重要功能,例如获取私有的成员字段和提供通用的异常。
- 第二,Java/COM接口自动为java对象提供标准的IUnknown和IDispatch COM接口,所以本地代码可以获取到公有的方法和字段。不幸的是,IDispatch接口没有解决java方法过度加载和匹配函数名大小写不敏感的问题。而且,所有通过IDispatch接口暴露的java方法都会被重新封装执行动态类型检查。这是因为在本质上IDispatch接口本设计成弱类型的语言。
- 第三,COM被设计为允许软件模块(包括功能完善的应用)协同工作,而不是解决单个的低级功能。我们认为让所有的java类和低层次本地方方法作为软件模块是不合适的。
- 第四,COM缺乏对UNIX平台的支持也是一个阻碍因素
尽管作为COM对象的java对象没有对本地代码暴露,但JNI接口本身对COM具备二进制兼容性。JNI采用COM采用的跳转表结构和调用约定。这意味着,一旦COM满足了跨平台特性,对JVM来说JNI就可以成为一个COM接口。
JNI并没有被设计为针对特定JVM的唯一的本地方法接口。一个标准的接口是有利于开发者的,这是的能在不同的JVM中加载同一个本地代码库。在一些情况下,开发者可能会使用低级的,特定虚拟机的接口来达到更高的效率。在其它情况下,开发者会采用高级接口来构建软件模块。从长远来看,软件技术变得更加成熟,本地方法会渐渐失去它们的意义。
Programming to the JNI
开发本地方法的人员应该学会JNI编程。JNI编程可以做到编程与终端用于的VM相隔离。通过标准化JNI,你将会更好地在JVM中使用本地方法库。
如果你是JVM的构建者,那么你应该在JVM中实现JNI机制。经过时间的检验JNI在VM的实现上不会牵涉任何上层代码而且没有什么限制,包括对象表述,垃圾收集机制等。如果你发现我们遗漏的问题请给我们反馈。
Changes
在Java SE 6.0中,一直被反对的JDK1_1InitArgs和JDK1_1AttachArgs结构已经被移除,取而代之的是JavaVMInitArgs和JavaVMAttachArgs结构。