Skip to content

Commit

Permalink
jooby-run: redo classloader strategy
Browse files Browse the repository at this point in the history
jooby-run (maven/gradle) run on top of jboss-modules. As today it uses a single classloader that contains the project classpath + any project dependency (jars).

Every time a change is done, jooby restarts the application, drop the classloader and creates a new classloader. This works OK but consumes memory due it must load all classes again.

Next release will change this by using two classloaders:

main: All project classes will be here.
jars: All external dependencies will be here.
Then on class change, we are doing to drop/re-create the main class loader only. ALL the external classes will remain the same.

This changes not only will reduce the memory usage on development but also makes application restart lot faster (of course).

fixes #3018
  • Loading branch information
jknack committed Jul 16, 2023
1 parent 3c07eab commit 0224265
Show file tree
Hide file tree
Showing 6 changed files with 122 additions and 65 deletions.
2 changes: 1 addition & 1 deletion modules/jooby-bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>2.37.0</version>
<version>2.27.2</version>
<configuration>
<skip>true</skip>
</configuration>
Expand Down
31 changes: 31 additions & 0 deletions modules/jooby-run/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,37 @@
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>copy</id>
<phase>generate-resources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}${file.separator}app${file.separator}libs</outputDirectory>
<artifactItems>
<artifactItem>
<groupId>io.jooby</groupId>
<artifactId>jooby</artifactId>
<version>3.0.0</version>
<overWrite>true</overWrite>
</artifactItem>
<artifactItem>
<groupId>io.jooby</groupId>
<artifactId>jooby-netty</artifactId>
<version>3.0.0</version>
<overWrite>true</overWrite>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,36 @@
*/
package io.jooby.run;

import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;

import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

import org.jboss.modules.ModuleFinder;
import org.jboss.modules.ModuleLoadException;
import org.jboss.modules.ModuleLoader;
import org.jboss.modules.ModuleSpec;

