An uncaught exception will eventually be handled by the UncaughtExceptionHandler of the thread that is executing the code. The Thread class has methods for setting the handler for uncaught exceptions. The documentation of Thread#setDefaultUncaughtExceptionHandler explains the process:
Uncaught exception handling is controlled first by the thread, then by the thread's ThreadGroup object and finally by the default uncaught exception handler. If the thread does not have an explicit uncaught exception handler set, and the thread's thread group (including parent thread groups) does not specialize its uncaughtException method, then the default handler's uncaughtException method will be invoked.
By setting the default uncaught exception handler, an application can change the way in which uncaught exceptions are handled (such as logging to a specific device, or file) for those threads that would already accept whatever "default" behavior the system provided.
If no other uncaught exception handler has been set, the one in ThreadGroup prints the exception's stack trace to System.err.
The specification does not seem to say which thread should execute the uncaught exception handler, but on the Oracle/OpenJDK JVM it is the thread that is about to terminate (that is, the one that threw the exception).