在介绍字节码连接之前,有必要介绍一下字节码验证。HotSpot虚拟机其实会遵守《Java虚拟机规范》,对Class文件中包含的信息进行合法性验证,以保证虚拟机的安全。从整体上来看,验证阶段大致上会进行如下4方面的验证:
- 文件格式验证:包括魔数,版本号等;
- 元数据验证:对程序进行语义分析,如是否有父类,是否继承了不被继承的类,不是抽象类,是否实现了父类或者接口中的所有要求被实现的方法;
- 字节码验证:指令级别的语义验证,如跳转指令不会跳转到方法体以外的代码上;
- 符号引用验证:符号引用转化为直接引用的时候,可以看作是对类自身以外的信息进行匹配验证,如通过全限定名,是否能找到对应的类。
文件格式的验证大部分都会在解析类文件的parseClassFile()函数中进行,例如对魔数,版本号的验证,如下:
cfs->guarantee_more(8, CHECK_(nullHandle)); // magic, major, minor u4 magic = cfs->get_u4_fast(); guarantee_property(magic == JAVA_CLASSFILE_MAGIC,"Incompatible magic value %u in class file %s",magic, CHECK_(nullHandle)); // Version numbers u2 minor_version = cfs->get_u2_fast(); u2 major_version = cfs->get_u2_fast(); // Check version numbers - we check this even with verifier off if (!is_supported_version(major_version, minor_version)) { if (name == NULL) { Exceptions::fthrow( THREAD_AND_LOCATION, vmSymbols::java_lang_UnsupportedClassVersionError(), "Unsupported major.minor version %u.%u", major_version, minor_version); } else { ResourceMark rm(THREAD); Exceptions::fthrow( THREAD_AND_LOCATION, vmSymbols::java_lang_UnsupportedClassVersionError(), "%s : Unsupported major.minor version %u.%u", name->as_C_string(), major_version, minor_version); } return nullHandle; }
魔数验证了值,而版本号能够让当前虚拟机判断是否支持运行这个Class版本的文件。通常在读取文件中相关字节码时,都会调用guarantee_more()方法,以保证文件中有足够的字节信息用来读取。
元数据验证的逻辑也大部分都在类解析阶段完成,例如在parseClassFile()中对父类的验证逻辑如下:
if (super_klass.not_null()) { 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); } }
验证父类不能为接口或有final修饰的类,否则将出现异常。
在classFileParse.cpp文件中定义了一系列verify_xxx()或check_xxx()方法,都是对元数据进行验证的,有兴趣的读者可自行阅读。
在InstanceKlass::link_class_impl()方法中调用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; return Verifier::verify(this_oop, mode, this_oop->should_verify_class(), CHECK_false); }
Verifier::verify()方法的实现如下:
bool Verifier::verify(instanceKlassHandle klass, Verifier::Mode mode, bool should_verify_class, TRAPS) { HandleMark hm; ResourceMark rm(THREAD); Symbol* exception_name = NULL; const size_t message_buffer_len = klass->name()->utf8_length() + 1024; char* message_buffer = NEW_RESOURCE_ARRAY(char, message_buffer_len); char* exception_message = message_buffer; const char* klassName = klass->external_name(); // 失败回退,新的使用StackMapTable属性进行验证的叫类型检查,而之前的叫类型推导验证 bool can_failover = FailOverToOldVerifier && klass->major_version() < NOFAILOVER_MAJOR_VERSION; // If the class should be verified, first see if we can use the split // verifier. If not, or if verification fails and FailOverToOldVerifier // is set, then call the inference verifier. if (is_eligible_for_verification(klass, should_verify_class)) { // STACKMAP_ATTRIBUTE_MAJOR_VERSION的值为50 if (klass->major_version() >= STACKMAP_ATTRIBUTE_MAJOR_VERSION) { ClassVerifier split_verifier(klass, THREAD); split_verifier.verify_class(THREAD); exception_name = split_verifier.result(); if ( can_failover && !HAS_PENDING_EXCEPTION && ( exception_name == vmSymbols::java_lang_VerifyError() || exception_name == vmSymbols::java_lang_ClassFormatError() ) ) { if (TraceClassInitialization || VerboseVerification) { tty->print_cr("Fail over class verification to old verifier for: %s", klassName); } // 如果失败,则回退到类型推导验证 exception_name = inference_verify(klass, message_buffer, message_buffer_len, THREAD); } if (exception_name != NULL) { exception_message = split_verifier.exception_message(); } } else { // 推导验证 exception_name = inference_verify(klass, message_buffer, message_buffer_len, THREAD); } } // ... }
由于数据流验证的高复杂性,虚拟机设计团队为了避免过多的时间消耗在字节码验证阶 段,在JDK 1.6之后的Javac编译器和Java虚拟机中进行了一项优化,给方法体的Code属性的 属性表中增加了一项名为“StackMapTable”的属性,这项属性描述了方法体中所有的基本块 (Basic Block,按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节 码验证期间,就不需要根据程序推导这些状态的合法性,只需要检查StackMapTable属性中 的记录是否合法即可。这样将字节码验证的类型推导转变为类型检查从而节省一些时间。
在JDK 1.6的HotSpot虚拟机中提供了-XX:-UseSplitVerifier选项来关闭这项优化,或者使用参数-XX:+FailOverToOldVerifier要求在类型校验失败的时候退回到旧的类型推导方式进 行校验。而在JDK 1.7之后,对于主版本号大于50的Class文件,使用类型检查来完成数据流 分析校验则是唯一的选择,不允许再退回到类型推导的校验方式。
验证阶段不是必须的,如果代码运行已经稳定了之后,可以通过设置参数-Xverfy:none参数来关闭类验证,减少虚拟机的类加载时间,提高运行效率。
相关文章的链接如下:
1、在Ubuntu 16.04上编译OpenJDK8的源代码
13、类加载器
14、类的双亲委派机制
15、核心类的预装载
16、Java主类的装载
17、触发类的装载
18、类文件介绍
19、文件流
20、解析Class文件
21、常量池解析(1)
22、常量池解析(2)
23、字段解析(1)
24、字段解析之伪共享(2)
25、字段解析(3)
28、方法解析
29、klassVtable与klassItable类的介绍
30、计算vtable的大小
31、计算itable的大小
32、解析Class文件之创建InstanceKlass对象
33、字段解析之字段注入
34、类的连接
作者持续维护的个人博客 classloading.com。
关注公众号,有HotSpot源码剖析系列文章!