这是JNI实现源码分析系列文章中的一部分,本系列文章结合Dalvik源码来说明JNI实现上的细节,本系列包括:
JNI实现源码分析【一 前言】
JNI实现源码分析【二 数据结构】
JNI实现源码分析【三 间接引用表】
JNI实现源码分析【四 函数调用】
JNI实现源码分析【五 结束语】
正文
在JNI实现源码分析【二 数据结构】的参数传递一节中,我们提到,JNI为了安全性的考虑使用了形如jobject的结构来传递参数。而jobject被表述为指针,但又不是直接指向Object的指针那么jobject是如何和真正的Object建立联系呢?
在JNI的API中,有一组API Global and Local References,这里的References又是什么?为啥会有这一组API?
答案都和间接引用表(IndirectRefTable)有关
0x01: IndirectRefTable
源码见IndirectRefTable.h
代码很复杂,等效理解就可以了,其作用就是一张保存了间接引用的表。让jobject和Object建立起联系。
0x02: 作用域
在JNI中,有两个不同的作用域:全局作用域(进程级别)和线程作用域(线程级别)。这两个作用域分别有自己的间接引用表。
全局作用域的间接引用表保存在gDvm.jniGlobalRefTable
中。gDvm
是一个全局变量,在虚拟机启动的时候就创建。
线程作用域的间接引用表保存在thread.jniLocalRefTable
中。和线程绑定,线程创建时创建,线程销毁时销毁。
JNI API中的全局引用和局部引用,指的就是全局作用域的间接引用表和线程作用域的间接引用表。
于是:
jobject NewGlobalRef(JNIEnv *env, jobject obj);
void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
我们就是操作了全局引用表
而:
jobject NewLocalRef(JNIEnv *env, jobject ref);
void DeleteLocalRef(JNIEnv *env, jobject localRef);
我们操作了线程引用表
让我们再来看看两个表的大小,在创建的时候,就指定了其大小:
#define kGlobalRefsTableInitialSize 512
#define kGlobalRefsTableMaxSize 51200
if (!gDvm.jniGlobalRefTable.init(kGlobalRefsTableInitialSize,
kGlobalRefsTableMaxSize,
kIndirectKindGlobal)) {
return false;
}
可以看到,全局引用表的初始大小为512,最大为51200。
#define kJniLocalRefMin 64
#define kJniLocalRefMax 512
if (!thread->jniLocalRefTable.init(kJniLocalRefMin,
kJniLocalRefMax, kIndirectKindLocal)) {
return false;
}
而局部引用表的初始大小为64,最大为512。 这里顺便提一下,当超过这个最大时,就会报local reference table overflow (max=512)
的错误。
0x03: jobject到Object的映射
到现在,我们应该可以顺理成章的理解到,jobject到Object的映射借用了间接引用表,没错!
我们来分析局部引用,全局引用是类似的。
static inline jobject addLocalReference(Thread* self, Object* obj) {
if (obj == NULL) {
return NULL;
}
IndirectRefTable* pRefTable = &self->jniLocalRefTable;
void* curFrame = self->interpSave.curFrame;
u4 cookie = SAVEAREA_FROM_FP(curFrame)->xtra.localRefCookie;
jobject jobj = (jobject) pRefTable->add(cookie, obj);
if (UNLIKELY(jobj == NULL)) {
AddLocalReferenceFailure(pRefTable);
}
if (UNLIKELY(gDvmJni.workAroundAppJniBugs)) {
// Hand out direct pointers to support broken old apps.
return reinterpret_cast<jobject>(obj);
}
return jobj;
}
非常明了的代码,我们使用了pRefTable->add
将实际对象添加到了间接引用表,从而获取了jobject的间接引用。
我们看一下add
做了啥:
IndirectRef IndirectRefTable::add(u4 cookie, Object* obj)
{
IRTSegmentState prevState;
prevState.all = cookie;
size_t topIndex = segmentState.parts.topIndex;
assert(obj != NULL);
assert(dvmIsHeapAddress(obj));
assert(table_ != NULL);
assert(alloc_entries_ <= max_entries_);
assert(segmentState.parts.numHoles >= prevState.parts.numHoles);
/*
* We know there's enough room in the table. Now we just need to find
* the right spot. If there's a hole, find it and fill it; otherwise,
* add to the end of the list.
*/
IndirectRef result;
IndirectRefSlot* slot;
int numHoles = segmentState.parts.numHoles - prevState.parts.numHoles;
if (numHoles > 0) {
assert(topIndex > 1);
/* find the first hole; likely to be near the end of the list,
* we know the item at the topIndex is not a hole */
slot = &table_[topIndex - 1];
assert(slot->obj != NULL);
while ((--slot)->obj != NULL) {
assert(slot >= table_ + prevState.parts.topIndex);
}
segmentState.parts.numHoles--;
} else {
/* add to the end, grow if needed */
if (topIndex == alloc_entries_) {
/* reached end of allocated space; did we hit buffer max? */
if (topIndex == max_entries_) {
ALOGE("JNI ERROR (app bug): %s reference table overflow (max=%d)",
indirectRefKindToString(kind_), max_entries_);
return NULL;
}
size_t newSize = alloc_entries_ * 2;
if (newSize > max_entries_) {
newSize = max_entries_;
}
assert(newSize > alloc_entries_);
IndirectRefSlot* newTable =
(IndirectRefSlot*) realloc(table_, newSize * sizeof(IndirectRefSlot));
if (table_ == NULL) {
ALOGE("JNI ERROR (app bug): unable to expand %s reference table "
"(from %d to %d, max=%d)",
indirectRefKindToString(kind_),
alloc_entries_, newSize, max_entries_);
return NULL;
}
memset(newTable + alloc_entries_, 0xd1,
(newSize - alloc_entries_) * sizeof(IndirectRefSlot));
alloc_entries_ = newSize;
table_ = newTable;
}
slot = &table_[topIndex++];
segmentState.parts.topIndex = topIndex;
}
slot->obj = obj;
slot->serial = nextSerial(slot->serial);
result = toIndirectRef(slot - table_, slot->serial, kind_);
assert(result != NULL);
return result;
}
我擦,真的是太复杂了,里面肯定包含了某个算法,反正就是通过参数cookie,通过slot等,在表的合适位置引用了真正的Object,然后返回了一个值(间接引用),后续通过这个值,能够去表里面的这个位置找到Object。
所以之前说过,jobject并不是直接指向Object的指针。甚至它并不是真正的地址,它仅仅是表的间接引用。
让我们继续看看,如何通过这个间接引用找到真实的Object吧:
Object* dvmDecodeIndirectRef(Thread* self, jobject jobj) {
if (jobj == NULL) {
return NULL;
}
switch (indirectRefKind(jobj)) {
case kIndirectKindLocal:
{
Object* result = self->jniLocalRefTable.get(jobj);
if (UNLIKELY(result == NULL)) {
ALOGE("JNI ERROR (app bug): use of deleted local reference (%p)", jobj);
ReportJniError();
}
return result;
}
case kIndirectKindGlobal:
{
// TODO: find a way to avoid the mutex activity here
IndirectRefTable* pRefTable = &gDvm.jniGlobalRefTable;
ScopedPthreadMutexLock lock(&gDvm.jniGlobalRefLock);
Object* result = pRefTable->get(jobj);
if (UNLIKELY(result == NULL)) {
ALOGE("JNI ERROR (app bug): use of deleted global reference (%p)", jobj);
ReportJniError();
}
return result;
}
case kIndirectKindWeakGlobal:
{
// TODO: find a way to avoid the mutex activity here
IndirectRefTable* pRefTable = &gDvm.jniWeakGlobalRefTable;
ScopedPthreadMutexLock lock(&gDvm.jniWeakGlobalRefLock);
Object* result = pRefTable->get(jobj);
if (result == kClearedJniWeakGlobal) {
result = NULL;
} else if (UNLIKELY(result == NULL)) {
ALOGE("JNI ERROR (app bug): use of deleted weak global reference (%p)", jobj);
ReportJniError();
}
return result;
}
case kIndirectKindInvalid:
default:
if (UNLIKELY(gDvmJni.workAroundAppJniBugs)) {
// Assume an invalid local reference is actually a direct pointer.
return reinterpret_cast<Object*>(jobj);
}
ALOGW("Invalid indirect reference %p in decodeIndirectRef", jobj);
ReportJniError();
return kInvalidIndirectRefObject;
}
}
可以看到,通过jobject可以计算出RefKind,即jobject还包含了类型信息。
真正找Object是在pRefTable->get
:
Object* IndirectRefTable::get(IndirectRef iref) const {
IndirectRefKind kind = indirectRefKind(iref);
if (kind != kind_) {
if (iref == NULL) {
ALOGW("Attempt to look up NULL %s reference", indirectRefKindToString(kind_));
return kInvalidIndirectRefObject;
}
if (kind == kIndirectKindInvalid) {
ALOGE("JNI ERROR (app bug): invalid %s reference %p",
indirectRefKindToString(kind_), iref);
abortMaybe();
return kInvalidIndirectRefObject;
}
// References of the requested kind cannot appear within this table.
return kInvalidIndirectRefObject;
}
u4 topIndex = segmentState.parts.topIndex;
u4 index = extractIndex(iref);
if (index >= topIndex) {
/* bad -- stale reference? */
ALOGE("JNI ERROR (app bug): accessed stale %s reference %p (index %d in a table of size %d)",
indirectRefKindToString(kind_), iref, index, topIndex);
abortMaybe();
return kInvalidIndirectRefObject;
}
Object* obj = table_[index].obj;
if (obj == NULL) {
ALOGI("JNI ERROR (app bug): accessed deleted %s reference %p",
indirectRefKindToString(kind_), iref);
abortMaybe();
return kInvalidIndirectRefObject;
}
u4 serial = extractSerial(iref);
if (serial != table_[index].serial) {
ALOGE("JNI ERROR (app bug): attempt to use stale %s reference %p",
indirectRefKindToString(kind_), iref);
abortMaybe();
return kInvalidIndirectRefObject;
}
return obj;
}
和我们前面想的一样,这里通过一堆的计算后,在Object* obj = table_[index].obj;
处找到了真实的Object。
0x04: JNI在背后默默做的事
在JNI环境中,我们永远接触不了真实的Object对象,上面映射方法是虚拟机内部的,我们在JNI环境也是没法调用的。所以,我们在JNI环境中,使用的都是间接引用,比如jobject,jmethodID等。确实,JNI的所有API都在使用这些间接引用。
那么,这里就有一个问题了,既然间接引用和间接引用表有关,那在使用JNI的API时,获取到这些间接引用时,JNI将真实的对象保存在哪个表里面?
答案是线程引用表,几乎每一个API都有JNIEnv,JNIEnv和线程绑定,可以很容易定位到线程引用表。放到线程应用表,随着线程的销毁,引用表也不会被销毁,不会一直占用空间。
我当初在JNI中想要获取Throwable.printStackTrace时,就因为调用相关的API,然后产生了很多的间接引用,将间接引用表撑爆,报了:local reference table overflow (max=512)
。
除了JNI的默认行为,假如我们想要自己控制引用的生命周期,比如提前删除,将引用放置到全局引用表等,我们可以使用Ref相关的API即可,记住,不用了一定要删除,不要存在引用泄漏。