It's not the Isolate that's keeping the process alive, per se; it's actually the ReceivePort. The ReceivePort doesn't "know" that the isolate has been killed, so it's still happily waiting for events to come along.
Calling receiverPort.close() is what will allow the process to end. In fact, you don't even technically need to kill the isolate in order for the process to end, as long as you close the stream.
Here's a full version of your code which terminates immediately:
import 'dart:io';
import 'dart:isolate';
Isolate? isolate;
void printX(SendPort sendPort) {
print(sendPort);
}
void main() async {
var receiverPort = ReceivePort();
isolate = await Isolate.spawn(printX, receiverPort.sendPort);
receiverPort.close();
}
Additional note: If there is a listener subscribed to the ReceivePort, and the listener cancels its subscription, then the ReceivePort will close on its own; however, it's designed to buffer incoming events until a listener is subscribed - so if no listener ever subscribes, it never closes itself.
So this code will also terminate:
void main() async {
var receiverPort = ReceivePort();
var subscription = receiverPort.listen((message) {});
isolate = await Isolate.spawn(printX, receiverPort.sendPort);
subscription.cancel();
}