From 253cdb1b8ff2e3a0f8a1295edd18646107322d45 Mon Sep 17 00:00:00 2001 From: Simon Ogorodnik Date: Wed, 22 Feb 2023 01:54:02 +0200 Subject: [PATCH] KT-56789: Fix memory leak in CoreJrtFileSystem CoreJrtFileSystem uses JrtFileSystemProvider provider to read contents of jrt-fs from JDK Implementation of FileSystems.newFileSystem causes metaspace memory leak that wasn't fixed until JDK 17, see https://bugs.openjdk.java.net/browse/JDK-8260621 When FileSystems.newFileSystem used to create jrt-fs on JDK9 with provided java.home value it creates new ClassLoader under-the-hood, which subsequently leaks due to aforementioned bug Remove conditional usage of `java.home` + FileSystems.newFileSystem and switch to use jrt-fs classloader cache regardless of compiler runtime JDK to reduce classloader leaks ^KT-56789 --- .../cli/jvm/modules/CoreJrtFileSystem.kt | 27 +++++++++---------- .../jetbrains/kotlin/gradle/KotlinDaemonIT.kt | 18 +++++++++++++ .../daemonJvmResourceLimits/build.gradle.kts | 10 +++++++ .../daemonJvmResourceLimits/gradle.properties | 1 + .../src/main/kotlin/helloWorld.kt | 9 +++++++ 5 files changed, 50 insertions(+), 15 deletions(-) create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/build.gradle.kts create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/gradle.properties create mode 100644 libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/src/main/kotlin/helloWorld.kt diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/modules/CoreJrtFileSystem.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/modules/CoreJrtFileSystem.kt index 0a04d9dea21..dbbc4c0a146 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/modules/CoreJrtFileSystem.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/modules/CoreJrtFileSystem.kt @@ -34,20 +34,17 @@ class CoreJrtFileSystem : DeprecatedVirtualFileSystem() { val jdkHome = File(jdkHomePath) val rootUri = URI.create(StandardFileSystems.JRT_PROTOCOL + ":/") val jrtFsJar = loadJrtFsJar(jdkHome) ?: return@createMap null - val fileSystem = - if (isAtLeastJava9()) { - FileSystems.newFileSystem(rootUri, mapOf("java.home" to jdkHome.absolutePath)) - } else { - /* - This ClassLoader actually lives as long as current thread due to ThreadLocal leak in jrtfs, - See https://bugs.openjdk.java.net/browse/JDK-8260621 - So that cache allows us to avoid creating too many classloaders for same JDK and reduce severity of that leak - */ - val classLoader = jrtFsClassLoaderCache.computeIfAbsent(jrtFsJar) { - URLClassLoader(arrayOf(jrtFsJar.toURI().toURL()), null) - } - FileSystems.newFileSystem(rootUri, emptyMap(), classLoader) - } + + /* + This ClassLoader actually lives as long as current thread due to ThreadLocal leak in jrt-fs, + See https://bugs.openjdk.java.net/browse/JDK-8260621 + So that cache allows us to avoid creating too many classloaders for same JDK and reduce severity of that leak + */ + val classLoader = globalJrtFsClassLoaderCache.computeIfAbsent(jrtFsJar) { + URLClassLoader(arrayOf(jrtFsJar.toURI().toURL()), null) + } + + val fileSystem = FileSystems.newFileSystem(rootUri, emptyMap(), classLoader) CoreJrtVirtualFile(this, jdkHomePath, fileSystem.getPath(""), parent = null) } @@ -87,6 +84,6 @@ class CoreJrtFileSystem : DeprecatedVirtualFileSystem() { return Pair(localPath, pathInJar) } - private val jrtFsClassLoaderCache = ContainerUtil.createConcurrentWeakValueMap() + private val globalJrtFsClassLoaderCache = ContainerUtil.createConcurrentWeakValueMap() } } diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinDaemonIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinDaemonIT.kt index ffe8c400ba0..3e1f0c8e673 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinDaemonIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/KotlinDaemonIT.kt @@ -136,6 +136,24 @@ class KotlinDaemonIT : KGPDaemonsBaseTest() { } } + @DisplayName("KT-56789: Kotlin daemon does not triggers OOM in Metaspace on multiple invocations") + @JdkVersions(versions = [JavaVersion.VERSION_11]) + @GradleWithJdkTest + @GradleTestVersions(minVersion = TestVersions.Gradle.MAX_SUPPORTED) + fun testMultipleCompilations(gradleVersion: GradleVersion, jdk: JdkVersions.ProvidedJdk) { + project( + "daemonJvmResourceLimits", + gradleVersion, + buildJdk = jdk.location + ) { + for (iteration in 0..300) { + build("clean", "assemble") { + assertKotlinDaemonReusesOnlyOneSession() + } + } + } + } + private fun BuildResult.assertGradleClasspathNotLeaked() { assertOutputContains("Kotlin compiler classpath:") diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/build.gradle.kts new file mode 100644 index 00000000000..57cffa3e2cf --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + kotlin("jvm") +} + +repositories { + mavenLocal() + mavenCentral() +} + +dependencies {} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/gradle.properties b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/gradle.properties new file mode 100644 index 00000000000..d9357460e61 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/gradle.properties @@ -0,0 +1 @@ +kotlin.daemon.jvmargs=-XX:MaxMetaspaceSize=128m -Xmx256m \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/src/main/kotlin/helloWorld.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/src/main/kotlin/helloWorld.kt new file mode 100644 index 00000000000..099b13aa0d4 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/resources/testProject/daemonJvmResourceLimits/src/main/kotlin/helloWorld.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +fun main() { + System.out.println("Hello, world!") + println("Hello, world!") +} \ No newline at end of file