본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[Android/안드로이드] JNI Local Reference Changes in ICS ( ICS 부터 바뀌는 JNI Local Reference )

by 돼지왕 왕돼지 2012. 4. 6.
반응형



안녕하세요 돼지왕 왕돼지입니다.

오늘은 "JNI Local Reference Changes in ICS" 를 주제로 알아보겠습니다.

이 글은  http://android-developers.blogspot.com/2011/11/jni-local-reference-changes-in-ics.html 내용 번역본입니다. ( 이해가 쉽도록 의역을 많이 넣었습니다. 모호한 내용은 원문을 확인하세요. )


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;




Wrapping up.


그렇습니다. JNI coding 을 할 때 조금 더 주의를 기울여야 합니다. 하지만 우리는 당신이 곧 더 복잡하지만 더 좋은 메모리 관리를 가능하게 한다는 그 장점을 깨닫게 될 것이라 생각합니다.



도움이 되셨다면 손가락 꾸욱~








반응형

댓글