Class初始化:一个有趣的问题

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 问题分析

这个问题主要涉及了虚拟机内部类初始化过程知识,参考:[类加载链接、初始化、实例化](http://www.jianshu.com/p/0bff0413fd2f
类初始过程包括:类加载、链接、初始化、实例化。这里主要关注 初始化 阶段,初始化 阶段主要执行 静态代码块初始化静态域成员,这两个操作都是在类初始化方法 <clinit> 中完成。
有分析步骤如下:

  1. 调用 TestStatic 类的静态方法 getActivity(),导致 TestStatic 类初始化,执行初始化方法 <clinit>;
  2. 在初始化方法中初始化静态域成员 sIntance , 并导致执行 TestStatic 的构造方法,在构造方法中实例化 Activity,并将对象引用保存在静态域成员 sActivity;
  3. 接下来继续执行 <clinit>, 将 *** sActivity*** 赋为 null<clinit> 执行完毕;
  4. 所以,最后静态方法 getActivity() 返回 sActivitynull

如果你的分析过程也是这样,那么恭喜你 答案 是正确的,但不要高兴太早,因为 分析过程是错误 的。如果认真读了 [类加载链接、初始化、实例化](http://www.jianshu.com/p/0bff0413fd2f), 就会发现问题发生在第 2 步骤的分析。

private static TestStatic sInstance = new TestStatic();

这里实例化 TestStatic 类时,new 的操作也会导致 TestStatic 类的初始化,因为 <clinit>还没执行完,即类初始化未完成。先贴 <clinit> 方法(字节码):

.method static constructor <clinit>()V
          .registers 1
          .prologue
00000000  new-instance            v0, TestStatic
00000004  invoke-direct           TestStatic-><init>()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 <init>()V
          .registers 2
          .prologue
00000000  invoke-direct           Object-><init>()V, p0
00000006  new-instance            v0, Activity
0000000A  invoke-direct           Activity-><init>()V, v0
00000010  sput-object             v0, TestStatic->sActivity:Activity
00000014  return-void
.end method

<clinit> 方法中第一条字节码:

00000000  new-instance            v0, TestStatic

new-instance 会触发 TestStatic 类的初始化,即在 <clinit> 方法中又调用了 <clinit>,难道就这样在这里死循环了?
当然不是, 那真相是怎样子的呢?
这个问题的背后是隐藏了一个关于 类初始化 很关键的知识点,接下来完整分析下这个过程。

2 类初始化

关于引起类的初始化的条件可参考 [类加载链接、初始化、实例化](http://www.jianshu.com/p/0bff0413fd2f), 就会发现问题发生在第 2 步骤的分析, 这里不再赘述。挑与本问题相关的2种条件进行分析。

  1. 调用 静态方法:字节码为:invoke-static
  2. 实例化类,字节码为: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 <clinit> for a class creates an instance
             * of itself, we will call <init> 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 <clinit>");
            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, "<clinit>", "()V");
    if (method == NULL) {
        LOGVV("No <clinit> found for %s", clazz->descriptor);
    } else {
        LOGVV("Invoking %s.<clinit>", clazz->descriptor);
        JValue unused;
        dvmCallMethod(self, method, NULL, &unused);
    }
    ...
    bail_unlock:

    dvmUnlockObject(self, (Object*) clazz);

    return (clazz->status != CLASS_ERROR);
}

理解dvmInitClass()执行过程是我们理解认清这个问题的关键。代码块省略了无关的部分,阅读起来逻辑比较简单清晰。

  1. 类的初始化通过ClasObject中的一个lock和status状态来处理并发初始化类的问题。
  2. 第一个进入该方法的人,上锁,其他人无法进来。接着,设置初始化类的线程ID,设置类status为:CLASS_INITIALIZING,解锁。调用initSFields()初始化一些简单静态域,最后看类是否有<clinit>方法,有的话则调用。执行完后,则类的初始化步骤完成。
  3. 在类首次还没初始化完成情况下 有其人 进入该方法,在第2点说明中,解锁 后,会进入 while(clazz->status == CLASS_INITIALIZING) 循环中,分2种情况:
    (1)如果是 正在初始化当前类的线程,则 直接退出
    (2)如果是 其他线程,则会 阻塞,直到当前的初始化完成(成功或失败),最后也是直接退出。

基于上面3点的分析,便可以将前面的问题解释清楚了。在<clinit>中,再次调用<clint>方法,如果是本线程,后一次调用会直接退出,否则,会阻塞,不会有 死循环
在本次的例子中,属于第1中情况,即 同一个线程中循环调用 <clinit> 方法
而第2中情况,即是多线程同时初始化一个 *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);
    ...
}   

new-instance指令的核心是为实例对象分配内存空间,而在这个操作之前,必须先保证类已经正确被初始化,否则会调用dvmInitClass()对类进行初始化。
回到例子中,这里有一个知识点值得了解下。

  1. 正常情况下,new实例一个类后,类的 实例化 是在 类初始化 后面完成。
  2. 在这个例子中不是,因为 TestStatic 类的 实例化 在其 <clinit> 方法中,执行 new-instance指令。dvmIsClassInitialized() 判断 TestStatic 到还初始化没完成。导致调用dvmInitClass()TestStatic 进行初始化。从上面分析得知,属于 第一种情况,因此这里直接返回。
  3. 然后执行 dvmAllocObject 给类的 对象 分配内存空间,并调用其构造方法初始化实例对象。
  4. 因此这里,可能会出现类的 实例对象初始化类初始化 前面完成。
    我们写java代码的时候,其实不用担心。因为同个线程中,出现这种情况的时候,会等到 类初始化 完成后才进行后面的操作,而不同的线程,是会阻塞到类初始化完成的。

3 总结

通过上面的分析,总结下这个问题正确的分析思路应该是:

  1. 调用 TestStatic 类的静态方法 getActivity(),导致 TestStatic 类初始化,执行初始化方法 <clinit>;
  2. 在初始化方法中 实例化 静态域成员 sIntance , 实例化过程中会再次触发 TestStatic 类的初始化,调到 <clinit> 方法,这次 <clinit> 直接退出。 继续 TestStatic 的实例化,并执行 TestStatic 的构造方法,在构造方法中实例化 Activity,并将对象引用保存在静态域成员 sActivity 中;
  3. 接下来继续执行 <clinit>, 将 *** sActivity*** 赋为 null<clinit> 执行完毕;
  4. 所以,最后静态方法 getActivity() 返回 sActivitynull
    从这个问题中,我们也分析了 类初始化 并发的问题。
    这个问题是我在工作群上偶然看到的,有人问为什么使用与 sActivity(与此一样的情况) 时返回为空?为了分析透彻而翻查了源码。其实,这个问题只要合理设计下自己的程序,就不会发生了。但我们依然可以从中学到背后的知识。
    原文作者:legendWorld
    原文地址: https://www.jianshu.com/p/75600047f381
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