dalvik jni解析

android开发者在移植第三方native库时,经常会使用到jni.
Java Native Interface(java本地接口),用于java与c/c++代码进行交互.需要了解的是,jni只是jvm的一个规范,各家虚拟机有各自的实现,本文分享一下dalvik中jobject reference的实现.

jobject reference type

jni规范中,对jobject定义了两种reference类型:

  • 全局引用(global reference)
  • 局部引用(local reference)

在JNI Specification中的说明是:

Local references are valid for the duration of a native method call, and are automatically freed after the native method returns. Global references remain valid until they are explicitly freed

局部引用的生命周期只在当前native函数上下文中有效;全局引用则虚拟机全局生效,直置开发者显式释放; dalvik额外定义了weak global reference,弱全局引用的weak与java层面中weak reference概念一致:在gc时,会被自动回收; android4.0版本发布之后,android官方博客有这样一篇文章jni local reference changes in ics

The best garbage collectors move objects around. This lets them offer very cheap allocation and bulk deallocation, avoids heap fragmentation, and may improve locality. Moving objects around is a problem if you’ve handed out pointers to them to native code. JNI uses types such as jobject to solve this problem: rather than handing out direct pointers, you’re given an opaque handle that can be traded in for a pointer when necessary. By using handles, when the garbage collector moves an object, it just has to update the handle table to point to the object’s new location. 

意思是说,为了避免内存碎片,虚拟机在gc过程中应该对堆对象进行内存整理,将堆中分散的指针压缩集中在一处;在之前的版本中,jobject是一个直接指针,直接指向了堆中的内存地址,因此无法采用压缩算法;又由于jobject是一个直接指针,之前的版本也未能实现GetObjectRefType接口;所以,我们现在采用对象句柄间接指向堆对像,解决了以上的问题;

那么问题来了,何为对象句柄? 对象句柄又如何区分reference类型?

IndirectRefTable

首先明确几个类型的定义:

typedef void*           jobject;

jobject是一个void指针,在arm下,长32位

typedef uint16_t            u2;
typedef uint32_t            u4;

dalvik虚拟机几个内部类型的定义,u4代表无符号32位

By using handles, when the garbage collector moves an object, it just has to update the handle table to point to the object’s new location.

官方博客的文章中提到的handle table,对应到dalvik的实现,就是IndirectRefTable

虚拟机维护了一个全局引用表jniGlobalRefTable,为每一个线程都维护一个局部引用表jniLocalRefTable,两者采用此相同的数据结构.IndirectRefTable的表元素称之为IndirectRefSlot

struct IndirectRefSlot {
   Object* obj;    /* object pointer itself, NULL if the slot is unused */
   u4  serial;     /* slot serial number */
};

IndirectRefSlot只有两个字段,obj即直接对象指针,serial是一个自增的无符号数,主要用于校验对比;
jni.h中将jobject定义成void指针,在虚拟机内部,jobject被表示为IndirectRef,仍然是一个void指针,不过虚拟机并不关心其类型,只需要明确的是,void指针有32位长;

typedef void* IndirectRef;

基于以上几个定义,获取一个jobject的流程可以转换成:构造一个IndirectRefSlot对象,添加到IndirectTable表中,获取索引值,并最终返回IndirectRef指针.
索引转换成IndirectRef的函数实现如下:

 static inline IndirectRef toIndirectRef(u4 index, u4 serial, IndirectRefKind kind) {
         return reinterpret_cast<IndirectRef>((serial << 20) | (index << 2) | kind);
  }

IndirectRef由3部分组成:

  • IndirectRefKind 即global/local/weak global,占最后2位;
  • index索引 占18位;
  • serial 校验位

通过这样一个位移运算,对象类型和对象表索引放到一个32位指针中;
当获取一个jobject对象类型时,可以直接判断jobject最后两位,但当操作jobject对象时,则需要额外的寻址操作,句柄的装包解包其实也是jni效率低下的原因之一.
有表的添加,自然有表对象的移除,当在IndirectRefTable表中删除一个jni引用时候,会将对应IndirectRefSlot的obj对象置为null,二次添加时,优先添加到空位IndirectRefSlot.

IndirectRefTable的压栈出栈

java函数分为普通函数和本地函数,虚拟机栈也分为普通栈和jni栈.虚拟机在编译期可以确定普通函数中局部变量的个数,当一个普通栈被压栈时,压栈的内存大小为栈StackSaveArea本身大小以及局部变量的内存大小;
而jni栈,虚拟机并不知道栈中会有多少个局部变量,jni栈只包含StackSaveArea的内存大小,jobject统一交给IndirectRefTable管理,因此,IndirectRefTable也需要压栈出栈的概念;
StackSaveArea有一个xtra union变量:

struct StackSaveArea;
....
union {
        /* for JNI native methods: bottom of local reference segment */
        u4          localRefCookie;

        /* for interpreted methods: saved current PC, for exception stack
          * traces and debugger traces */
        const u2*   currentPc;
} xtra;
  ...

从注释可以看出,java method模式下,xtra用于表示解释器的pc值;jni method,用于表示bottom of local reference segment
对于segment,我们可以理解成一个与栈类似的结构,IndirectRefTable在压栈时,会将IndirectRefTable的IRTSegmentState变量,赋值给xtra.localRefCookie

union IRTSegmentState {
  u4          all;
  struct {
         u4      topIndex:16;            /* index of first unused entry */
         u4      numHoles:16;            /* #of holes in entire table */
   } parts;
}

注意IRTSegmentState是一个32位长的union,从注释可以看出,前16位用于记录当前IndirectRefTable的索引位,后16位用于记录当前栈之前,IndirectRefTable的空位个数. 采取这种设计的原因是为了快速的出栈,虚拟机可以直接从StackSaveArea中取到上一个栈的topIndex,重置回IndirectRefTable表,而不需要逐一回退.
jni api有一对pushLocalFrame/popLocalFrame函数,也是利用了IRTSegmentState进行本地引用的批量删除.

垃圾回收

虽然在官方blog中说明采取间接指针的目的是为了进行内存压缩,但在最后几个dalvik版本中,采取的仍是mark-sweep算法(art模式采取mark-copy,真正实现了对堆内存的整理).mark过程中,虚拟机会分别在mark rootSet以及Mark Thread root阶段对jniGlobalRefTable和jniLocalRefTable进行标记,避免GC回收.

本文来自网易实践者社区,经作者郑文授权发布。