Java 虚拟机内部类静态字段的初始化与访问

Published: 02 Sep 2014 Category: 技术

要明白 Java 虚拟机如何访问类的静态变量,首先要明白下面几个问题:

  • 虚拟机内部是如何表示一个 Java 类的
  • 静态变量存储在哪里
  • 虚拟机如何访问到这些静态变量

这篇文章也从这围绕这三个问题展开,并结合 OpenJDK 中 HotSpot 的源代码作分析。

Java 虚拟机内部如何表示类

HotSpot 虚拟机在内部使用两组类来表示 Java 的类和对象

  • OOP(ordinary object pointer),用来描述对象实例信息
  • Klass,用来描述Java类,是虚拟机内部Java类的对等体

例如,constantPoolOop 表示在 Class 文件中描述 的常量池, instanceOop 表示 一个Java类型实例; instanceKlass 在虚拟机层面 描述一个 Java 类。

虚拟机在拿到一个 .class 文件后,会对它进行解析,并创建相应的 instanceKlass。其中主要包含:

  • 读取魔数
  • 读取版本号
  • 解析常量池 (parseconstantpool)
  • 解析字段信息 (parse_fields)
  • 创建 Java 镜像类 (create_mirror)

注意虚拟机并不是直接用 instanceKlass 表示 Java 类了,而是又创建了一个镜像类,并且二者相互引用 。 至于为什么这样做,我还没有搞清楚。可以参考 RednaxelaFX 的这篇 借助HotSpot SA来一窥PermGen上的对象 中相应的描述。

在源代码中 ClassFileParser::parseClassFile 方法负责解析 .class 文件。

//OpenJDK-Research\hotspot\src\share\vm\classfile\classFileParser.cpp

instanceKlassHandle 
ClassFileParser::parseClassFile(
  Symbol* name,
  ClassLoaderData* loader_data,
  Handle protection_domain,
  KlassHandle host_klass,
  GrowableArray<Handle>* cp_patches,
  TempNewSymbol& parsed_name,
  bool verify,
  TRAPS) {

  ...

  // Magic value
  u4 magic = cfs->get_u4_fast();


  // Version numbers
  u2 minor_version = cfs->get_u2_fast();
  u2 major_version = cfs->get_u2_fast();

  // Constant pool
  constantPoolHandle cp = parse_constant_pool(CHECK_(nullHandle));

  Array<u2>* fields = parse_fields(class_name,
                                     access_flags.is_interface(),
                                     &fac, &java_fields_count,
                                     CHECK_(nullHandle));


  // We can now create the basic Klass* for this klass
  _klass = InstanceKlass::allocate_instance_klass(loader_data,
                                                    vtable_size,
                                                    itable_size,
                                                    info.static_field_size,
                                                    total_oop_map_size2,
                                                    rt,
                                                    access_flags,
                                                    name,
                                                    super_klass(),
                                                    !host_klass.is_null(),
                                                    CHECK_(nullHandle));

// Allocate mirror and initialize static fields
java_lang_Class::create_mirror(this_klass, protection_domain, CHECK_(nullHandle));
  ...

}

我们注意到, create_mirror 方法是在 java_lang_Class 类中。另外,我们还听过 Java 中的类都是 java.lang.Class 的实例,这 二者之间是不是有什么关系呢?到 create_mirror 里我们可以看到

oop java_lang_Class::create_mirror(KlassHandle k, Handle protection_domain, TRAPS) {
   ...
    // Allocate mirror (java.lang.Class instance)
    Handle mirror = InstanceMirrorKlass::cast(SystemDictionary::Class_klass())->allocate_instance(k, CHECK_0);

    ...
    }

说明这个 mirror 确实是 java.lang.Class 的实例。另外,我们平时通过 obj.getClass() 得到的类,其实是通过 obj->_klass->_java_mirror 得到的,参见 R 的另一篇 借HSDB来探索HotSpot VM的运行时数据. 也就是说,直接暴露给 Java 的 是 java_mirror, 而不是 InstanceKlass.

静态变量存储在哪里

首先,根据 R 在刚才提到的博客中的说法,静态变量存储在 _java_mirror 的尾部,我们再搜索一下,静态变量是如何从 .class 中提取 出来,分配空间并初始化的,又存储在哪里。

