There is a race condition in JvmtiThreadState::state_for_while_locked() and ~JavaThread(). In state_for_while_locked(), we check the terminated state of the Thread and if it is not exiting, we create the JvmtiThreadState and record it in JavaThread::_jvmti_thread_state. In ~JavaThread(), we set the _terminated field to exiting, and then check _jvmti_thread_state to see if the JvmtiThreadState object needs to be deallocated. There is no common lock code between the two, so it is possible for state_for_while_locked() to see a non-exited thread state, but then the entirety of ~JavaThread() may run in another thread before the JvmtiThreadState is created and set in the JavaThread::_jvmti_thread_state field. In that case we end up with a live JvmtiThreadState that refers to a dead thread (as the check of _jvmti_thread_state in ~JavaThread() will see NULL and not deallocate the JvmtiThreadState).
A solution that appears to work:
In JvmtiThreadState::state_for_while_locked(), create the state unconditionally if it does not exist, and set it in JavaThread::_jvmti_thread_state. Then check for thread exited state and if the thread has exited, "rollback" and delete the JavaThreadState. Also mark the fields _jvmti_thread_state and _terminated as volatile and use memory fences to ensure that the loads and stores of those fields are visible to other threads.