原创

类的连接之验证

在介绍字节码连接之前,有必要介绍一下字节码验证。HotSpot虚拟机其实会遵守《Java虚拟机规范》,对Class文件中包含的信息进行合法性验证,以保证虚拟机的安全。从整体上来看,验证阶段大致上会进行如下4方面的验证:

  1. 文件格式验证:包括魔数,版本号等;
  2. 元数据验证:对程序进行语义分析,如是否有父类,是否继承了不被继承的类,不是抽象类,是否实现了父类或者接口中的所有要求被实现的方法;
  3. 字节码验证:指令级别的语义验证,如跳转指令不会跳转到方法体以外的代码上;
  4. 符号引用验证:符号引用转化为直接引用的时候,可以看作是对类自身以外的信息进行匹配验证,如通过全限定名,是否能找到对应的类。
    文件格式的验证大部分都会在解析类文件的parseClassFile()函数中进行,例如对魔数,版本号的验证,如下:
    ```
    cfs->guaranteemore(8, CHECK(nullHandle)); // magic, major, minor
    u4 magic = cfs->get_u4_fast();
    guarantee_property(magic == JAVA_CLASSFILEMAGIC,"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()) {
THROWMSG(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参数来关闭类验证,减少虚拟机的类加载时间,提高运行效率。

正文到此结束