Make LayeredClassLoaders parallel capable

The docs for ClassLoader,
https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html
say that all non-hierarchical custom classloaders should be registered
as parallel capable. The docs also suggest that custom classloaders
should try to only override findClass so I reworked LayerdClassLoader to
only override findClass. I also added locking to the class loading to
make it safe for concurrent loading.

All of the custom classloaders besides LayeredClassLoader either
subclass URLClassLoader or LayeredClassLoader but don't override
loadClass. Because those two classloaders are parallel capable, the
subclasses should be as well. It isn't possible to make classloaders
that are implemented in scala parallel capable because scala 2 doesn't
support jvm static blocks (dotty does support this with an annotation).
To work around this, I re-worked some of the classloaders so that they
are either directly implemented in java or I subclassed a scala
implementation class in java.
This commit is contained in:
Ethan Atkins 2019-06-03 11:58:48 -07:00
parent cc8c66c66d
commit 625470cdd5
9 changed files with 298 additions and 147 deletions

View File

@ -0,0 +1,34 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.classpath;
import java.net.URL;
import java.net.URLClassLoader;
public class WrappedLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
WrappedLoader(final ClassLoader parent) {
super(new URL[] {}, parent);
}
@Override
public URL[] getURLs() {
final ClassLoader parent = getParent();
return (parent instanceof URLClassLoader)
? ((URLClassLoader) parent).getURLs()
: super.getURLs();
}
@Override
public String toString() {
return "WrappedClassLoader(" + getParent() + ")";
}
}

View File