在上一节的代码中,我们可以看到这一句:

  Array<u2>* fields = parse_fields(class_name,
                                     access_flags.is_interface(),
                                     &fac, &java_fields_count,
                                     CHECK_(nullHandle));

在 《HotSpot实战》中,3.2 节介绍类的加载过程时说过

这个函数用于读取字段信息,计算出域大小与偏移量,并根据域分配策略对
字段存储顺序进行分配。这些信息会在后续步骤填入 instanceKlass  对象中成为类信息的一部分。

根据 Java 虚拟机规范 中的说明,字段(field) 由下列数据结构表示:

field_info {
 u2 access_flag;
 u2 name_index;
 u2 descriptor_index;
 u2 attriutes_count;
 attribute_info attributes[attributes_count];
}

其中 access_flag 表示访问属性,例如 是否为 public, 是否为 static 等等。 name_index 表示这个字段的名字 在常量池中的索引。 descriptor_index 表示这个字段的描述符在常量池中的索引,描述符表示字段的语法符号,比如整形数 的语法符号是 'I', 一个一维数组的语法符号是 '['.

我们来看一下这个函数,从中可以看到函数如何填充 field_info.

// OpenJDK-Research\hotspot\src\share\vm\classfile\classFileParser.cpp

Array<u2>* ClassFileParser::parse_fields(Symbol* class_name,
                                         bool is_interface,
                                         FieldAllocationCount *fac,
                                         u2* java_fields_count_ptr, TRAPS) {
  ...

  // Allocate a temporary resource array for field data. For each field,
  // a slot is reserved in the temporary array for the generic signature
  // index. After parsing all fields, the data are copied to a permanent
  // array and any unused slots will be discarded.
  ResourceMark rm(THREAD);
  u2* fa = NEW_RESOURCE_ARRAY_IN_THREAD(
             THREAD, u2, total_fields * (FieldInfo::field_slots + 1));

  for (int n = 0; n < length; n++) {
    ...

    AccessFlags access_flags;
    jint flags = cfs->get_u2_fast() & JVM_RECOGNIZED_FIELD_MODIFIERS;
    access_flags.set_flags(flags);


    u2 name_index = cfs->get_u2_fast();
    Symbol*  name = _cp->symbol_at(name_index); 


    u2 signature_index = cfs->get_u2_fast();
    Symbol*  sig = _cp->symbol_at(signature_index);

    ...

    FieldInfo* field = FieldInfo::from_field_array(fa, n);
    field->initialize(access_flags.as_short(),
                      name_index,
                      signature_index,
                      constantvalue_index);

    // Remember how many oops we encountered and compute allocation type
    FieldAllocationType atype = fac->update(is_static, type);
    field->set_allocation_type(atype);
  }

  Array<u2>* fields = MetadataFactory::new_array<u2>(
          _loader_data, index * FieldInfo::field_slots + num_generic_signature,
          CHECK_NULL);
  _fields = fields; // save in case of error
  {
    int i = 0;
    for (; i < index * FieldInfo::field_slots; i++) {
      fields->at_put(i, fa[i]);
    }
    for (int j = total_fields * FieldInfo::field_slots;
         j < generic_signature_slot; j++) {
      fields->at_put(i++, fa[j]);
    }
    assert(i == fields->length(), "");
  }

先开辟数组空间 fa, 再从类文件中提取信息,然后将信息临时放在 fa 中的 FieldInfo 结构中,等解析完所有字段,将所有字段信息最终放在 fields 。另外,注意那句 fac->update,它会统计所有类型出现的次数。

我们看看FieldInfoinitialize 方法:

  void initialize(u2 access_flags,
                  u2 name_index,
                  u2 signature_index,
                  u2 initval_index) {
    _shorts[access_flags_offset] = access_flags;
    _shorts[name_index_offset] = name_index;
    _shorts[signature_index_offset] = signature_index;
    _shorts[initval_index_offset] = initval_index;
    _shorts[low_packed_offset] = 0;
    _shorts[high_packed_offset] = 0;
  }

我们注意到这里并没有计算偏移,计算偏移还是在 parseClassFile 方法中:

//OpenJDK-Research\hotspot\src\share\vm\classfile\classFileParser.cpp

instanceKlassHandle 
ClassFileParser::parseClassFile(
  Symbol* name,
  ClassLoaderData* loader_data,
  Handle protection_domain,
  KlassHandle host_klass,
  GrowableArray<Handle>* cp_patches,
  TempNewSymbol& parsed_name,
  bool verify,
  TRAPS) {


    // layout_fields 会为 fields 设置 offset
    FieldLayoutInfo info;
    layout_fields(class_loader, &fac, &parsed_annotations, &info, CHECK_NULL);
  }

其中的 layout_fields 会根据之前统计的各个类型的数目 fac,计算各种类型数据的偏移。

//

void ClassFileParser::layout_fields(Handle class_loader,
                                    FieldAllocationCount* fac,
                                    ClassAnnotationCollector* parsed_annotations,
                                    FieldLayoutInfo* info,
                                    TRAPS) {

                                    ...

  // Iterate over fields again and compute correct offsets.
  // The field allocation type was temporarily stored in the offset slot.
  // oop fields are located before non-oop fields (static and non-static).
  for (AllFieldStream fs(_fields, _cp); !fs.done(); fs.next()) {

    // skip already laid out fields
    if (fs.is_offset_set()) continue;

    // contended instance fields are handled below
    if (fs.is_contended() && !fs.access_flags().is_static()) continue;

    int real_offset;
    FieldAllocationType atype = (FieldAllocationType) fs.allocation_type();

    // pack the rest of the fields
    switch (atype) {
      case STATIC_OOP:
        real_offset = next_static_oop_offset;
        next_static_oop_offset += heapOopSize;
        break;
      case STATIC_BYTE:
        real_offset = next_static_byte_offset;
        next_static_byte_offset += 1;
        break;
      case STATIC_SHORT:
        real_offset = next_static_short_offset;
        next_static_short_offset += BytesPerShort;
        break;
      case STATIC_WORD:
        real_offset = next_static_word_offset;
        next_static_word_offset += BytesPerInt;
        break;
      case STATIC_DOUBLE:
        real_offset = next_static_double_offset;
        next_static_double_offset += BytesPerLong;
        break;

      ...

      default:
        ShouldNotReachHere();
    }
    fs.set_offset(real_offset);
  }


  ....

    // Pass back information needed for InstanceKlass creation
  info->nonstatic_oop_offsets = nonstatic_oop_offsets;
  info->nonstatic_oop_counts = nonstatic_oop_counts;
  info->nonstatic_oop_map_count = nonstatic_oop_map_count;
  info->total_oop_map_count = total_oop_map_count;
  info->instance_size = instance_size;
  info->static_field_size = static_field_size;
  info->nonstatic_field_size = nonstatic_field_size;
  info->has_nonstatic_fields = has_nonstatic_fields;
}

其中的 set_offset 会真正设置 FieldInfo 中的偏移,我们一路跟踪过去,会发现调用FieldInfo 的 这个函数设置了偏移。

  void set_offset(u4 val)                        {
    val = val << FIELDINFO_TAG_SIZE; // make room for tag
    _shorts[low_packed_offset] = extract_low_short_from_int(val) | FIELDINFO_TAG_OFFSET;
    _shorts[high_packed_offset] = extract_high_short_from_int(val);
  }

其中的 info 会将偏移信息记录下来,在创建 InstanceKlass 的时候会用上。


//OpenJDK-Research\hotspot\src\share\vm\classfile\classFileParser.cpp

instanceKlassHandle 
ClassFileParser::parseClassFile(
  Symbol* name,
  ClassLoaderData* loader_data,
  Handle protection_domain,
  KlassHandle host_klass,
  GrowableArray<Handle>* cp_patches,
  TempNewSymbol& parsed_name,
  bool verify,
  TRAPS) {
    ...

    // We can now create the basic Klass* for this klass
    _klass = InstanceKlass::allocate_instance_klass(loader_data,
                                                    vtable_size,
                                                    itable_size,
                                                    info.static_field_size,
                                                    total_oop_map_size2,
                                                    rt,
                                                    access_flags,
                                                    name,
                                                    super_klass(),
                                                    !host_klass.is_null(),
                                                    CHECK_(nullHandle));
  ...

注意 info.static_field_size 会被传进去,用于分配空间。

然后,再看看 mirror 的创建。

    // Allocate mirror and initialize static fields
    java_lang_Class::create_mirror(this_klass, protection_domain, CHECK_(nullHandle));

其内部会首先为 mirror 分配空间:

oop java_lang_Class::create_mirror(KlassHandle k, Handle protection_domain, TRAPS) {
Handle mirror = InstanceMirrorKlass::cast(SystemDictionary::Class_klass())->allocate_instance(k, CHECK_0);

// Initialize static fields
InstanceKlass::cast(k())->do_local_static_fields(&initialize_static_field, CHECK_NULL);


return mirror();

注意 allocate_instance 内部会根据 k 中的信息,计算需要分配的空间,包含静态变量的大小。然后对 mirror 的空间进行分配。

之后, do_local_static_fields 会对静态字段进行初始化。 注意此是传入的函数指针表示的 initialize_static_field 函数, do_local_static_fields 会在内部遍历所有静态字段,然后调用这个函数对他们进行初始化。

static void initialize_static_field(fieldDescriptor* fd, TRAPS) {
  Handle mirror (THREAD, fd->field_holder()->java_mirror());
  assert(mirror.not_null() && fd->is_static(), "just checking");
  if (fd->has_initial_value()) {
    BasicType t = fd->field_type();
    int offset = fd->offset();
    switch (t) {
      case T_BYTE:
        mirror()->byte_field_put(fd->offset(), fd->int_initial_value());
              break;
      case T_BOOLEAN:
        mirror()->bool_field_put(fd->offset(), fd->int_initial_value());
              break;
      case T_CHAR:
        mirror()->char_field_put(fd->offset(), fd->int_initial_value());
              break;
      case T_SHORT:
        mirror()->short_field_put(fd->offset(), fd->int_initial_value());
              break;
      case T_INT:
        mirror()->int_field_put(fd->offset(), fd->int_initial_value());
        break;
      case T_FLOAT:
        mirror()->float_field_put(fd->offset(), fd->float_initial_value());
        break;
      case T_DOUBLE:
        mirror()->double_field_put(fd->offset(), fd->double_initial_value());
        break;
      case T_LONG:
        mirror()->long_field_put(fd->offset(), fd->long_initial_value());
        break;
      case T_OBJECT:
        {
          #ifdef ASSERT
          TempNewSymbol sym = SymbolTable::new_symbol("Ljava/lang/String;", CHECK);
          assert(fd->signature() == sym, "just checking");
          #endif
          oop string = fd->string_initial_value(CHECK);
          mirror()->obj_field_put(fd->offset(), string);
        }
        break;
      default:
        THROW_MSG(vmSymbols::java_lang_ClassFormatError(),
                  "Illegal ConstantValue attribute in class file");
    }
  }
}

这里会根据类型的不同分别进行初始化。

我们看看 int_field_put 内部做些什么。

inline void oopDesc::int_field_put(int offset, jint contents)       { *int_field_addr(offset) = contents;    }
inline jint*     oopDesc::int_field_addr(int offset)    const { return (jint*)    field_base(offset); }
inline void*     oopDesc::field_base(int offset)        const { return (void*)&((char*)this)[offset]; }

可以看到,存储位置最终放在 mirror 对象偏移为 offset 处,这个 offset 就是之前对 field 设置的 offset.

到这里,我们已经知道这些静态变量存储在哪里了。

如何访问这些静态变量

根据刚才的分析过程,我们也看到静态变量存储在哪里了,通过什么函数可以访问到他们,可以做一个实验,打印出所有静态变量的值, 看看如何访问这些静态变量。 我之前写过文章,通过 JVMTI 写一个 agent ,每次执行一条字节码之前,就跳到我们自己定义的一个函数中 执行。在我们自己定义的函数中,可以调用虚拟机提供的接口,访问一些数据。在这次实验中,我修改了其中一个函数的内部实现,使得 我们的 agent 调用这个函数的外部接口时,内部可以打印出所有整数类型静态变量的值。

我测试用的 Java 程序是一个多线程程序

import java.lang.Thread;

public class PossibleReordering {
    static int x = 0, y = 0;
    static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread one = new Thread(new Runnable() {
            public void run() {
                        a = 19;
                        x = b;
            }
        });

        Thread other = new Thread(new Runnable() {

            public void run() {
                        b = 18;
                        y = a;
                        a = 20;
                        x = b;
                        a = 49;
                        x = b;
                        a = 29;
                        x = b;
                        a = 16;
                        x = b;
                        a = 17;
                        x = b;
                        a = 155;
                        x = b;
                        a = 321;
                        x = b;
            }
        });

        one.start();
        other.start();
        one.join();
        other.join();
        System.out.println("end");
    }
}

Agent 中开启单步执行,并设置回调函数。

   /* add capabilities */
    memset(&capa, 0, sizeof(jvmtiCapabilities));
    capa.can_generate_single_step_events = 1;

   /* add callbacks */
    memset(&callbacks, 0, sizeof(callbacks));
    callbacks.SingleStep = &callbackSingleStep;

回调函数会在每次执行一条字节码前被调用。回调函数中调用一个 GetMethodLocation 函数,

        error = (*jvmti)->GetMethodLocation( \
                jvmti, method, &s_location, &e_location)

同时在虚拟机内部改写了 GetMethodLocation 的内部对应函数。

// method_oop - pre-checked for validity, but may be NULL meaning obsolete method
// start_location_ptr - pre-checked for NULL
// end_location_ptr - pre-checked for NULL
jvmtiError
    JvmtiEnv::GetMethodLocation(Method* method_oop, jlocation* start_location_ptr, jlocation* end_location_ptr) {

        NULL_CHECK(method_oop, JVMTI_ERROR_INVALID_METHODID);
        // get start and end location
        (*end_location_ptr) = (jlocation) (method_oop->code_size() - 1);
        if (method_oop->code_size() == 0) {
            // there is no code so there is no start location
            (*start_location_ptr) = (jlocation)(-1);
        } else {
            (*start_location_ptr) = (jlocation)(0);
        }

        InstanceKlass *klass = method_oop->method_holder();
        oop jmirror = klass->java_mirror();
        int fields_count = klass->java_fields_count();
        Array<u2> * fields = klass->fields();

        instanceKlassHandle handle(klass);
        jclass declaring_class;
        char *class_signature;
        this->GetMethodDeclaringClass(method_oop, &declaring_class); 
        this->GetClassSignature(jmirror, &class_signature, NULL);

        if (strstr(class_signature, "Possible") != NULL) {
            for (JavaFieldStream fs(handle()); !fs.done(); fs.next()) {
                if (fs.access_flags().is_static()) {
                    fieldDescriptor& fd = fs.field_descriptor();
                    if (fd.field_type() == T_INT) {
                        int offset = fd.offset();
                        int int_field = jmirror->int_field(offset);
                        if (int_field != 0) {
                            printf("visit: %s %d, %d\n", fd.name()->as_C_string(), offset, int_field);
                        }
                    }
                }
            }
        }

        return JVMTI_ERROR_NONE;
} /* end GetMethodLocation */

从中可以看出,如何获取当前类的名字,如何遍历当前所有字段,如何获取字段名。如何根据偏移值获取字段值。 另外,我将 在 layout_fields 函数内部也捕获这个类所有静态整数类型的偏移,看看初始化偏移时的值与实际访问时 的偏移值是不是同一个值,经过验证,确实是这样。

// layout_fields 内部插入的代码
    if (strstr(this->_class_name->as_C_string(), "Possible") != NULL) {
        if (atype == STATIC_WORD) {
            printf("init: name(%s) offset(%d)\n", fs.name()->as_C_string(), real_offset);
        }
    }

执行之后,虚拟机会打印出相关的字段信息。

PossibleReordering
init: name(x) offset(80)
init: name(y) offset(84)
init: name(a) offset(88)
init: name(b) offset(92)

visit: a 88, 19
visit: a 88, 19

visit: x 80, 18
visit: y 84, 19
visit: a 88, 49
visit: b 92, 18

visit: x 80, 18
visit: y 84, 19
visit: a 88, 49
visit: b 92, 18
visit: x 80, 18
visit: y 84, 19
visit: a 88, 321
visit: b 92, 18
visit: x 80, 18
visit: y 84, 19
visit: a 88, 321
visit: b 92, 18
visit: x 80, 18
visit: y 84, 19
visit: a 88, 321
visit: b 92, 18
end
visit: x 80, 18
visit: y 84, 19
visit: a 88, 321
visit: b 92, 18

注意初始化各个字段偏移时的偏移值与实际访问时的偏移值是一致的,另外,想一下 a 的值为 155 为什么没打印出来?