Skip to content

Commit c0ca9c0

Browse files
ALikhachevSpace Team
authored andcommitted
[Maven] Introduce BTA classloader cache
Increases metaspace limit in the test to allow 2 classloader to be created ^KT-81435 Verification Pending
1 parent 4961890 commit c0ca9c0

File tree

8 files changed

+193
-13
lines changed

8 files changed

+193
-13
lines changed
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
invoker.environmentVariables.MAVEN_OPTS = -XX:MaxMetaspaceSize=150M
1+
# enough for 2 compiler classloaders
2+
invoker.environmentVariables.MAVEN_OPTS = -XX:MaxMetaspaceSize=300M
3+
# capture classloader creation lines from debug logs
4+
invoker.debug = true

libraries/tools/kotlin-maven-plugin-test/src/it/test-multimodule-in-process/module2/pom.xml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,21 @@
2929
</goals>
3030
</execution>
3131
</executions>
32+
33+
<!-- Apply a compiler plugin. It should currently lead to the creation of another classloader -->
34+
<configuration>
35+
<compilerPlugins>
36+
<plugin>all-open</plugin>
37+
</compilerPlugins>
38+
</configuration>
39+
40+
<dependencies>
41+
<dependency>
42+
<groupId>org.jetbrains.kotlin</groupId>
43+
<artifactId>kotlin-maven-allopen</artifactId>
44+
<version>${kotlin.version}</version>
45+
</dependency>
46+
</dependencies>
3247
</plugin>
3348
</plugins>
3449
</build>

libraries/tools/kotlin-maven-plugin-test/src/it/test-multimodule-in-process/module3/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@
3030
</execution>
3131
</executions>
3232
</plugin>
33+
<!-- Apply another plugin to ensure it does not affect the compiler classloader -->
34+
<plugin>
35+
<groupId>org.apache.maven.plugins</groupId>
36+
<artifactId>maven-source-plugin</artifactId>
37+
<version>2.1.2</version>
38+
</plugin>
3339
</plugins>
3440
</build>
3541

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
def buildLogFile = new File(basedir, "build.log")
2+
3+
def lines = buildLogFile.readLines()
4+
def createdClassloaders = lines.count { it.startsWith("[DEBUG] Creating classloader") }
5+
6+
// 1 classloader for modules 1, 3, 4, 5
7+
// 2 classloader for module 2 with a compiler plugin
8+
return createdClassloaders == 2

libraries/tools/kotlin-maven-plugin/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@
8282
<version>1</version>
8383
<scope>compile</scope>
8484
</dependency>
85+
<dependency>
86+
<groupId>com.google.guava</groupId>
87+
<artifactId>guava</artifactId>
88+
<version>33.5.0-jre</version>
89+
</dependency>
90+
<dependency>
91+
<groupId>org.jetbrains</groupId>
92+
<artifactId>annotations</artifactId>
93+
<version>26.0.2-1</version>
94+
</dependency>
8595
</dependencies>
8696