@ -10,7 +10,7 @@ package sbt.internal.classpath
import java.io.File
import java.lang.management.ManagementFactory
import java.lang.ref.{ Reference, ReferenceQueue, SoftReference }
import java.net.{ URL, URLClassLoader }
import java.net.URLClassLoader
import java.util.concurrent.atomic.AtomicInteger
import sbt.internal.inc.classpath.{
@ -142,14 +142,6 @@ private[sbt] class ClassLoaderCache(
private[this] val cleanupThread = new CleanupThread(ClassLoaderCache.threadID.getAndIncrement())
private[this] val lock = new Object
private class WrappedLoader(parent: ClassLoader) extends URLClassLoader(Array.empty, parent) {
// This is to make dotty work which extracts the URLs from the loader
override def getURLs: Array[URL] = parent match {
case u: URLClassLoader => u.getURLs
case _ => Array.empty
}
override def toString: String = s"WrappedLoader($parent)"
}
private def close(classLoader: ClassLoader): Unit = classLoader match {
case a: AutoCloseable => a.close()
case _ =>

View File

@ -0,0 +1,32 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal;
import java.net.URL;
import java.net.URLClassLoader;
class FlatLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
FlatLoader(final URL[] urls, final ClassLoader parent) {
super(urls, parent);
}
@Override
public String toString() {
final StringBuilder jars = new StringBuilder();
for (final URL u : getURLs()) {
jars.append(" ");
jars.append(u);
jars.append("\n");
}
return "FlatLoader(\n parent = " + getParent() + "\n jars = " + jars.toString() + ")";
}
}

View File

@ -0,0 +1,26 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal;
import java.io.File;
import scala.collection.immutable.Map;
import scala.collection.Seq;
class LayeredClassLoader extends LayeredClassLoaderImpl {
LayeredClassLoader(
final Seq<File> classpath,
final ClassLoader parent,
final Map<String, String> resources,
final File tempDir) {
super(classpath, parent, resources, tempDir);
}
static {
ClassLoader.registerAsParallelCapable();
}
}

View File

@ -0,0 +1,23 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal;
import java.io.File;
import scala.collection.immutable.Map;
import scala.collection.Seq;
class ResourceLoader extends ResourceLoaderImpl {
ResourceLoader(
final Seq<File> classpath, final ClassLoader parent, final Map<String, String> resources) {
super(classpath, parent, resources);
}
static {
ClassLoader.registerAsParallelCapable();
}
}

View File

@ -0,0 +1,29 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal;
import java.net.URL;
import java.net.URLClassLoader;
final class ScalaReflectClassLoader extends URLClassLoader {
static {
ClassLoader.registerAsParallelCapable();
}
private final URL jar;
ScalaReflectClassLoader(final URL jar, final ClassLoader parent) {
super(new URL[] {jar}, parent);
this.jar = jar;
}
@Override
public String toString() {
return "ScalaReflectClassLoader(" + jar + " parent = " + getParent() + ")";
}
}

View File

@ -9,7 +9,6 @@ package sbt
package internal
import java.io.File
import java.net.URLClassLoader
import java.nio.file.Path
import sbt.ClassLoaderLayeringStrategy._
@ -139,17 +138,12 @@ private[sbt] object ClassLoaders {
case f if f.getName == "scala-reflect.jar" =>
si.allJars.find(_.getName == "scala-reflect.jar")
}.flatten
class ScalaReflectClassLoader(jar: File)
extends URLClassLoader(Array(jar.toURI.toURL), scalaLibraryLayer) {
override def toString: String =
s"ScalaReflectClassLoader($jar, parent = $scalaLibraryLayer)"
}
val scalaReflectLayer = scalaReflectJar
.map { file =>
cache.apply(
file -> IO.getModifiedTimeOrZero(file) :: Nil,
scalaLibraryLayer,
() => new ScalaReflectClassLoader(file)
() => new ScalaReflectClassLoader(file.toURI.toURL, scalaLibraryLayer)
)
}
.getOrElse(scalaLibraryLayer)
@ -201,19 +195,6 @@ private[sbt] object ClassLoaders {
} else parent
}
private class ResourceLoader(
classpath: Seq[File],
parent: ClassLoader,
resources: Map[String, String]
) extends LayeredClassLoader(classpath, parent, resources, new File("/dev/null")) {
override def findClass(name: String): Class[_] = throw new ClassNotFoundException(name)
override def loadClass(name: String, resolve: Boolean): Class[_] = {
val clazz = parent.loadClass(name)
if (resolve) resolveClass(clazz)
clazz
}
override def toString: String = "ResourceLoader"
}
// Creates a one or two layered classloader for the provided classpaths depending on whether
// or not the classpath contains any snapshots. If it does, the snapshots are placed in a layer
// above the regular jar layer. This allows the snapshot layer to be invalidated without
@ -235,14 +216,9 @@ private[sbt] object ClassLoaders {
} else parent
}
private[this] class FlatLoader(classpath: Seq[File], parent: ClassLoader)
extends URLClassLoader(classpath.map(_.toURI.toURL).toArray, parent) {
override def toString: String =
s"FlatClassLoader(parent = $interfaceLoader, jars =\n${classpath.mkString("\n")}\n)"
}
// helper methods
private def flatLoader(classpath: Seq[File], parent: ClassLoader): ClassLoader =
new FlatLoader(classpath, parent)
new FlatLoader(classpath.map(_.toURI.toURL).toArray, parent)
private[this] def modifiedTimes(stamps: Seq[(Path, FileStamp)]): Seq[(File, Long)] = stamps.map {
case (p, LastModified(lm)) => p.toFile -> lm
case (p, _) =>

View File

@ -1,112 +0,0 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
import java.io.File
import java.net.URLClassLoader
import java.util.concurrent.ConcurrentHashMap
import sbt.internal.inc.classpath._
import sbt.io.IO
import scala.collection.JavaConverters._
import scala.collection.mutable.ListBuffer
private[sbt] class LayeredClassLoader(
classpath: Seq[File],
parent: ClassLoader,
override protected val resources: Map[String, String],
tempDir: File,
) extends URLClassLoader(classpath.toArray.map(_.toURI.toURL), parent)
with RawResources
with NativeCopyLoader
with AutoCloseable {
private[this] val nativeLibs = new java.util.HashSet[File]().asScala
override protected val config = new NativeCopyConfig(
tempDir,
classpath,
IO.parseClasspath(System.getProperty("java.library.path", ""))
)
override def findLibrary(name: String): String = {
super.findLibrary(name) match {
case null => null
case l =>
nativeLibs += new File(l)
l
}
}
private[this] val loaded = new ConcurrentHashMap[String, Class[_]]
/*
* Override findClass to memoize its result. We need to do this because in loadClass we will
* delegate to findClass if the current LayeredClassLoader cannot load a class but it is a
* descendant of the thread's context class loader and a class loader below it in the layering
* hierarchy is able to load the required class. Unlike loadClass, findClass does not cache
* the result which would make it possible to return multiple versions of the same class.
*/
override def findClass(name: String): Class[_] = loaded.get(name) match {
case null =>
val res = super.findClass(name)
loaded.putIfAbsent(name, res) match {
case null => res
case clazz => clazz
}
case c => c
}
override def loadClass(name: String, resolve: Boolean): Class[_] = {
try super.loadClass(name, resolve)
catch {
case e: ClassNotFoundException =>
val loaders = new ListBuffer[LayeredClassLoader]
var currentLoader: ClassLoader = Thread.currentThread.getContextClassLoader
do {
currentLoader match {
case cl: LayeredClassLoader if cl != this => loaders.prepend(cl)
case _ =>
}
currentLoader = currentLoader.getParent
} while (currentLoader != null && currentLoader != this)
if (currentLoader == this) {
val resourceName = name.replace('.', '/').concat(".class")
loaders
.collectFirst {
case l if l.findResource(resourceName) != null =>
val res = l.findClass(name)
if (resolve) l.resolveClass(res)
res
}
.getOrElse(throw e)
} else throw e
}
}
override def close(): Unit = nativeLibs.foreach(NativeLibs.delete)
override def toString: String = s"""LayeredClassLoader(
| classpath =
| ${classpath mkString "\n "}
| parent =
| ${parent.toString.linesIterator.mkString("\n ")}
|)""".stripMargin
}
private[internal] object NativeLibs {
private[this] val nativeLibs = new java.util.HashSet[File].asScala
ShutdownHooks.add(() => {
nativeLibs.foreach(IO.delete)
IO.deleteIfEmpty(nativeLibs.map(_.getParentFile).toSet)
nativeLibs.clear()
})
def addNativeLib(lib: String): Unit = {
nativeLibs.add(new File(lib))
()
}
def delete(file: File): Unit = {
nativeLibs.remove(file)
file.delete()
()
}
}

View File

@ -0,0 +1,151 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal
import java.io.File
import java.net.URLClassLoader
import java.util.concurrent.ConcurrentHashMap
import sbt.internal.inc.classpath._
import sbt.io.IO
import scala.collection.JavaConverters._
import scala.collection.mutable.ListBuffer
private[sbt] class LayeredClassLoaderImpl(
classpath: Seq[File],
parent: ClassLoader,
override protected val resources: Map[String, String],
tempDir: File,
) extends URLClassLoader(classpath.toArray.map(_.toURI.toURL), parent)
with RawResources
with NativeCopyLoader
with AutoCloseable {
private[this] val nativeLibs = new java.util.HashSet[File]().asScala
override protected val config = new NativeCopyConfig(
tempDir,
classpath,
IO.parseClasspath(System.getProperty("java.library.path", ""))
)
override def findLibrary(name: String): String = {
super.findLibrary(name) match {
case null => null
case l =>
nativeLibs += new File(l)
l
}
}
private[this] val loaded = new ConcurrentHashMap[String, Class[_]]
private[this] val classLocks = new ConcurrentHashMap[String, AnyRef]()
/*
* Override findClass to both memoize its result and look down the class hierarchy to attempt to
* load a missing class from a descendant loader. If we didn't cache the loaded classes,
* then it would be possible for this class loader to load a different version of the class than
* the descendant, which would likely cause a crash. The search for the class in the descendants
* allows java reflection to work in cases where the class to load via reflection is not directly
* visible to the class that is attempting to load it.
*/
override def findClass(name: String): Class[_] = loaded.get(name) match {
case null =>
val newLock = new AnyRef
val lock = classLocks.putIfAbsent(name, newLock) match {
case null => newLock
case l => l
}
lock.synchronized {
try {
val clazz = super.findClass(name)
loaded.putIfAbsent(name, clazz) match {
case null => clazz
case c => c
}
} catch {
case e: ClassNotFoundException =>
/*
* If new threads are spawned, they inherit the context class loader of the parent
* This means that if a test or run task spawns background threads to do work, then the
* same context class loader is available on all of the background threads. In the test
* and run tasks, we temporarily set the context class loader of the main sbt thread to
* be the classloader generated by ClassLoaders.getLayers. This creates an environment
* that looks somewhat like a forked jvm with the app classloader set to be the
* generated class loader. If the test or run main changes the thread context class
* loader, this search might fail even if it would have passed on the initial entry
* into the method. Applications generally only modify the context classloader if they
* are manually loading classes. It's likely that if an application generated
* ClassLoader needs access to the classes in the sbt classpath, then it would be using
* the original context class loader as the parent of the new context class loader
* anyway.
*
* If we wanted to make this change permanent so that the user could not
* override the global context classloader, we would possibly need to intercept the
* classloading of java.lang.Thread itself to return a custom Thread class that mirrors
* the java.lang.Thread api, but stores the context class loader in a custom field.
*
*/
var currentLoader: ClassLoader = Thread.currentThread.getContextClassLoader
val loaders = new ListBuffer[LayeredClassLoader]
do {
currentLoader match {
case cl: LayeredClassLoader if cl != this => loaders.prepend(cl)
case _ =>
}
currentLoader = currentLoader.getParent
} while (currentLoader != null && currentLoader != this)
if (currentLoader == this && loaders.nonEmpty) {
val resourceName = name.replace('.', '/').concat(".class")
loaders
.collectFirst {
case l if l.findResource(resourceName) != null => l.findClass(name)
}
.getOrElse(throw e)
} else throw e
}
}
case c => c
}
override def close(): Unit = nativeLibs.foreach(NativeLibs.delete)
override def toString: String = s"""LayeredClassLoader(
| classpath =
| ${classpath mkString "\n "}
| parent =
| ${parent.toString.linesIterator.mkString("\n ")}
|)""".stripMargin
}
private[internal] class ResourceLoaderImpl(
classpath: Seq[File],
parent: ClassLoader,
resources: Map[String, String]
) extends LayeredClassLoader(classpath, parent, resources, new File("/dev/null")) {
override def findClass(name: String): Class[_] = throw new ClassNotFoundException(name)
override def loadClass(name: String, resolve: Boolean): Class[_] = {
val clazz = parent.loadClass(name)
if (resolve) resolveClass(clazz)
clazz
}
override def toString: String = "ResourceLoader"
}
private[internal] object NativeLibs {
private[this] val nativeLibs = new java.util.HashSet[File].asScala
ShutdownHooks.add(() => {
nativeLibs.foreach(IO.delete)
IO.deleteIfEmpty(nativeLibs.map(_.getParentFile).toSet)
nativeLibs.clear()
})
def addNativeLib(lib: String): Unit = {
nativeLibs.add(new File(lib))
()
}
def delete(file: File): Unit = {
nativeLibs.remove(file)
file.delete()
()
}
}