深入理解类加载阶段之准备阶段

原创 吴就业 115 0 2020-04-19

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/2e377f84a9404d4986dbda2688111185

作者:吴就业
链接:https://wujiuye.com/article/2e377f84a9404d4986dbda2688111185
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

本篇文章写于2020年04月19日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。

准备阶段是为类中定义的静态变量分配内存并设置初始化值的阶段,这里的初始值通常情况下指的是对应类型的零值,比如int类型的零值为0。而给静态字段赋值通常是在编译器生成的类初始化方法<clinit>方法中完成的。

public class ClassLoaderTest {
    static inttestIntStaticField = 123;
    static {
        System.out.println("my name is ClassLoaderTest!");
    }
}

ClassLoaderTest的静态字段testIntStaticFieldint类型,编译后可以在<clinit>方法中找到赋值语句。编译后的<clinit>方法代码如下图所示。

clinit方法字节码

从图中可以看出,静态字段testIntStaticField的赋值是在初始化阶段调用类的<clinit>方法才开始赋值的,而在准备阶段只是赋予零值。

那么在hotspot源码中,准备阶段是什么时候开始的呢,我们回到前面分析的ClassFileParserparseClassFile方法,在字节码流解析生成存储到方法区的InstanceKlass实例并完成一些如验证类是否重写final方法等验证之后,会调用java_lang_Class::create_mirror方法,代码如下。

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

create_mirror方法用于创建一个java.lang.Class对象,这个对象存储在堆中,也称为InstanceKlass的镜像。从注释中可以看出,调用该方法还会初始化静态字段。create_mirror方法中会调用initialize_mirror_fields方法初始化镜像的字段,initialize_mirror_fields方法的部分代码如下。

// 初始化镜像java.lang.Class的字段
void java_lang_Class::initialize_mirror_fields(KlassHandle k,Handle mirror,
                            Handle protection_domain,TRAPS) {
  ......
  // Initialize static fields
  //  初始化静态字段
  InstanceKlass::cast(k())->do_local_static_fields(&initialize_static_field, mirror, CHECK);
}

在调用InstanceKlassdo_local_static_fields方法时,传递了一个方法指针initialize_static_field,该方法会被回调执行,因此我们直接看initialize_static_field方法的实现,代码如下。

// 初始化静态字段
static void initialize_static_field(fieldDescriptor* fd, Handle mirror, TRAPS) {
  assert(mirror.not_null() && fd->is_static(), "just checking");
  // 是否有初始值
  if (fd->has_initial_value()) {
    BasicType t = fd->field_type();
    // 根据类型设置零值
    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:
        {
          // 如果字段是引用类型,ConstantValue 只支持字符串类型
          #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");
    }
  }
}

initialize_static_field方法首先会判断这个字段是否有初始值,有初始值才会给该静态字段赋值为初始值。

怎么判断是否有初始化值?就是判断该字段是否有一个ConstantValue_attribute属性。但是在本例中ClassLoaderTesttestIntStaticField并没有这个属性,因此不会为testIntStaticField字段赋值为初始值。在vm/classfile/javaClasses.cpp文件中,在initialize_static_field方法添加如下日记打印。

static void initialize_static_field(fieldDescriptor* fd, Handle mirror, TRAPS) {
  assert(mirror.not_null() && fd->is_static(), "just checking");
  // 日记打印
  const char* plog = "testIntStaticField";
  if(!strncmp((const char*)fd->name()->bytes(), plog, strlen(plog))){
      printf("java_lang_Class static initialize_mirror_fields  %s,%s, %d \n",
        fd->signature()->as_utf8(), fd->name()->as_utf8(),fd->has_initial_value());
  }
  // end 日记打印
  if (fd->has_initial_value()) {
    ......
  }
}

编写测试代码加载ClassLoaderTest,测试代码如下。

Class<?> classLoaderTestClass = Class.forName(
             "com.wujiuye.asmbytecode.book.fourth.ClassLoaderTest");

重新编译openjdk后,使用编译后的jdkjava命令运行测试代码,程序输出日记如下。

java_lang_Class static initialize_mirror_fields  I,testIntStaticField, 0

现在我们将ClassLoaderTesttestIntStaticField字段改为静态常量,代码如下。

public class ClassLoaderTest {
    final static int testIntStaticField = 123;
    static {
        System.out.println("my name is ClassLoaderTest!");
    }
}

将静态字段testIntStaticField改为常量后,日记打印输出如下。

java_lang_Class static initialize_mirror_fields  I,testIntStaticField, 1

initialize_static_field方法日记打印输出的has_initial_value为1,说明该字段已经存在一个ConstantValue_attribute属性,我们可以使用classpy工具查看,如下图所示。

ConstantValue_attribute属性

现在再看编译器生成的<clinit>方法。

clinit方法字节码

<clinit>方法已经没有为变量赋值的字节码指令了。而此时该字段已经存在一个ConstantValue_attribute属性,所以在准备阶段就为该字段赋值为初始值123

因此我们可以得出结论,如果字段存在ConstantValue_attribute属性,那么字段将会在类加载的准备阶段被赋值为初始化值,即ConstantValue_attribute属性保存的初始值。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

如何获取泛型类的参数化类型?解密Java泛型

框架怎么知道这个`T`到底是什么类型呢?

这又是导致事务注解@Transactional不生效的一个原因

事务方法`A`调用事务方法`B`,当方法`B`抛出的异常被方法`A` `catch`后会发生什么?

在同一个线程下数据源多次切换的回溯问题

在某些场景下,我们可能需要多次切换数据源才能处理完同一个请求,也就是在一个线程上多次切换数据源。

访问者模式在ASM框架中的使用

访问者模式的定义是:封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。

为什么要使用Redis的多数据库呢?

`Redis`多数据库是我在`Redis`设计中最糟糕的决定,我希望在某种程度上,我们可以放弃多个数据库的支持,但我认为可能已经太晚了,因为有很多人在工作中使用这个特性。

遇到VerifyError束手无策?从虚拟机源码分析原因

在学习使用asm动态生成字节码的过程中,我们或多或少都会遇到这样个错误,那么越到这个问题我们该如何解决呢?