8797
<build>
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2010-2025 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
4+
*/
5+
6+
package org.jetbrains.kotlin.maven;
7+
8+
import com.google.common.cache.Cache;
9+
import com.google.common.cache.CacheBuilder;
10+
import org.jetbrains.annotations.NotNull;
11+
import org.jetbrains.kotlin.buildtools.api.SharedApiClassesClassLoader;
12+
13+
import java.io.File;
14+
import java.util.List;
15+
import java.util.Objects;
16+
import java.util.concurrent.TimeUnit;
17+
18+
/**
19+
* A static holder for a thread-safe cache of class loaders.
20+
* Persists the cache between builds and different mojos using the same build process unless a different classloader is used for loading the Maven plugin.
21+
* In such cases, the remaining cache is garbage collected with the related class loader.
22+
* The cache utilizes a combination of time-based eviction and soft references for efficient resource usage.
23+
* Almost copied "as is" from KGP's `ClassLoadersCacheHolder`
24+
*/
25+
class ClassLoaderCache {
26+
static class ClassLoaderCacheKey {
27+
private final List<File> classpath;
28+
private final ParentClassLoaderProvider parentClassLoaderProvider;
29+
30+
ClassLoaderCacheKey(List<File> classpath, ParentClassLoaderProvider parentClassLoaderProvider) {
31+
this.classpath = classpath;
32+
this.parentClassLoaderProvider = parentClassLoaderProvider;
33+
}
34+
35+
@Override
36+
public boolean equals(Object o) {
37+
if (o == null || getClass() != o.getClass()) return false;
38+
ClassLoaderCacheKey key = (ClassLoaderCacheKey) o;
39+
return Objects.equals(classpath, key.classpath) &&
40+
Objects.equals(parentClassLoaderProvider, key.parentClassLoaderProvider);
41+
}
42+
43+
@Override
44+
public int hashCode() {
45+
return Objects.hash(classpath, parentClassLoaderProvider);
46+
}
47+
48+
public List<File> getClasspath() {
49+
return classpath;
50+
}
51+
52+
public ParentClassLoaderProvider getParentClassLoaderProvider() {
53+
return parentClassLoaderProvider;
54+
}
55+
}
56+
57+
/**
58+
* The cache can be accessed by multiple compilations concurrently, hence the cache must be thread-safe.
59+
* As per `com.google.common.cache.Cache` documentation, implementations are expected to be thread-safe.
60+
* Utilizes the double-check locking singleton pattern, so @Volatile is crucial for correctness.
61+
*/
62+
private static volatile Cache<@NotNull ClassLoaderCacheKey, @NotNull ClassLoader> classLoaders;
63+
private static final Object lock = new Object();
64+
65+
static Cache<@NotNull ClassLoaderCacheKey, @NotNull ClassLoader> getCache(Long classLoaderCacheTimeoutInSeconds) {
66+
if (classLoaders == null) {
67+
synchronized(lock) {
68+
if (classLoaders == null) {
69+
classLoaders = CacheBuilder.newBuilder()
70+
.expireAfterAccess(classLoaderCacheTimeoutInSeconds, TimeUnit.SECONDS)
71+
.softValues()
72+
.build();
73+
}
74+
}
75+
}
76+
return classLoaders;
77+
}
78+
}
79+
80+
/**
81+
* A provider of the parent [ClassLoader] for newly created ClassLoader instances.
82+
* <p>
83+
* An implementation must override `equals` and `hashCode`! It's used as a part of a {@link Cache} key
84+
*/
85+
interface ParentClassLoaderProvider {
86+
ClassLoader getClassLoader();
87+
}
88+
89+
class SharedBuildToolsApiClassesClassLoaderProvider implements ParentClassLoaderProvider {
90+
@Override
91+
public ClassLoader getClassLoader() {
92+
return SharedApiClassesClassLoader.newInstance();
93+
}
94+
95+
@Override
96+
public int hashCode() {
97+
return SharedBuildToolsApiClassesClassLoaderProvider.class.hashCode();
98+
}
99+
100+
@Override
101+
public boolean equals(Object other) {
102+
return other instanceof SharedBuildToolsApiClassesClassLoaderProvider;
103+
}
104+
}

libraries/tools/kotlin-maven-plugin/src/main/java/org/jetbrains/kotlin/maven/K2JVMCompileMojo.java

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.nio.file.Paths;
4949
import java.time.Duration;
5050
import java.util.*;
51+
import java.util.concurrent.ExecutionException;
5152
import java.util.function.Consumer;
5253
import java.util.stream.Collectors;
5354
import java.util.stream.Stream;
@@ -102,6 +103,12 @@ public class K2JVMCompileMojo extends KotlinCompileMojoBase<K2JVMCompilerArgumen
102103
@Parameter(property = "kotlin.compiler.daemon", defaultValue = "true")
103104
protected boolean useDaemon;
104105

