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(与此一样的情况) 时返回为空?为了分析透彻而翻查了源码。其实,这个问题只要合理设计下自己的程序,就不会发生了。但我们依然可以从中学到背后的知识。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,064评论 5 466
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,606评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,011评论 0 328
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,550评论 1 269
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,465评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,919评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,428评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,075评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,208评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,185评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,191评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,914评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,482评论 3 302
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,585评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,825评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,194评论 2 344
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,703评论 2 339

推荐阅读更多精彩内容