JNI Local Reference Changes in ICS
당신이 native code 를 쓰는 것이 아닌 native 함수만 사용한다면 그만 읽어도 됩니다. 하지만, 당신이 JNI 를 이용해서 native code 를 사용한다면, 당신은 이것을 읽는 것이 큰 도움이 될 것입니다.
What's changing, and why?
모든 개발자들은 좋은 Garbage Collector 를 원합니다. 최고의 GC 는 object 를 마구 조종합니다. 이를 통해 가볍게 memory allocation 을 할 수도 있고, bulk deallocation 도 할 수 있으며, heap fragmentation 도 막을 수 있습니다. 게다가 locality 를 향상시켜 속도도 향상 시킬 수 있습니다. 만약 JNI 단으로 pointer 를 그대로 전달한다면 GC의 object 를 조종하는 행동에 제약을 걸게 됩니다. JNI 는 pointer 를 직접 전달하기보다는, jobject 같은 type 을 씀으로서 이런 문제를 해결합니다. 이 jobject 는 opaque reference 로 원할 때 pointer 로 교환될 수 있습니다.
이 opaque reference 를 사용하기 때문에 GC 가 object 를 이사시킬 때, 그저 pointer 주소 table 을 수정해주기만 하면 됩니다. 다시 말해, GC 가 돈다고 해서 native code 가 dangling pointer 를 가지고 있는 것은 아니라는 의미입니다.
Ice Cream Sandwich 전의 targetSdkVersion 을 가지고 있는 앱들이라면 기존의 JNI 코드들을 그대로 사용해도 상관이 없습니다만, Ice Cream Sandwich부터는 JNI bug compatibility mode 가 생겨서 code 를 수정해야 합니다.
CheckJNI 는 Ice Cream Sandwich 에서의 error 를 찾아내고 report 할 수 있도록 update 되어 있습니다. CheckJNI 는 debuggable="true" 라면 Ice Cream Sandwich 부터는 기본적으로 켜져 있습니다.
A quick primer on JNI references
JNI 에는 여러개의 reference 종류가 있습니다. 그 중에서 가장 중요한 것이 local reference 와 global reference 입니다. 어떤 jobject 가 주어졌다면 그것은 local 혹은 global reference 입니다. ( 물론 weak global reference 도 있지만, 여기서는 다루지 않겠습니다. 주제와 연관이 없으니깐요. )
global/local reference 는 lifetime 과 scope 의 측면에서 차이가 있습니다. global reference 는 JNIEnv* 를 통해서 어떤 thread 에서든 사용할 수 있으며, DeleteGlobalRef() 가 명시적으로 불리기 전까지는 계속 유효합니다. local 의 경우는 생성한 thread 에서만 유효하며, DeleteLocalRef() 가 불리기 전까지 혹은 native method 가 return 하기 전까지 유효하죠. ( 우리는 native method return을 통한 해제가 더 익숙하죠. ). native method return 발생시, 모든 local reference 는 자동적으로 삭제됩니다.
이전 system 에서는, local reference 들은 direct pointer 였으며, 절대 무료화되지 않았습니다. 다시 말해 명시적으로 DeleteLocalRef() 를 호출하거나 암시적으로 PopLocalFrame() 을 호출하기 전까지는 무한정 사용할 수 있었다는 것입니다.
비록 JNIEnv* 가 하나의 thread 에서만 유효하지만, 안드로이드가 JNIEnv* 에 per-thread state 를 기술하지 않았기 때문에 다른 thread 에서 사용되는 경우가 있었습니다. 이제는 per-thread local reference table 이 생겼습니다. 그래서 이제는 "무조건" JNIEnv* 를 알맞은 thread 에서만 사용해야 합니다.
이것이 ICS 가 detect 해낼 버그입니다. 이제부터 이 문제에 대해 여러가지 일반적인 case 들을 함께 살펴보며, 어떻게 고쳐야 하는지 알아보겠습니다. 중요한 것은 "당신이" 고칠 수 있어야 한다는 것입니다 왜냐하면 미래의 Android release ( Ice Cream Sandwich 를 비롯한 추후 버전 ) 에서는 object 를 이사시키는 작업을 더 강력히 추진할 것이기 때문에 이 bug-compatibility mode 를 계~속 지원하는 것은 불가능합니다.
Common JNI reference bugs
Bug : Forgetting to call NewGlobalRef() when stashing a jobject in a native peer.
만약 당신이 native peer ( 오래 존재하는 java object 에 매칭되는 native object 로 보통 Java object 가 생성될 때 생성되고, Java object 의 finalizer가 실행될 때 함께 파괴되는 ) 를 가지고 있다면, 당신은 그것을 jobject 에 넣어놔서는 안됩니다. 왜냐하면 당신이 다음에 사용하려고 한다면 그 때는 유효하지 않을테니깐요. ( JNIEnv* 도 마찬가지입니다. 같은 thread 에서 다시 해당 함수를 호출할 때는 유효할"찌도" 모르겠습니다만, 그 외의 경우에서는 유효하지 않습니다. )
class MyPeer {
public:
MyPeer(jstring s) {
str_ = s; // Error: stashing a reference without ensuring it’s global.
}
jstring str_;
};
static jlong MyClass_newPeer(JNIEnv* env, jclass) {
jstring local_ref = env->NewStringUTF("hello, world!");
MyPeer* peer = new MyPeer(local_ref);
return static_cast<jlong>(reinterpret_cast<uintptr_t>(peer));
// Error: local_ref is no longer valid when we return, but we've stored it in 'peer'.
}
static void MyClass_printString(JNIEnv* env, jclass, jlong peerAddress) {
MyPeer* peer = reinterpret_cast<MyPeer*>(static_cast<uintptr_t>(peerAddress));
// Error: peer->str_ is invalid!
ScopedUtfChars s(env, peer->str_);
std::cout << s.c_str() << std::endl;
}
JNI global reference 를 이용하기만 하면 위 코드의 문제점들을 쉽게 해결할 수 있습니다. 왜냐하면 global reference 는 자동으로 절대 해지되지 않으며, 당신이 "반드시" 해제해야 하는 것이기 때문입니다. 당신의 destructor 가 JNIEnv* 를 가지고 있지 않았다는 사실 때문이 약간 이상하게 보일 수는 있습니다. 가장 쉬운 방법은 "destroy" 함수를 만들고, Java peer 의 finalizer 에서 이 함수를 호출하게 하는 것이 좋습니다.
class MyPeer {
public:
MyPeer(JNIEnv* env, jstring s) {
this->s = env->NewGlobalRef(s);
}
~MyPeer() {
assert(s == NULL);
}
void destroy(JNIEnv* env) {
env->DeleteGlobalRef(s);
s = NULL;
}
jstring s;
};
당신은 반드시 NewGlobalRef() 와 DeleteGlobalRef() 가 pair 를 이루도록 해야 합니다. CheckJNI 는 global reference 의 leak 을 잡아낼 것입니다. 하지만 leak 의 제한선이 상당히 높기 때문에, 평소에 조심하는 것이 좋겠죠.
만약 당신이 이런 에러를 고치지 않는다면, 이런 형태의 에러에 직면하게 될 것입니다.
JNI ERROR (app bug): accessed stale local reference 0x5900021 (index 8 in a table of size 8)
JNI WARNING: jstring is an invalid local reference (0x5900021)
in LMyClass;.printString:(J)V (GetStringUTFChars)
"main" prio=5 tid=1 RUNNABLE
| group="main" sCount=0 dsCount=0 obj=0xf5e96410 self=0x8215888
| sysTid=11044 nice=0 sched=0/0 cgrp=[n/a] handle=-152574256
| schedstat=( 156038824 600810 47 ) utm=14 stm=2 core=0
at MyClass.printString(Native Method)
at MyClass.main(MyClass.java:13)
만약 당신이 다른 thread 에서 JNIEnv* 를 사용한다면 다음과 같은 error 를 보게 될테죠.
JNI WARNING: threadid=8 using env from threadid=1
in LMyClass;.printString:(J)V (GetStringUTFChars)
"Thread-10" prio=5 tid=8 NATIVE
| group="main" sCount=0 dsCount=0 obj=0xf5f77d60 self=0x9f8f248
| sysTid=22299 nice=0 sched=0/0 cgrp=[n/a] handle=-256476304
| schedstat=( 153358572 709218 48 ) utm=12 stm=4 core=8
at MyClass.printString(Native Method)
at MyClass$1.run(MyClass.java:15)
Bug : Mistakenly assuming FindClass() returns global references
FindClass() 는 local reference 를 return 합니다. 하지만 많은 사람들이 global reference 를 return 할 것이라고 생각합니다. class unloading 이 없는 system에서는 jfieldID 와 jmethodID 는 global 처럼 사용할 수는 있습니다. ( 사실 그녀석들은 global reference 가 아니지만, system unloading 이 일어나지 않는 system 에서는 비슷한 life time 을 갖기 때문에 그렇게 써도 된다는 것이죠. ). 쉽게 하는 실수는 "static jclass" 에 FindClass() 의 return 값인 jclass 를 바로 link 할 때입니다. local reference 를 직접 global reference 로 바꿔주지 않고 link 시킨다면 그 코드는 잘못된 코드입니다. 다음 코드는 이 문제를 발생시키지 않는 방법을 제공합니다.
static jclass gMyClass;
static jclass gSomeClass;
static void MyClass_nativeInit(JNIEnv* env, jclass
myClass) {
// ‘myClass’ (and any other non-primitive arguments) are only local references.
gMyClass = env->NewGlobalRef(myClass);
// FindClass only returns local references.
jclass someClass = env->FindClass("SomeClass");
if (someClass == NULL) {
return; // FindClass already threw an exception such as NoClassDefFoundError.
}
gSomeClass = env->NewGlobalRef(someClass);
}
만약 이 문제에 직면하면 에러는 다음과 같이 나올겁니다.
JNI ERROR (app bug): attempt to use stale local reference 0x4200001d (should be 0x4210001d)
JNI WARNING: 0x4200001d is not a valid JNI reference
in LMyClass;.useStashedClass:()V (IsSameObject)
Bug : Calling DeleteLocalRef() and continuing to use the deleted reference.
DeleteLocalRef() 를 호출한 reference 를 계속 해서 사용하는 것은 말할 것도 없이 잘못된 코드입니다. 하지만 그동안은 그냥 작동이 되곤 했습니다. 그래서 당신이 행한 실수를 찾을수도 없었고, 인식하지 못했을 수 있습니다. 보통 하는 실수는 native code 에서 오랫동안 도는 loop에서 개발자들이 local reference 의 갯수 limit 넘지 않도록 local reference 를 사용 후 바로바로 해제하는 과정에서 발생합니다. 이 때 실수로 return value 도 함께 삭제하는 실수가 발생합니다.
고치는 방법은 매우 쉽습니다. 사용하려고 하는 reference에 DeleteLocalRef() 를 호출하지 마세요.( return 할 reference 에 특히 조심하세요. )
Bug : Calling PopLocalFrame() and continuing to use a popped reference.
이 문제는 앞서 다뤘던 문제에 비해 조금 더 변형된 형태입니다. PushLocalFrame() 과 PopLocalFrame() 은 대량의 local reference 를 삭제할 수 있게 도와줍니다. PopLocalFrame() 을 호출할 때 frame 에서 사용하던 reference 중 하나늘 parameter 로 전달합니다. 이 때 전달된 local reference 는 delete 되지 않고 유지됩니다. ( 보통 이 value 가 return value 가 되죠. ) 이 parameter 에 NULL 을 물론 넣을 수도 있습니다. 이전에는 다음과 같은 잘못된 방법을 사용하기 일쑤였습니다.
static jobjectArray MyClass_returnArray(JNIEnv* env,
jclass) {
env->PushLocalFrame(256);
jobjectArray array = env->NewObjectArray(128, gMyClass, NULL);
for (int i = 0; i < 128; ++i) {
env->SetObjectArrayElement(array, i, newMyClass(i));
}
env->PopLocalFrame(NULL); // Error: should pass 'array'.
return array; // Error: array is no longer valid.
}
고치는 방법은 PopLocalFrame() 의 parameter 로 reference 를 던달하는 것입니다. 위의 예제에서 모든 array element 의 reference 를 유지하지 않는 다는 사실을 눈여겨 보세요..GC 가 array 를 collect 하기 전에는 array가 point 하고 있는 element 들은 GC 의 대상이 되지 않습니다. ( 어떤 object든 pointing 당하고 있다면 ).
이것 관련 에러가 발생하면 다음과 같은 error 를 보실 수 있을겁니다.
JNI ERROR (app bug): accessed stale local reference 0x2d00025 (index 9 in a table of size 8)
JNI WARNING: invalid reference returned from native code
in LMyClass;.returnArray:()[Ljava/lang/Object;
그렇습니다. JNI coding 을 할 때 조금 더 주의를 기울여야 합니다. 하지만 우리는 당신이 곧 더 복잡하지만 더 좋은 메모리 관리를 가능하게 한다는 그 장점을 깨닫게 될 것이라 생각합니다.
댓글