类的连接之验证

在介绍字节码连接之前,有必要介绍一下字节码验证。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的源代码 

2、调试HotSpot源代码

3、HotSpot项目结构 

4、HotSpot的启动过程 

5、HotSpot二分模型(1)

6、HotSpot的类模型(2)  

7、HotSpot的类模型(3) 

8、HotSpot的类模型(4)

9、HotSpot的对象模型(5)  

10、HotSpot的对象模型(6) 

11、操作句柄Handle(7)

12、句柄Handle的释放(8)

13、类加载器 

14、类的双亲委派机制 

15、核心类的预装载

16、Java主类的装载  

17、触发类的装载  

18、类文件介绍 

19、文件流 

20、解析Class文件 

21、常量池解析(1) 

22、常量池解析(2)

23、字段解析(1)

24、字段解析之伪共享(2) 

25、字段解析(3)  

26、字段解析之OopMapBlock(4)

27、方法解析之Method与ConstMethod介绍  

28、方法解析

29、klassVtable与klassItable类的介绍  

30、计算vtable的大小 

31、计算itable的大小 

32、解析Class文件之创建InstanceKlass对象 

33、字段解析之字段注入 

34、类的连接  

作者持续维护的个人博客  classloading.com

关注公众号,有HotSpot源码剖析系列文章!

  

原文地址:https://www.cnblogs.com/mazhimazhi/p/13473410.html