类加载过程
类加载过程
类加载过程包括加载、链接和初始化三个阶段,其中链接阶段又可细分为验证、准备、解析三个阶段[1]。了解类加载过程中的加载以及验证阶段将有助于我们更好的写出被Java虚拟机认可的class字节码,以及当遇到加载类抛出VerifyError错误时知道如何去排查问题、解决问题。
类加载的加载阶段主要是将一个类的字节码加载到内存中,并将其转化为方法区运行时数据结构,同时在堆中生成一个镜像,即Class对象。将字节流转化为方法区运行时数据结构,对于hotspot虚拟机而言,就是生成InstanceKlass实例。
将一个类的字节码加载到内存中,可以是根据文件路径加载,也可以是根据类名和classpath解析成文件路径加载,也可以是从网络上下载。在class文件加密的情况下,加载阶段还包括了解密,这是由用户自定义类加载器完成的,虚拟机加载java基础类库的类不存在加解密行为。
前面我们分析了Class的静态方法forName与类加载器的loadClass方法的底层实现,了解到Class的静态方法forName最终会调用类加载器的loadClass方法完成类的加载。在分析类加载器的loadClass方法底层实现时,只分析到了SystemDictionary的resolve_from_stream方法。从调用类加载器的loadClass方法到底层hotspot虚拟机的SystemDictionary的resolve_from_stream方法执行完成,就已经完成了类加载的加载阶段。
将class文件加载到内存后,需要将加载到内存的字节缓存根据class文件结构解析成InstanceKlass实例。前面第二章我们学习了如何使用Java去解析class字节码文件,对class文件的存储结构有了很深的了解,如果读者对这块内容感兴趣,也可看下hotspot是如何解析的。源码在classFileParser.cpp文件中,ClassFileParser类的parseClassFile方法[2]。
ClassFileParser的parseClassFile方法解析文件的顺序依然是魔数、主次版本号、常量池、类的访问标志、this与super类的符号引用、实现的接口、字段表、方法表、属性表。因为class文件的存储结构就是按顺序存储的,除魔数和版本号我们可以直接使用偏移量获取外,都只能按顺序解析获取class文件结构的各项。
在解析完class文件结构成之后,加载阶段就已经结束了,接着是验证阶段。但验证并不只是在解析完成之后才去验证,从hotspot源码中我们能看到,验证阶段有一部分动作是与加载阶段交叉进行的,如文件格式验证,以及确保父类不是接口、final方法不能被重写等。如代码清单4-8所示。
代码清单4-8 classFileParser方法的部分源码
instanceKlassHandle ClassFileParser::parseClassFile(Symbol* name, ClassLoaderData* loader_data,Handle protection_domain, KlassHandle host_klass,GrowableArray<Handle>* cp_patches, TempNewSymbol& parsed_name,bool verify,TRAPS) { ...... if (super_klass.not_null()) { if (super_klass->has_default_methods()) { has_default_methods = true; } if (super_klass->is_interface()) { ResourceMark rm(THREAD); Exceptions::fthrow( THREAD_AND_LOCATION, vmSymbols::java_lang_IncompatibleClassChangeError(), "class %s has interface %s as super class", class_name->as_klass_external_name(), super_klass->external_name() ); return nullHandle; } // Make sure super class is not final if (super_klass->is_final()) { THROW_MSG_(vmSymbols::java_lang_VerifyError(), "Cannot inherit from final class", nullHandle); } } ...... // check if this class can access its super class check_super_class_access(this_klass, CHECK_(nullHandle)); // check if this class can access its superinterfaces check_super_interface_access(this_klass, CHECK_(nullHandle)); // check if this class overrides any final method check_final_method_override(this_klass, CHECK_(nullHandle)); // check that if this class is an interface then it doesn't have static methods if (this_klass->is_interface()) { /* An interface in a JAVA 8 classfile can be static */ if (_major_version < JAVA_8_VERSION) { check_illegal_static_method(this_klass, CHECK_(nullHandle)); } } ...... }
验证阶段除对文件格式验证外,在class字节码解析生成InstanceKlass实例之后,会对InstanceKlass再进行验证,如元数据验证、字节码验证、符号引用验证等。在SystemDictionary的resolve_from_stream方法中,我们可以看到,在调用ClassFileParser的parseClassFile方法完成之后,还调用了define_instance_class方法或者find_or_define_instance_class渴望继续完成链接阶段。如代码清单4-9所示。
代码清单4-9 SystemDictionary的resolve_from_stream方法部分源码
if (!HAS_PENDING_EXCEPTION) { ...... // Add class just loaded // If a class loader supports parallel classloading handle parallel define requests // find_or_define_instance_class may return a different InstanceKlass if (is_parallelCapable(class_loader)) { k = find_or_define_instance_class(class_name, class_loader, k, THREAD); } else { define_instance_class(k, THREAD); } }
find_or_define_instance_class方法中也会调用define_instance_class方法,只不过多了异步加锁的逻辑。define_instance_class方法的源码如代码清单4-10所示。
代码清单4-10 SystemDictionary的define_instance_class方法
void SystemDictionary::define_instance_class(instanceKlassHandle k, TRAPS) { ...... // 1 Symbol* name_h = k->name(); unsigned int d_hash = dictionary()->compute_hash(name_h, loader_data); int d_index = dictionary()->hash_to_index(d_hash); ...... // 2 if (k->class_loader() != NULL) { methodHandle m(THREAD, Universe::loader_addClass_method()); JavaValue result(T_VOID); JavaCallArguments args(class_loader_h); args.push_oop(Handle(THREAD, k->java_mirror())); JavaCalls::call(&result, m, &args, CHECK); } ...... update_dictionary(d_index, d_hash, p_index, p_hash,k, class_loader_h, THREAD); ....... // 3 k->eager_initialize(THREAD); ...... }
我们分析下代码清单4-10中的注释1、2、3部分。第一部分是根据类名和类加载器计算出一个索引,该索引用于存放class文件加载后生成的InstanceKlass实例,存储结构如图4.3所示。

图4.3 Dictionary存储类的结构
第二部分判断类加载器是否为空,不为空情况下调用类加载器的addClass方法,以使用此加载器记录每个加载的类的Class实例,该方法在ClassLoader中的定义如下。
void addClass(Class<?> c) { classes.addElement(c); }
子类并不需要重写该方法,也重写不了该方法,因为该方法是包私有的,外部访问不了。因此,我们自己实现类加载器不需要自己去实现缓存加载的类的Class实例。前面说的Class的静态方法forName最终也是调用类加载器的loadClass方法完成类的加载的,这个结论也可通过在ClassLoader的addClass方法中下断点验证。
第三部分虽然调用instanceKlassHandle实例的eager_initialize方法想要完成链接阶段,包括验证阶段的字节码验证行为,但此时并不会执行,实际执行是在类初始化之前。引起类初始化的4条指令如new、getstatic、putstatic、invokestatic,虚拟机在执行这四条指令时,先判断类是否已经初始化,未初始化则完成类的初始化,这也是链接与字节码验证的时机。部分的java.lang.VerifyError的发生并不是在ClassLoader的loadClass方法,而是在加载完成之后想要使用时。
在instanceKlassHandle实例的eager_initialize方法中会调用eager_initialize_impl方法,eager_initialize_impl方法中调用了InstanceKlass的link_class_impl方法。link_class_impl方法的源码如代码清单4-11所示。
代码清单4-11 InstanceKlass的link_class_impl方法部分源码
bool InstanceKlass::link_class_impl( instanceKlassHandle this_oop, bool throw_verifyerror, TRAPS) { ...... // 在链接此类之前链接父类 instanceKlassHandle super(THREAD, this_oop->super()); if (super.not_null()) { // 父类是接口抛出异常 if (super->is_interface()) { // check if super class is an interface ResourceMark rm(THREAD); Exceptions::fthrow( THREAD_AND_LOCATION, vmSymbols::java_lang_IncompatibleClassChangeError(), "class %s has interface %s as super class", this_oop->external_name(), super->external_name() ); return false; } // 递归调用链接父类 link_class_impl(super, throw_verifyerror, CHECK_false); } // 在链接该类之前链接该类实现的所有接口 Array<Klass*>* interfaces = this_oop->local_interfaces(); int num_interfaces = interfaces->length(); for (int index = 0; index < num_interfaces; index++) { HandleMark hm(THREAD); instanceKlassHandle ih(THREAD, interfaces->at(index)); link_class_impl(ih, throw_verifyerror, CHECK_false); } // 如果类在链接其超类的过程中被链接则结束 if (this_oop->is_linked()) { return true; } ...... // 验证和重写 { ...... if (!this_oop->is_linked()) { if (!this_oop->is_rewritten()) { { // 验证字节码 bool verify_ok = verify_code(this_oop, throw_verifyerror, THREAD); if (!verify_ok) { return false; } } if (this_oop->is_linked()) { return true; } this_oop->rewrite_class(CHECK_false); } else if (this_oop()->is_shared()) { ...... } this_oop->link_methods(CHECK_false); ...... this_oop->set_init_state(linked); ...... } return true; }
验证字节码部分的逻辑比较复杂,整个调用链路是InstanceKlass::verify_code->Verifier::verify->Verifier::verify_class->ClassVerifier::verify_class->ClassVerifier::verify_method。
verify_method方法中包括验证方法签名、异常处理表、局部变量表、栈映射桢、字节码指令。在后面我们使用ASM操作字节码时可能会遇到各种验证错误,在遇到VerifyError时,从hotspot源码中查找验证失败的原因是最高效的解决方案。
现在我们编写一个验证链接阶段在类初始化之前的测试程序,使用ClassLoader加载一个类,在加载完成后打印类名,并在打印类名完成后通过反射创建一个该类的实例,代码如下。
public static void main(String[] args) throws Exception { Class<?> clz = LinkAndVerifyTest.class.getClassLoader() .loadClass("com.wujiuye.asmbytecode.book.fourth.VerifyTest2"); System.out.println(clz); try { Object obj= clz.newInstance(); } catch (Exception e) { e.printStackTrace(); } }
接着我们还要修改hotspot源码,在源码中添加日记打印。分别在instanceKlassHandle的eager_initialize方法、InstanceKlass的eager_initialize方法、InstanceKlass的link_class_impl方法、InstanceKlass的verify_code方法添加日记打印。以在InstanceKlass的verify_code方法添加日记打印为例,代码如下。
bool InstanceKlass::verify_code( instanceKlassHandle this_oop, bool throw_verifyerror, TRAPS) { // 1) Verify the bytecodes Verifier::Mode mode = throw_verifyerror ? Verifier::ThrowException : Verifier::NoException; // 日记打印 const char* plog = "com/wujiuye/"; if(!strncmp((const char*)this_oop->name()->bytes(), plog, strlen(plog))){ printf("InstanceKlass::verify_code %s \n",this_oop->name()->as_utf8()); } // end日记打印 return Verifier::verify(this_oop, mode, this_oop->should_verify_class(), CHECK_false); }
修改完成后重新编译虚拟机[3],使用编译后的jdk的java命令执行测试程序,程序输出如图4.4所示。

图4.4 VerifyTest2类加载过程
打印类加载器加载的Class对象的类名在链接阶段之前,说明链接阶段是在类初始化阶段触发的。链接阶段包括验证、准备和解析,所以验证阶段的字节码验证在类初始化之前才开始验证。
那么准备阶段又是在什么时候完成的呢?是在验证阶段全部完成之后吗,如果是这样,那准备阶段将在字节码验证完成之后才能开始。
准备阶段是为类中定义的静态变量分配内存并设置初始化值的阶段,这里的初始值通常情况下指的是对应类型的零值,比如int类型的零值为0。而给静态字段赋值通常是在编译器生成的类初始化方法<clinit>方法中完成的。如下代码所示。
public class ClassLoaderTest { static inttestIntStaticField = 123; static { System.out.println("my name is ClassLoaderTest!"); } }
ClassLoaderTest的静态字段testIntStaticField是int类型,编译后可以在<clinit>方法中找到赋值语句。编译后的<clinit>方法代码如图4.5所示。

图4.5 ClassLoaderTest编译器生成的<clinit>方法
从图4.5中可以看出,静态字段testIntStaticField的赋值是在初始化阶段调用类的<clinit>方法才开始赋值的,而在准备阶段只是赋予零值。那么在HotSpot源码中,准备阶段是什么时候开始的呢,我们回到前面分析的ClassFileParser的parseClassFile方法,在字节码流解析生成存储到方法区的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); }
在调用InstanceKlass的do_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属性。但是在本例中ClassLoaderTest的testIntStaticField并没有这个属性,因此不会为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后,使用编译后的jdk的java命令运行测试代码。程序输出如图4.6所示。

图4.6 加载ClassLoaderTest的结果输出
现在我们将ClassLoaderTest的testIntStaticField字段改为静态常量,代码如下。
public class ClassLoaderTest { final static int testIntStaticField = 123; static { System.out.println("my name is ClassLoaderTest!"); } }
将静态字段testIntStaticField改为常量后,日记打印输出如图4.7所示。

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

图4.8 testIntStaticField字段的属性
现在再看编译器生成的<clinit>方法,如图4.9所示。

图4.9 ClassLoaderTest的<clinit>方法
从图4.9可以看出,<clinit>方法已经没有为变量赋值的字节码指令了。而此时该字段已经存在一个ConstantValue_attribute属性,所以在准备阶段就为该字段赋值为初始值123。因此我们可以得出结论,如果字段存在ConstantValue_attribute属性,那么字段将会在类加载的准备阶段被赋值为初始化值,即ConstantValue_attribute属性保存的初始值。
注释:
[1] 周志明.《深入理解Java虚拟机》第三部分第七章:虚拟机类加载机制
[2] classFileParser.cpp源码文件的路径:/hotspot/src/share/vm/classfile/classFileParser.cpp文件中
[3] 由于编译HotSpot虚拟机是在Linux操作系统上完成的,所以使用java命令执行测试程序也是在Linux操作系统上完成的