Blog

hotspot の NullPointerException 時の表示をわかりやすくできないかなあと思って調べたけど挫折した

解決したいこと

foo.bar(baz.boz()); みたいなケースで、NPE が発生した場合に、foo が null で例外発生したのか baz が null で例外発生したのかわからん

やってみる

bash configure --with-native-debug-symbols=internal --enable-headless-only --with-debug-level=slowdebug  --disable-warnings-as-errors
make

動かしてみると以下のような出力を得られる。

Exception in thread "main" java.lang.NullPointerException
        at NPE.main(NPE.java:4)

これは以下のソースから出力されている。Thread 単位で uncaught exception が発生させられている。

jdk/src/java.base/share/classes/java/lang/ThreadGroup.java の uncaughtException だ。

uncaughtException は jdk/src/java.base/share/classes/java/lang/Thread.java の dispatchUncaughtException から呼ばれているようだ。

これは hotspot/src/share/vm/runtime/thread.cpp の JavaThread::exit から呼ばれている。

ここには hotspot/src/share/vm/prims/jni.cpp の jni_DetachCurrentThread から来ていることがわかる。

更にたどるとjdk/src/java.base/share/native/libjli/java.c のJavaMain がエントリポイントになっていて

/* Invoke main method. */
(*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);


/*
 * The launcher's exit code (in the absence of calls to
 * System.exit) will be non-zero if main threw an exception.
 */
ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;

のあたりで処理が行われる。つまり、psvm 実行された後で、例外が設定されているかどうかを見て、例外がVMに設定されていれば、例外を処理するという形。

問題は、NullPointerException のインスタンスが作成される箇所がどこかという点なので、そこまで追う。

ここで、env は JNIEnv で、これの定義は hotspot/src/share/vm/prims/jni.h にある。

void CallStaticVoidMethodV(jclass cls, jmethodID methodID,
                           va_list args) {
    functions->CallStaticVoidMethodV(this,cls,methodID,args);
}

であって、functions の定義は 

const struct JNINativeInterface_ *functions;

なので、hotspot/src/share/vm/prims/jni.cpp に実装がある。で、それは同じファイルの jni_invoke_static にいく。

hotspot/src/share/vm/runtime/javaCalls.cpp の JavaCalls::call に行くが、ここでまさかの os::os_exception_wrapper に。。

os::os_exception_wrapper は、Win32 対応で必要なだけの存在らしく、実際にはこれだけのことです。linux だと素でただのラッパです。

void
os::os_exception_wrapper(java_call_t f, JavaValue* value, const methodHandle& method,
                         JavaCallArguments* args, Thread* thread) {
  f(value, method, args, thread);
}

つまり、JavaCalls::call は実際には以下と同じ。

call_helper(result, method, args, THREAD);

call_helper の中身は、色々やっているが実際にコールしているのは以下の部分。

  // do call
  { JavaCallWrapper link(method, receiver, result, CHECK);
    { HandleMark hm(thread);  // HandleMark used by HandleMarkCleaner


      StubRoutines::call_stub()(
        (address)&link,
        // (intptr_t*)&(result->_value), // see NOTE above (compiler problem)
        result_val_address,          // see NOTE above (compiler problem)
        result_type,
        method(),
        entry_point,
        args->parameters(),
        args->size_of_parameters(),
        CHECK
      );


      result = link.result();  // circumvent MS C++ 5.0 compiler bug (result is clobbered across call)
      // Preserve oop return value across possible gc points
      if (oop_result_flag) {
        thread->set_vm_result((oop) result->get_jobject());
      }
    }
  } // Exit JavaCallWrapper (can block - potential return oop must be preserved)

で、StubRoutines::call_stub が実際にメソッドを呼んでいる部分なわけですが、 その実装は hotspot/src/share/vm/runtime/stubRoutines.hpp にあります。

こいつは、マクロを使ったハックで pointer から function pointer に warnings 無しでキャストするということをやっていたりして、ちょっと読みにくいですが、実際には StubRoutines::_call_stub_entry をコールしているだけということになります。

で、 StubRoutines::_call_stub_entry はマシン語生成していて大変読みにくいということがわかった。

で、色々調べていくと TemplateTable::resolve_cache_and_index でメソッド呼び出し分コードは JIT されていることがわかった。

で、これは

InterpreterRuntime::resolve_from_cache を呼んでいる。

IRT_ENTRY(void, InterpreterRuntime::resolve_from_cache(JavaThread* thread, Bytecodes::Code bytecode)) {
  switch (bytecode) {
  case Bytecodes::_getstatic:
  case Bytecodes::_putstatic:
  case Bytecodes::_getfield:
  case Bytecodes::_putfield:
    resolve_get_put(thread, bytecode);
    break;
  case Bytecodes::_invokevirtual:
  case Bytecodes::_invokespecial:
  case Bytecodes::_invokestatic:
  case Bytecodes::_invokeinterface:
    resolve_invoke(thread, bytecode);
    break;
  case Bytecodes::_invokehandle:
    resolve_invokehandle(thread);
    break;
  case Bytecodes::_invokedynamic:
    resolve_invokedynamic(thread);
    break;
  default:
    fatal("unexpected bytecode: %s", Bytecodes::name(bytecode));
    break;
  }
}
IRT_END

InterpreterRuntime::resolve_invoke では、LinkResolver::resolve_invoke を呼んでいる