106+
@Parameter(property = "kotlin.compiler.classloader.cache.timeoutSeconds")
107+
@Nullable
108+
protected Long classLoaderCacheTimeoutSeconds;
109+
110+
private static final Duration DEFAULT_CLASSLOADER_CACHE_TIMEOUT = Duration.ofMinutes(30);
111+
105112
@Parameter(property = "kotlin.compiler.daemon.jvmArgs")
106113
protected List<String> kotlinDaemonJvmArgs;
107114

@@ -260,19 +267,36 @@ private KotlinToolchains getKotlinToolchains() throws MojoExecutionException {
260267
kotlinArtifactResolver.resolveArtifact("org.jetbrains.kotlin", "kotlin-build-tools-impl", getMavenPluginVersion()).stream(),
261268
kotlinArtifactResolver.resolveArtifact("org.jetbrains.kotlin", "kotlin-scripting-compiler-embeddable", getMavenPluginVersion()).stream()
262269
).collect(Collectors.toCollection(LinkedHashSet::new));
263-
URL[] urls = artifacts.stream().map(artifact -> {
264-
File file = artifact.getFile();
265-
try {
266-
return file.toURI().toURL();
267-
}
268-
catch (MalformedURLException e) {
269-
throw new RuntimeException("Failed to convert file to URL: " + file, e);
270-
}
271-
}).toArray(URL[]::new);
272-
ClassLoader btaClassLoader = new URLClassLoader(urls, SharedApiClassesClassLoader.newInstance());
270+
List<File> files = artifacts.stream().map(Artifact::getFile).collect(Collectors.toList());
271+
ClassLoader btaClassLoader = getBtaClassLoader(files);
273272
return KotlinToolchains.loadImplementation(btaClassLoader);
274-
} catch (Exception e) {
275-
throw new MojoExecutionException("Failed to load Kotlin Build Tools API implementation", e);
273+
} catch (Throwable t) {
274+
throw new MojoExecutionException("Failed to load Kotlin Build Tools API implementation", t);
275+
}
276+
}
277+
278+
private ClassLoader getBtaClassLoader(List<File> files) {
279+
ClassLoaderCache.ClassLoaderCacheKey cacheKey =
280+
new ClassLoaderCache.ClassLoaderCacheKey(files, new SharedBuildToolsApiClassesClassLoaderProvider());
281+
try {
282+
long cacheTimeout = classLoaderCacheTimeoutSeconds == null
283+
? DEFAULT_CLASSLOADER_CACHE_TIMEOUT.getSeconds()
284+
: classLoaderCacheTimeoutSeconds;
285+
return ClassLoaderCache.getCache(cacheTimeout).get(cacheKey, () -> {
286+
getLog().debug("Creating classloader for " + cacheKey.getClasspath());
287+
URL[] urls = cacheKey.getClasspath().stream().map(file -> {
288+
try {
289+
return file.toURI().toURL();
290+
}
291+
catch (MalformedURLException e) {
292+
throw new RuntimeException(e);
293+
}
294+
}).toArray(URL[]::new);
295+
return new URLClassLoader(urls, cacheKey.getParentClassLoaderProvider().getClassLoader());
296+
});
297+
}
298+
catch (ExecutionException e) {
299+
throw new RuntimeException(e);
276300
}
277301
}
278302

repo/artifacts-tests/src/test/resources/org/jetbrains/kotlin/kotlin-maven-plugin/kotlin-maven-plugin.pom

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@
8282
<version>1</version>
8383
<scope>compile</scope>
8484
</dependency>
85+
<dependency>
86+
<groupId>com.google.guava</groupId>
87+
<artifactId>guava</artifactId>
88+
<version>33.5.0-jre</version>
89+
</dependency>
90+
<dependency>
91+
<groupId>org.jetbrains</groupId>
92+
<artifactId>annotations</artifactId>
93+
<version>26.0.2-1</version>
94+
</dependency>
8595
</dependencies>
8696

8797
<build>

0 commit comments

Comments
 (0)