Class初期化:興味深い問題

9678 ワード

1問題の説明


1.1「null」or「Activityインスタンス参照」


次のコードを読んで考えてください:TestStatic.getActivity()の戻り値は「null」ですか、「Activityのインスタンス参照」ですか.
public class TestStatic {
    private static TestStatic sInstance = new TestStatic();
    private static Activity sActivity = null;

    private TestStatic() {
        sActivity  = new Activity();
    }

    public static Activity getActivity() {
        return sActivity;
    }
}

1.2問題分析


この問題は主に仮想マシン内部のクラス初期化プロセスの知識に関連しており、参考:[クラスロードリンク、初期化、インスタンス化](Class Load Links,Initialization,Instance)http://www.jianshu.com/p/0bff0413fd2f)クラスの初期処理には、クラスのロード、リンク、初期化、インスタンス化が含まれます.ここでは主に初期化フェーズに注目し、初期化フェーズは主に静的コードブロックを実行し、静的ドメインメンバーを初期化します.この2つの操作はクラス初期化方法で完了します.分析手順は次のとおりです.
  • TestStaticクラスの静的メソッドgetActivity()を呼び出し、TestStaticクラスの初期化を招き、初期化メソッドを実行する.
  • 初期化方法で静的ドメインメンバーsIntanceを初期化し、TestStaticの構築方法を実行し、構築方法でActivityをインスタンス化し、オブジェクト参照を静的ドメインメンバーsActivityに保存する.
  • は次に実行を継続し、***sActivity***をnullに割り当て、実行を完了する.
  • したがって、最後の静的方法getActivity()はsActivityがnullであることを返す.

  • もしあなたの分析過程もそうであれば、答えが正しいことをおめでとうございますが、あまり早く喜ばないでください.分析過程が間違っているからです.[クラスロードリンク、初期化、インスタンス化](Class Load Links,Initialization,Instance)を真剣に読むと(http://www.jianshu.com/p/0bff0413fd2f)を選択すると、2ステップ目の解析で問題が発生していることがわかります.
    private static TestStatic sInstance = new TestStatic();
    

    ここでTestStaticクラスをインスタンス化すると、newの操作によってTestStaticクラスの初期化が発生します.まだ実行されていないため、クラスの初期化が完了していません.プリペイド方法(バイトコード):
    .method static constructor ()V
              .registers 1
              .prologue
    00000000  new-instance            v0, TestStatic
    00000004  invoke-direct           TestStatic->()V, v0
    0000000A  sput-object             v0, TestStatic->sInstance:TestStatic
    0000000E  const/4                 v0, 0x0
    00000010  sput-object             v0, TestStatic->sActivity:Activity
    00000014  return-void
    .end method
    
    .method private constructor ()V
              .registers 2
              .prologue
    00000000  invoke-direct           Object->()V, p0
    00000006  new-instance            v0, Activity
    0000000A  invoke-direct           Activity->()V, v0
    00000010  sput-object             v0, TestStatic->sActivity:Activity
    00000014  return-void
    .end method
    

    メソッドの最初のバイトコード:
    00000000  new-instance            v0, TestStatic
    

    New-instanceはTestStaticクラスの初期化をトリガーします.すなわち、メソッドで呼び出されます.このようにしてここでデッドサイクルになったのでしょうか.もちろんそうではありません.その真相はどうなっていますか.この問題の背後には,クラス初期化に関する重要な知識点が隠されており,次にこの過程を完全に分析する.

    2クラス初期化


    クラスの初期化を引き起こす条件については、[クラスロードリンク、初期化、インスタンス化](Class Load Links,Initialization,Instance)を参照してください(http://www.jianshu.com/p/0bff0413fd2f)では,第2ステップの解析で問題が発生していることが分かるが,ここではこれ以上述べない.本問題に関連する2つの条件を選んで分析する.
  • 静的メソッドを呼び出す:バイトコード:invoke-static;
  • クラスをインスタンス化し、バイトコードはnew-instanceです.

  • 2.1 invoke-staticバイトコードトリガのクラス初期化


    仮想マシン(Dalvik)は、バイトコードを次のコードブロックで実行するように解釈する(関連しない部分は省略する).
        GOTO_TARGET(invokeStatic, bool methodCallRange)
        EXPORT_PC();
        ...
        methodToCall = dvmDexGetResolvedMethod(methodClassDex, ref);
        if (methodToCall == NULL) {
        methodToCall = dvmResolveMethod(curMethod->clazz, ref, METHOD_STATIC);
        if (methodToCall == NULL) {
            ILOGV("+ unknown method");
            GOTO_exceptionThrown();
        }
        ...
        }
        ...
        GOTO_invokeMethod(methodCallRange, methodToCall, vsrc1, vdst);
        GOTO_TARGET_END
    

    dvmDexGetResolvedMethodはまずodexファイルから呼び出された静的メソッドを見つけ、dvmResolveMethod()は呼び出された静的メソッドが属するクラスが正しくロードされたかどうかを判断して初期化します.そうでなければ、クラスのロード、初期化などの操作がトリガーされます.
    Method* dvmResolveMethod(const ClassObject* referrer, u4 methodIdx,
    MethodType methodType)
    {
        ...
        resClass = dvmResolveClass(referrer, pMethodId->classIdx, false);
        ...
        if (methodType == METHOD_DIRECT) {
              resMethod = dvmFindDirectMethod(resClass, name, &proto);
          } else if (methodType == METHOD_STATIC) {
              resMethod = dvmFindDirectMethodHier(resClass, name, &proto);
          } else {
              resMethod = dvmFindVirtualMethodHier(resClass, name, &proto);
          }
          ...
          /*
           * If we're the first to resolve this class, we need to initialize
           * it now.  Only necessary for METHOD_STATIC.
           */
          if (methodType == METHOD_STATIC) {
              if (!dvmIsClassInitialized(resMethod->clazz) &&
                  !dvmInitClass(resMethod->clazz))
              {
                  assert(dvmCheckException(dvmThreadSelf()));
                  return NULL;
              } else {
                  assert(!dvmCheckException(dvmThreadSelf()));
              }
          } else {
                /*
                 * Edge case: if the  for a class creates an instance
                 * of itself, we will call  on a class that is still being
                 * initialized by us.
                 */
                 assert(dvmIsClassInitialized(resMethod->clazz) ||
                 dvmIsClassInitializing(resMethod->clazz));
          }
          ...
      }
    

    呼び出しdvmIsClassInitialized()は、クラスが正しく初期化されているかどうかを判断し、クラスのstatusがCLASS_にあるかどうかを判断します.INITIALIZED状態.
    /*
     * Determine if a class has been initialized.
     */
    INLINE bool dvmIsClassInitialized(const ClassObject* clazz) {
        return (clazz->status == CLASS_INITIALIZED);
    }
    

    クラスが初期化されていない場合は、dvmInitClass()メソッドを呼び出してクラスを初期化します.
    bool dvmInitClass(ClassObject* clazz)
    {
        ...
        dvmLockObject(self, (Object*) clazz);
        ...
        while (clazz->status == CLASS_INITIALIZING) {
            if (clazz->initThreadId == self->threadId) {
                //ALOGV("HEY: found a recursive ");
                goto bail_unlock;
               }
              ...
              /*
             * Wait for the other thread to finish initialization.  We pass
             * "false" for the "interruptShouldThrow" arg so it doesn't throw
             * an exception on interrupt.
             */
            dvmObjectWait(self, (Object*) clazz, 0, 0, false);
            ...
            if (clazz->status == CLASS_INITIALIZING) {
                ALOGI("Waiting again for class init");
                continue;
            }
            ...
            goto bail_unlock;
        }
        ...
        clazz->initThreadId = self->threadId;
        android_atomic_release_store(CLASS_INITIALIZING,
                                 (int32_t*)(void*)&clazz->status);
        dvmUnlockObject(self, (Object*) clazz);
        ...
        initSFields(clazz);
        
        /* Execute any static initialization code.*/
        method = dvmFindDirectMethodByDescriptor(clazz, "", "()V");
        if (method == NULL) {
            LOGVV("No  found for %s", clazz->descriptor);
        } else {
            LOGVV("Invoking %s.", clazz->descriptor);
            JValue unused;
            dvmCallMethod(self, method, NULL, &unused);
        }
        ...
        bail_unlock:
    
        dvmUnlockObject(self, (Object*) clazz);
    
        return (clazz->status != CLASS_ERROR);
    }
    

    dvmInitClass()実行プロセスを理解することは,この問題を認識する鍵である.コードブロックは関係のない部分を省略し、論理的に読みやすい.
  • クラスの初期化はClasObjectのlockとstatusの状態によって同時初期化クラスの問題を処理する.
  • 最初にこの方法に入った人は、鍵をかけて、他の人は入ることができません.次に、初期化クラスのスレッドIDを設定し、クラスstatusを:CLASS_INITIALIZING、ロック解除.呼び出しinitSFields()は、単純な静的ドメインを初期化し、最後にクラスにメソッドがあるかどうかを見て、ある場合は呼び出します.実行が完了すると、クラスの初期化ステップが完了します.
  • クラスが最初に初期化が完了していない場合にその人がこのメソッドに入り、2つ目の説明では、ロック解除後にwhile(clazz->status==CLASS_INITIALIZING)サイクルに入り、2つのケースに分けて、(1)現在のクラスを初期化しているスレッドであれば、直接終了する;(2)他のスレッドの場合、現在の初期化が完了(成功または失敗)するまでブロックされ、最後に直接終了します.

  • 上の3点の分析に基づいて、前の問題をはっきり説明することができます.で、メソッドを再呼び出しします.このスレッドの場合、後の呼び出しは直接終了します.そうしないと、ブロックされ、デッドループはありません.今回の例では,1番目のケース,すなわち同一スレッドにおけるループ呼び出し方法に属する.第2のケースは、マルチスレッドが1つの*Classを同時に初期化すると、仮想マシンが初期化操作が完了するまでブロックを選択します.Javaコードを書くときは、仮想マシンが手伝ってくれたので、このような初期化が同時に発生する問題を処理する必要はありません.

    2.2 new-instanceトリガのクラス初期化


    New-instanceは、仮想マシンによってコードブロックとして以下のように解釈される.
    HANDLE_OPCODE(OP_NEW_INSTANCE /*vAA, class@BBBB*/)
    {
        ...
        clazz = dvmDexGetResolvedClass(methodClassDex, ref);
        ...
        if (!dvmIsClassInitialized(clazz) && !dvmInitClass(clazz))
            GOTO_exceptionThrown();
        ...
        newObj = dvmAllocObject(clazz, ALLOC_DONT_TRACK);
        ...
    }   
    

    新-instance命令の核心はインスタンスオブジェクトにメモリ領域を割り当てることであり、この操作の前にクラスが正しく初期化されていることを保証する必要があります.そうしないと、dvmInitClass()を呼び出してクラスを初期化します.例に戻ると、ここには理解に値する知識点があります.
  • 通常、newインスタンスのクラスの後、クラスのインスタンス化はクラス初期化の後に完了します.
  • この例では、TestStaticクラスのインスタンス化がその方法でnew-instance命令を実行するためではない.dvmIsClassInitialized()はTestStaticがまだ初期化が完了していないと判断した.dvmInitClass()を呼び出してTestStaticを初期化します.上の分析から分かるように、1つ目のケースなので、ここは直接戻ります.
  • はその後、dvmAllocObjectを実行してクラスのオブジェクトにメモリ領域を割り当て、その構築方法を呼び出してインスタンスオブジェクトを初期化する.

  • したがって、ここでは、クラスのインスタンスオブジェクトの初期化がクラスの初期化の前に完了する可能性があります.私たちがjavaコードを書くときは、実は心配しなくてもいいです.同じスレッドでは、このような場合、クラスの初期化が完了するまで後の操作が行われますが、異なるスレッドでは、クラスの初期化が完了するまでブロックされます.

    3まとめ


    上の分析を通じて、この問題の正しい分析構想をまとめると、
  • TestStaticクラスの静的メソッドgetActivity()を呼び出し、TestStaticクラスの初期化を招き、初期化メソッドを実行する.
  • 初期化メソッドで静的ドメインメンバーsIntanceをインスタンス化し、インスタンス化中にTestStaticクラスの初期化が再びトリガーされ、メソッドに移行し、今回は直接終了します.TestStaticのインスタンス化を継続し、TestStaticの構築方法を実行し、構築方法でActivityをインスタンス化し、オブジェクト参照を静的ドメインメンバーsActivityに保存します.
  • は次に実行を継続し、***sActivity***をnullに割り当て、実行を完了する.
  • したがって、最後の静的方法getActivity()はsActivityがnullであることを返す.この問題から,クラス初期化の同時化の問題も解析した.この質問は私がワークグループで偶然見たもので、なぜsActivity(これと同じ場合)を使ったときに空に戻ったのかと聞かれました.徹底的に分析するためにソースコードを調べた.実は、この問題は自分のプログラムを合理的に設計すれば、起こらない.しかし、私たちは依然として背後の知識を学ぶことができます.