From f4a2bcd0ce59d71af65fc7478476f9645ac598a0 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Tue, 30 Apr 2019 12:16:33 -0700 Subject: [PATCH 1/2] Close URLClassLoaders after getting bridge The ZincComponentCompilerSpec was fail with a metaspace related error when run locally with a recent version of sbt on my machine. I was able to stop these failures by closing the URLClassLoader instances used to compile the bridge. --- .../sbt/internal/inc/IvyBridgeProviderSpecification.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zinc-lm-integration/src/test/scala/sbt/internal/inc/IvyBridgeProviderSpecification.scala b/zinc-lm-integration/src/test/scala/sbt/internal/inc/IvyBridgeProviderSpecification.scala index 4d145a517..14e3a0640 100644 --- a/zinc-lm-integration/src/test/scala/sbt/internal/inc/IvyBridgeProviderSpecification.scala +++ b/zinc-lm-integration/src/test/scala/sbt/internal/inc/IvyBridgeProviderSpecification.scala @@ -8,6 +8,7 @@ package sbt.internal.inc import java.io.File +import java.net.URLClassLoader import sbt.io.IO import sbt.io.syntax._ @@ -59,6 +60,8 @@ abstract class IvyBridgeProviderSpecification extends FlatSpec with Matchers { val provider = getZincProvider(bridge1, targetDir, log) val scalaInstance = provider.fetchScalaInstance(scalaVersion, log) val bridge = provider.fetchCompiledBridge(scalaInstance, log) + scalaInstance.loader.asInstanceOf[URLClassLoader].close() + scalaInstance.loaderLibraryOnly.asInstanceOf[URLClassLoader].close() val target = targetDir / s"target-bridge-$scalaVersion.jar" IO.copyFile(bridge, target) target From 3a6ff8afca722493647ebcdce07a841c076cd2c9 Mon Sep 17 00:00:00 2001 From: Ethan Atkins Date: Tue, 30 Apr 2019 12:01:48 -0700 Subject: [PATCH 2/2] Use global classloader cache for scala instance I noticed in a heap dump of sbt that there were many classloaders for the scala instance. I then realized that we were making a new classloader for the scala library on every test run. Even worse, the ScalaInstanceLoader instance was never closed which lead to a metaspace leak. I moved the scala instance classloader to the global classloader cache. Not only will these be correctly cached, they will be closed if evicted from the cache. --- .../main/scala/sbt/internal/ClassLoaders.scala | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/main/src/main/scala/sbt/internal/ClassLoaders.scala b/main/src/main/scala/sbt/internal/ClassLoaders.scala index 6366343c8..ecd4fc50a 100644 --- a/main/src/main/scala/sbt/internal/ClassLoaders.scala +++ b/main/src/main/scala/sbt/internal/ClassLoaders.scala @@ -40,6 +40,7 @@ private[sbt] object ClassLoaders { rawRuntimeDependencies = dependencyJars(Runtime / dependencyClasspath).value.filterNot(exclude), allDependencies = dependencyJars(dependencyClasspath).value.filterNot(exclude), + globalCache = (Scope.GlobalScope / classLoaderCache).value, runtimeCache = (Runtime / classLoaderCache).value, testCache = (Test / classLoaderCache).value, resources = ClasspathUtilities.createClasspathResources(fullCP, si), @@ -73,6 +74,7 @@ private[sbt] object ClassLoaders { ) s.log.warn(s"$showJavaOptions will be ignored, $showFork is set to false") } + val globalCache = (Scope.GlobalScope / classLoaderCache).value val runtimeCache = (Runtime / classLoaderCache).value val testCache = (Test / classLoaderCache).value val exclude = dependencyJars(exportedProducts).value.toSet ++ instance.allJars @@ -86,6 +88,7 @@ private[sbt] object ClassLoaders { fullCP = classpath, rawRuntimeDependencies = runtimeDeps, allDependencies = allDeps, + globalCache = globalCache, runtimeCache = runtimeCache, testCache = testCache, resources = ClasspathUtilities.createClasspathResources(classpath, instance), @@ -114,6 +117,7 @@ private[sbt] object ClassLoaders { fullCP: Seq[File], rawRuntimeDependencies: Seq[File], allDependencies: Seq[File], + globalCache: ClassLoaderCache, runtimeCache: ClassLoaderCache, testCache: ClassLoaderCache, resources: Map[String, String], @@ -143,7 +147,8 @@ private[sbt] object ClassLoaders { val allTestDependencies = if (layerTestDependencies) allDependenciesSet else Set.empty[File] val allRuntimeDependencies = (if (layerDependencies) rawRuntimeDependencies else Nil).toSet - val scalaInstanceLayer = new ScalaInstanceLoader(si) + val scalaInstanceLayer = + globalCache.get((si.allJars.toSeq, interfaceLoader, resources, tmp)) // layer 2 val runtimeDependencySet = allDependenciesSet intersect allRuntimeDependencies val runtimeDependencies = rawRuntimeDependencies.filter(runtimeDependencySet) @@ -187,17 +192,6 @@ private[sbt] object ClassLoaders { if (snapshots.isEmpty) jarLoader else cache.get((snapshots, jarLoader, resources, tmp)) } - private class ScalaInstanceLoader(val instance: ScalaInstance) - extends URLClassLoader(instance.allJars.map(_.toURI.toURL), interfaceLoader) { - override def equals(o: Any): Boolean = o match { - case that: ScalaInstanceLoader => this.instance.allJars.sameElements(that.instance.allJars) - case _ => false - } - override def hashCode: Int = instance.hashCode - override lazy val toString: String = - s"ScalaInstanceLoader($interfaceLoader, jars = {${instance.allJars.mkString(", ")}})" - } - // helper methods private def flatLoader(classpath: Seq[File], parent: ClassLoader): ClassLoader = new URLClassLoader(classpath.map(_.toURI.toURL).toArray, parent) {