class FlattenClasspath implements ModuleFinder {

class JoobyModuleFinder implements ModuleFinder {
private static final String JARS = "jars";
private final Set<Path> resources;
private final Set<Path> jars;
private final String name;

FlattenClasspath(String name, Set<Path> resources, Set<Path> dependencies) {
JoobyModuleFinder(String name, Set<Path> resources, Set<Path> jars) {
this.name = name;
this.resources = new LinkedHashSet<>(resources.size() + dependencies.size());
this.resources = new LinkedHashSet<>(resources.size() + 1);
this.resources.addAll(resources);

this.resources.add(joobyRunHook(getClass()));
this.resources.addAll(dependencies);

this.jars = jars;
}

/**
Expand All @@ -51,12 +54,30 @@ static Path joobyRunHook(Class loader) {
}

@Override
public ModuleSpec findModule(String name, ModuleLoader delegateLoader)
throws ModuleLoadException {
public ModuleSpec findModule(String name, ModuleLoader delegateLoader) {
if (this.name.equals(name)) {
return Specs.spec(name, resources, Collections.emptySet());
return ModuleSpecHelper.create(name, resources, singleton(JARS));
} else if (JARS.equals(name)) {
return ModuleSpecHelper.create(name, jars, emptySet());
}

return null;
}

@Override
public String toString() {
var buffer = new StringBuilder();
resources.stream()
.forEach(
it -> {
if (!buffer.isEmpty()) {
buffer.append(File.pathSeparator);
}
buffer.append(it);
});
if (!jars.isEmpty()) {
buffer.append("; jars: ").append(jars);
}
return buffer.toString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.run;

import org.jboss.modules.Module;
import org.jboss.modules.ModuleFinder;
import org.jboss.modules.ModuleLoader;

class JoobyModuleLoader extends ModuleLoader {

JoobyModuleLoader(ModuleFinder finder) {
super(finder);
}

public void unload(String name, final Module module) {
super.unloadModuleLocal(name, module);
}
}
41 changes: 11 additions & 30 deletions modules/jooby-run/src/main/java/io/jooby/run/JoobyRun.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@

import org.jboss.modules.Module;
import org.jboss.modules.ModuleClassLoader;
import org.jboss.modules.ModuleFinder;
import org.jboss.modules.ModuleLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -49,28 +47,11 @@
*/
public class JoobyRun {

private static class Event {
private final long time;

Event(long time) {
this.time = time;
}
}

private static class ExtModuleLoader extends ModuleLoader {

ExtModuleLoader(ModuleFinder... finders) {
super(finders);
}

public void unload(String name, final Module module) {
super.unloadModuleLocal(name, module);
}
}
private record Event(long time) {}

private static class AppModule {
private final Logger logger;
private final ExtModuleLoader loader;
private final JoobyModuleLoader loader;
private final JoobyRunOptions conf;
private Module module;
private ClassLoader contextClassLoader;
Expand All @@ -85,7 +66,7 @@ private static class AppModule {

AppModule(
Logger logger,
ExtModuleLoader loader,
JoobyModuleLoader loader,
ClassLoader contextClassLoader,
JoobyRunOptions conf) {
this.logger = logger;
Expand Down Expand Up @@ -118,7 +99,6 @@ public Exception start() {
if (port != null) {
args.add("server.port=" + port);
}
args.add("server.join=false");
module.run(conf.getMainClass(), args.toArray(new String[0]));
} catch (ClassNotFoundException x) {
String message = x.getMessage();
Expand Down Expand Up @@ -147,6 +127,7 @@ public Exception start() {
}

private void printErr(Throwable source) {
source.printStackTrace();
Throwable cause = withoutReflection(source);
StackTraceElement[] stackTrace = cause.getStackTrace();
int truncateAt = stackTrace.length;
Expand Down Expand Up @@ -361,14 +342,14 @@ public void start() throws Throwable {
.map(Path::toString)
.collect(Collectors.joining(File.pathSeparator));
System.setProperty("jooby.run.classpath", classPathString);
var finder = new JoobyModuleFinder(options.getProjectName(), resources, dependencies);

ModuleFinder[] finders = {
new FlattenClasspath(options.getProjectName(), resources, dependencies)
};

ExtModuleLoader loader = new ExtModuleLoader(finders);
module =
new AppModule(logger, loader, Thread.currentThread().getContextClassLoader(), options);
new AppModule(
logger,
new JoobyModuleLoader(finder),
Thread.currentThread().getContextClassLoader(),
options);
ScheduledExecutorService se;
Exception error = module.start();
if (error == null) {
Expand Down Expand Up @@ -474,7 +455,7 @@ private void onFileChange(DirectoryChangeEvent.EventType kind, Path path) {
}

@SuppressWarnings("unchecked")
private static <E extends Throwable> void sneakyThrow0(final Throwable x) throws E {
public static <E extends Throwable> E sneakyThrow0(final Throwable x) throws E {
throw (E) x;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,41 @@

import org.jboss.modules.DependencySpec;
import org.jboss.modules.ModuleDependencySpecBuilder;
import org.jboss.modules.ModuleLoadException;
import org.jboss.modules.ModuleSpec;
import org.jboss.modules.PathUtils;
import org.jboss.modules.ResourceLoaderSpec;
import org.jboss.modules.filter.PathFilters;

final class Specs {
final class ModuleSpecHelper {

private Specs() {}
private ModuleSpecHelper() {}

static DependencySpec metaInf(String moduleName) {
return new ModuleDependencySpecBuilder()
.setImportFilter(PathFilters.acceptAll())
.setExportFilter(PathFilters.getMetaInfServicesFilter())
.setName(moduleName)
.setOptional(false)
.build();
public static ModuleSpec create(String name, Set<Path> resources, Set<String> dependencies) {
ModuleSpec.Builder builder = newModule(name, resources);

// dependencies
for (String dependency : dependencies) {
builder.addDependency(
new ModuleDependencySpecBuilder()
.setImportFilter(PathFilters.acceptAll())
.setExportFilter(PathFilters.getMetaInfServicesFilter())
.setName(dependency)
.setOptional(false)
.build());
}
return builder.create();
}

public static ModuleSpec spec(String name, Set<Path> resources, Set<String> dependencies)
throws ModuleLoadException {
private static ModuleSpec.Builder newModule(String name, Set<Path> resources) {
try {
ModuleSpec.Builder builder = ModuleSpec.build(name);
// Add all JDK classes
builder.addDependency(DependencySpec.createSystemDependencySpec(PathUtils.getPathSet(null)));
// needed, so that the module can load classes from the resource root
builder.addDependency(DependencySpec.createLocalDependencySpec());
// Add the module's own content
builder.addDependency(DependencySpec.OWN_DEPENDENCY);

for (Path path : resources) {
if (Files.isDirectory(path)) {
builder.addResourceRoot(
Expand All @@ -49,18 +61,9 @@ public static ModuleSpec spec(String name, Set<Path> resources, Set<String> depe
createResourceLoaderSpec(createJarResourceLoader(new JarFile(path.toFile()))));
}
}

// needed, so that the module can load classes from the resource root
builder.addDependency(DependencySpec.createLocalDependencySpec());
// add dependency on the JDK paths
builder.addDependency(DependencySpec.createSystemDependencySpec(PathUtils.getPathSet(null)));
// dependencies
for (String dependency : dependencies) {
builder.addDependency(Specs.metaInf(dependency));
}
return builder.create();
return builder;
} catch (IOException x) {
throw new ModuleLoadException(name, x);
throw JoobyRun.sneakyThrow0(x);
}
}
}

0 comments on commit 0224265

Please sign in to comment.