void InterpreterRuntime::resolve_invoke(JavaThread* thread, Bytecodes::Code bytecode) {
  Thread* THREAD = thread;
  // extract receiver from the outgoing argument list if necessary
  Handle receiver(thread, NULL);
  if (bytecode == Bytecodes::_invokevirtual || bytecode == Bytecodes::_invokeinterface) {
    ResourceMark rm(thread);
    methodHandle m (thread, method(thread));
    Bytecode_invoke call(m, bci(thread));
    Symbol* signature = call.signature();
    receiver = Handle(thread,
                  thread->last_frame().interpreter_callee_receiver(signature));
    assert(Universe::heap()->is_in_reserved_or_null(receiver()),
           "sanity check");
    assert(receiver.is_null() ||
           !Universe::heap()->is_in_reserved(receiver->klass()),
           "sanity check");
  }

  // resolve method
  CallInfo info;
  constantPoolHandle pool(thread, method(thread)->constants());

  {
    JvmtiHideSingleStepping jhss(thread);
    LinkResolver::resolve_invoke(info, receiver, pool,
                                 get_index_u2_cpcache(thread, bytecode), bytecode,
                                 CHECK);

LinkResolver::resolve_invoke からは resolve_invokevirtual に遷移している。

void LinkResolver::resolve_invoke(CallInfo& result, Handle recv, const constantPoolHandle& pool, int index, Bytecodes::Code byte, TRAPS) {
  switch (byte) {
    case Bytecodes::_invokestatic   : resolve_invokestatic   (result,       pool, index, CHECK); break;
    case Bytecodes::_invokespecial  : resolve_invokespecial  (result,       pool, index, CHECK); break;
    case Bytecodes::_invokevirtual  : resolve_invokevirtual  (result, recv, pool, index, CHECK); break;
    case Bytecodes::_invokehandle   : resolve_invokehandle   (result,       pool, index, CHECK); break;
    case Bytecodes::_invokedynamic  : resolve_invokedynamic  (result,       pool, index, CHECK); break;
    case Bytecodes::_invokeinterface: resolve_invokeinterface(result, recv, pool, index, CHECK); break;
  }
  return;
}

LinkResolver::resolve_invokevirtual から resolve_virtual_call へ。

void LinkResolver::resolve_invokevirtual(CallInfo& result, Handle recv,
                                          const constantPoolHandle& pool, int index,
                                          TRAPS) {

  LinkInfo link_info(pool, index, CHECK);
  KlassHandle recvrKlass (THREAD, recv.is_null() ? (Klass*)NULL : recv->klass());
  resolve_virtual_call(result, recv, recvrKlass, link_info, /*check_null_or_abstract*/true, CHECK);
}

LinkResolver::resolve_virtual_call は runtime_resolve_virtual_method を呼ぶ

void LinkResolver::resolve_virtual_call(CallInfo& result, Handle recv, KlassHandle receiver_klass,
                                        const LinkInfo& link_info,
                                        bool check_null_and_abstract, TRAPS) {
  methodHandle resolved_method = linktime_resolve_virtual_method(link_info, CHECK);
  runtime_resolve_virtual_method(result, resolved_method,
                                 link_info.resolved_klass(),
                                 recv, receiver_klass,
                                 check_null_and_abstract, CHECK);
}

LinkResolver::runtime_resolve_virtual_method に至って、ついに NullPointerException を投げている!!

void LinkResolver::runtime_resolve_virtual_method(CallInfo& result,
                                                  const methodHandle& resolved_method,
                                                  KlassHandle resolved_klass,
                                                  Handle recv,
                                                  KlassHandle recv_klass,
                                                  bool check_null_and_abstract,
                                                  TRAPS) {

  // setup default return values
  int vtable_index = Method::invalid_vtable_index;
  methodHandle selected_method;

  assert(recv.is_null() || recv->is_oop(), "receiver is not an oop");

  // runtime method resolution
  if (check_null_and_abstract && recv.is_null()) { // check if receiver exists
    THROW(vmSymbols::java_lang_NullPointerException()); ←←←← ここ!!
  }

で、この THROW の実体は以下の通り

#define THROW(name)                                 \
  { Exceptions::_throw_msg(THREAD_AND_LOCATION, name, NULL); return;  }

THREAD_AND_LOCATION は 以下

#define THREAD_AND_LOCATION                      THREAD, __FILE__, __LINE__

THREAD は以下。 要するに、各メソッドの引数に TRAPS をつけておいて、THREAD 変数で参照するってだけ。

// The THREAD & TRAPS macros facilitate the declaration of functions that throw exceptions.
// Convention: Use the TRAPS macro as the last argument of such a function; e.g.:
//
// int this_function_may_trap(int x, float y, TRAPS)

#define THREAD __the_thread__
#define TRAPS  Thread* THREAD

であって、Exceptions::_throw_msg は、以下の通り

void Exceptions::_throw_msg(Thread* thread, const char* file, int line, Symbol* name, const char* message,
                            Handle h_loader, Handle h_protection_domain) {
  // Check for special boot-strapping/vm-thread handling
  if (special_exception(thread, file, line, name, message)) return;
  // Create and throw exception
  Handle h_cause(thread, NULL);
  Handle h_exception = new_exception(thread, name, message, h_cause, h_loader, h_protection_domain);
  _throw(thread, file, line, h_exception, message);
}

ここまで調べた結果

なかなか目的を達成するのは難しそうなのでまた今度。