diff --git a/CHANGELOG.md b/CHANGELOG.md
index 388823233cd06e7f2f4212891272e4d6551c31f7..525292e87267f6a53daabed30a1ca02e1bed77dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
 
 ## [(next version)](./README.md#for-developers)
 
+* Updated ProB 2 to version 4.0.0-SNAPSHOT.
+* Improved the performance of loading machines by reusing the existing instance of ProB instead of starting a new one for each machine.
 * Significantly refactored the logic for parsing commands and their arguments.
 	* This is an internal change and should not affect any user-visible behavior. That is, all inputs that were accepted by previous versions should still be accepted - if any previously valid inputs are no longer accepted, this is a bug.
 	* As a side effect, the inspection and code completion features now work better in a few edge cases.
diff --git a/build.gradle b/build.gradle
index 1d79d7cf24963d26188d0d2756d18eed45a0694b..18edb4d51d3c18078db28e052f6915b91175360d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -39,7 +39,7 @@ configurations.all {
 dependencies {
 	implementation(group: "com.google.code.gson", name: "gson", version: "2.8.6")
 	implementation(group: "com.google.guava", name: "guava", version: "28.2-jre")
-	implementation(group: "de.hhu.stups", name: "de.prob2.kernel", version: "3.11.0")
+	implementation(group: "de.hhu.stups", name: "de.prob2.kernel", version: "4.0.0-SNAPSHOT")
 	implementation(group: "io.github.spencerpark", name: "jupyter-jvm-basekernel", version: "2.3.0")
 	implementation(group: "org.jetbrains", name: "annotations", version: "18.0.0")
 	implementation(group: "se.sawano.java", name: "alphanumeric-comparator", version: "1.4.1")
diff --git a/src/main/java/de/prob2/jupyter/ProBKernel.java b/src/main/java/de/prob2/jupyter/ProBKernel.java
index 95e5c6909545b61e580ca3cc28cf2b1f8831b369..8513b96debc84b0cdcd582cac9b53defc2139afe 100644
--- a/src/main/java/de/prob2/jupyter/ProBKernel.java
+++ b/src/main/java/de/prob2/jupyter/ProBKernel.java
@@ -16,6 +16,7 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.Properties;
 import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
@@ -25,10 +26,12 @@ import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
 
+import de.prob.animator.ReusableAnimator;
 import de.prob.animator.domainobjects.ErrorItem;
 import de.prob.exception.ProBError;
 import de.prob.scripting.ClassicalBFactory;
 import de.prob.statespace.AnimationSelector;
+import de.prob.statespace.StateSpace;
 import de.prob.statespace.Trace;
 import de.prob2.jupyter.commands.AssertCommand;
 import de.prob2.jupyter.commands.BrowseCommand;
@@ -218,6 +221,7 @@ public final class ProBKernel extends BaseKernel {
 	}
 	
 	private final @NotNull AnimationSelector animationSelector;
+	private final @NotNull ReusableAnimator animator;
 	
 	private final @NotNull Map<@NotNull String, @NotNull Command> commands;
 	private final @NotNull AtomicReference<@Nullable Thread> currentEvalThread;
@@ -226,12 +230,13 @@ public final class ProBKernel extends BaseKernel {
 	private @NotNull Path currentMachineDirectory;
 	
 	@Inject
-	private ProBKernel(final @NotNull Injector injector, final @NotNull ClassicalBFactory classicalBFactory, final @NotNull AnimationSelector animationSelector) {
+	private ProBKernel(final @NotNull Injector injector, final @NotNull ClassicalBFactory classicalBFactory, final @NotNull AnimationSelector animationSelector, final @NotNull ReusableAnimator animator) {
 		super();
 		
 		this.setShouldReplaceStdStreams(false);
 		
 		this.animationSelector = animationSelector;
+		this.animator = animator;
 		
 		this.commands = COMMAND_CLASSES.stream()
 			.map(injector::getInstance)
@@ -240,7 +245,10 @@ public final class ProBKernel extends BaseKernel {
 		this.currentEvalThread = new AtomicReference<>(null);
 		this.variables = new HashMap<>();
 		
-		this.animationSelector.changeCurrentAnimation(new Trace(classicalBFactory.create("(initial Jupyter machine)", "MACHINE repl END").load()));
+		this.switchMachine(Paths.get(""), stateSpace -> {
+			classicalBFactory.create("(initial Jupyter machine)", "MACHINE repl END").loadIntoStateSpace(stateSpace);
+			return new Trace(stateSpace);
+		});
 		this.currentMachineDirectory = Paths.get("");
 	}
 	
@@ -272,6 +280,28 @@ public final class ProBKernel extends BaseKernel {
 		this.currentMachineDirectory = currentMachineDirectory;
 	}
 	
+	public void unloadMachine() {
+		final Trace oldTrace = this.animationSelector.getCurrentTrace();
+		if (oldTrace != null) {
+			assert oldTrace.getStateSpace() == this.animator.getCurrentStateSpace();
+			this.animationSelector.changeCurrentAnimation(null);
+			oldTrace.getStateSpace().kill();
+		}
+		this.setCurrentMachineDirectory(Paths.get(""));
+	}
+	
+	public void switchMachine(final @NotNull Path machineDirectory, final @NotNull Function<@NotNull StateSpace, @NotNull Trace> newTraceCreator) {
+		this.unloadMachine();
+		final StateSpace newStateSpace = this.animator.createStateSpace();
+		try {
+			this.animationSelector.changeCurrentAnimation(newTraceCreator.apply(newStateSpace));
+		} catch (final RuntimeException e) {
+			newStateSpace.kill();
+			throw e;
+		}
+		this.setCurrentMachineDirectory(machineDirectory);
+	}
+	
 	@Override
 	public @NotNull String getBanner() {
 		return "ProB Interactive Expression and Predicate Evaluator (on Jupyter)\nType \":help\" for more information.";
diff --git a/src/main/java/de/prob2/jupyter/commands/LoadCellCommand.java b/src/main/java/de/prob2/jupyter/commands/LoadCellCommand.java
index cc5a928295dd370a6a3b588d108bb5a113443b22..b0525f60c7e1838f8411eef1e099054590ff722c 100644
--- a/src/main/java/de/prob2/jupyter/commands/LoadCellCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/LoadCellCommand.java
@@ -77,10 +77,13 @@ public final class LoadCellCommand implements Command {
 		final String body = args.get(CODE_PARAM);
 		final Map<String, String> preferences = CommandUtils.parsePreferences(args.get(PREFS_PARAM));
 		
-		this.animationSelector.changeCurrentAnimation(new Trace(CommandUtils.withSourceCode(body, () ->
-			this.classicalBFactory.create("(machine from Jupyter cell)", body).load(preferences)
-		)));
-		this.proBKernelProvider.get().setCurrentMachineDirectory(Paths.get(""));
+		this.proBKernelProvider.get().switchMachine(Paths.get(""), stateSpace -> {
+			stateSpace.changePreferences(preferences);
+			CommandUtils.withSourceCode(body, () ->
+				this.classicalBFactory.create("(machine from Jupyter cell)", body).loadIntoStateSpace(stateSpace)
+			);
+			return new Trace(stateSpace);
+		});
 		return new DisplayData("Loaded machine: " + this.animationSelector.getCurrentTrace().getStateSpace().getMainComponent());
 	}
 	
diff --git a/src/main/java/de/prob2/jupyter/commands/LoadFileCommand.java b/src/main/java/de/prob2/jupyter/commands/LoadFileCommand.java
index ccb1456850bf696d1d6a52670a71f013fb51739b..b1cccf91638c0df1086741a676c4c162567568f3 100644
--- a/src/main/java/de/prob2/jupyter/commands/LoadFileCommand.java
+++ b/src/main/java/de/prob2/jupyter/commands/LoadFileCommand.java
@@ -101,13 +101,16 @@ public final class LoadFileCommand implements Command {
 		}
 		final Map<String, String> preferences = CommandUtils.parsePreferences(args.get(PREFS_PARAM));
 		
-		try {
-			final ModelFactory<?> factory = this.injector.getInstance(FactoryProvider.factoryClassFromExtension(extension));
-			this.animationSelector.changeCurrentAnimation(new Trace(factory.extract(machineFilePath.toString()).load(preferences)));
-			this.proBKernelProvider.get().setCurrentMachineDirectory(machineFileDirectory);
-		} catch (IOException | ModelTranslationError e) {
-			throw new RuntimeException(e);
-		}
+		final ModelFactory<?> factory = this.injector.getInstance(FactoryProvider.factoryClassFromExtension(extension));
+		this.proBKernelProvider.get().switchMachine(machineFileDirectory, stateSpace -> {
+			stateSpace.changePreferences(preferences);
+			try {
+				factory.extract(machineFilePath.toString()).loadIntoStateSpace(stateSpace);
+			} catch (final IOException | ModelTranslationError e) {
+				throw new RuntimeException(e);
+			}
+			return new Trace(stateSpace);
+		});
 		return new DisplayData("Loaded machine: " + this.animationSelector.getCurrentTrace().getStateSpace().getMainComponent());
 	}