JVM 如何实际转换对象并发出 ClassCastException?

How does the JVM actually cast objects and issue a ClassCastException?

当您将对象投射到特定的 class(例如 casted = (NewClass)obj;)时,幕后会发生什么?我猜 JVM 会以某种方式检查 obj 的实际 class 是否是 NewClass 的子 class,但是对象实例是否有办法知道何时它正在 "casted"?

也欢迎指出一些 JVM 实现的 documentation/FAQ,因为我还没有找到任何...

关于 "why an object should know when it is being casted?" 的编辑:

我最近在考虑实现一种既是 InputStream 又是 OutputStream 的管道。因为这些是 classes 而不是接口,所以不能两者都是(因为 Java 不能扩展多个 classes),所以我想知道是否有一种方法可以显示一个对象通过某种可拦截的转换操作,对自身的不同看法。

并不是说我无论如何都想实施它(好吧,我会为了测试和有趣的黑客目的而实施它;))因为它太危险了并且会允许各种疯狂的滥用和误用。

JVM 有一个字节码,checkcast,用于检查是否可以有效地执行转换。 JLS§5.5.3, and the details of the checkcast bytecode are described in the JVM spec§6.5 中描述了实际的转换检查语义。例如,

public static void main(String args[]) {
   Number n = Integer.valueOf(66); // Autoboxing

   incr((Integer) n);

   System.out.println(n);
}

产生

 public static void main(java.lang.String[]);
    Code:
       0: bipush        66
       2: invokestatic  #3                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       5: astore_1
       6: aload_1
       7: checkcast     #4                  // class java/lang/Integer
      10: invokestatic  #5                  // Method incr:(Ljava/lang/Integer;)V
      13: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
      16: aload_1
      17: invokevirtual #7                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      20: return

此外,通过深入研究 Hotspot 的源代码,我们可以看到 checkcast 的两个实现,一个用于生产,另一个用于简单测试和早期移植。

首先显示的是基于生产模板的解释器(感谢 apangin 让我意识到了这一点),它生成的代码对应于对要进行强制转换检查的引用进行空检查,加载class 信息、对子类型检查的调用以及可能跳转到抛出 ClassCastException:

的代码
void TemplateTable::checkcast() {
  transition(atos, atos);
  Label done, is_null, ok_is_subtype, quicked, resolved;
  __ testptr(rax, rax); // object is in rax
  __ jcc(Assembler::zero, is_null);

  // Get cpool & tags index
  __ get_cpool_and_tags(rcx, rdx); // rcx=cpool, rdx=tags array
  __ get_unsigned_2_byte_index_at_bcp(rbx, 1); // rbx=index
  // See if bytecode has already been quicked
  __ cmpb(Address(rdx, rbx,
                  Address::times_1,
                  Array<u1>::base_offset_in_bytes()),
          JVM_CONSTANT_Class);
  __ jcc(Assembler::equal, quicked);
  __ push(atos); // save receiver for result, and for GC
  call_VM(noreg, CAST_FROM_FN_PTR(address, InterpreterRuntime::quicken_io_cc));
  // vm_result_2 has metadata result
  __ get_vm_result_2(rax, r15_thread);
  __ pop_ptr(rdx); // restore receiver
  __ jmpb(resolved);

  // Get superklass in rax and subklass in rbx
  __ bind(quicked);
  __ mov(rdx, rax); // Save object in rdx; rax needed for subtype check
  __ movptr(rax, Address(rcx, rbx,
                       Address::times_8, sizeof(ConstantPool)));

  __ bind(resolved);
  __ load_klass(rbx, rdx);

  // Generate subtype check.  Blows rcx, rdi.  Object in rdx.
  // Superklass in rax.  Subklass in rbx.
  __ gen_subtype_check(rbx, ok_is_subtype);

  // Come here on failure
  __ push_ptr(rdx);
  // object is at TOS
  __ jump(ExternalAddress(Interpreter::_throw_ClassCastException_entry));

  // Come here on success
  __ bind(ok_is_subtype);
  __ mov(rax, rdx); // Restore object in rdx

  // Collect counts on whether this check-cast sees NULLs a lot or not.
  if (ProfileInterpreter) {
    __ jmp(done);
    __ bind(is_null);
    __ profile_null_seen(rcx);
  } else {
    __ bind(is_null);   // same as 'done'
  }
  __ bind(done);
}

简单的非生产解释器可以在 bytecodeInterpreter.cpp line 2048 向我们展示另一个示例。当达到 checkcast 时,我们实际上可以看到 sample 兼容的字节码解释器中发生了什么:

  CASE(_checkcast):
      if (STACK_OBJECT(-1) != NULL) {
        VERIFY_OOP(STACK_OBJECT(-1));
        u2 index = Bytes::get_Java_u2(pc+1);
        if (ProfileInterpreter) {
          // needs Profile_checkcast QQQ
          ShouldNotReachHere();
        }
        // Constant pool may have actual klass or unresolved klass. If it is
        // unresolved we must resolve it
        if (METHOD->constants()->tag_at(index).is_unresolved_klass()) {
          CALL_VM(InterpreterRuntime::quicken_io_cc(THREAD), handle_exception);
        }
        Klass* klassOf = (Klass*) METHOD->constants()->slot_at(index).get_klass();
        Klass* objKlassOop = STACK_OBJECT(-1)->klass(); //ebx
        //
        // Check for compatibilty. This check must not GC!!
        // Seems way more expensive now that we must dispatch
        //
        if (objKlassOop != klassOf &&
            !objKlassOop->is_subtype_of(klassOf)) {
          ResourceMark rm(THREAD);
          const char* objName = objKlassOop->external_name();
          const char* klassName = klassOf->external_name();
          char* message = SharedRuntime::generate_class_cast_message(
            objName, klassName);
          VM_JAVA_ERROR(vmSymbols::java_lang_ClassCastException(), message);
        }
      } else {
        if (UncommonNullCast) {
            //              istate->method()->set_null_cast_seen();
            // [RGV] Not sure what to do here!

        }
      }
      UPDATE_PC_AND_CONTINUE(3);

简而言之,它从堆栈中获取参数,从常量池中获取 Class 对象(必要时解析),并检查参数是否可分配给 class。如果不是,它获取对象类型的名称和尝试转换到的 class 的名称,构造异常消息,并随该消息抛出 ClassCastException。奇怪的是,抛出 ClassCastException 的机制与用于 athrow 字节码的机制不同(使用 VM_JAVA_ERROR 而不是 set_pending_exception)。

对编辑的回应: 最好只使用类型系统和 OOP 原则而不是奇怪的 Java 内部机制。只要有一个 Pipe class (扩展对象),它有一个 getInputStream 和一个 getOutputStream 方法,每个 returns 一个对应内部 class(即 Pipe$PipeInputStreamPipe$PipeOutputStream,两者都访问 Pipe 的 private/protected 状态)

此检查发生两次,一次是在编译器的编译期间,另一次是在 运行 期间由 jvm。

在编译时,编译器会查看元素是否属于同一层次结构(例如:Vehicle->Car->BMW),并且只有在 运行jvm 才看到什么对象实际上 is.The 编译器处理引用,而 jvm 处理对象。

现在,在类转换异常期间,被错误转换的元素位于同一层次结构中,因此编译成功,但是 运行 期间的 jvm 会看到对象并识别出不正确的转换,异常是抛出。