By default, sbt executes the run and test tasks within its own JVM instance.
It emulates running an external java command by invoking the task in an isolated
ClassLoader. Compared to forking, this approach reduces the start
start up latency and total runtime. The performance benefit from simply reusing
the JVM is modest. Class loading and linking of the application dependencies
dominate the start up time of many applications. sbt reduces this start up
latency by re-using some of the loaded classes between runs. It does this by
creating a layered ClassLoader following the standard delegation model of a java
ClassLoader.
The outermost layer, which always contains the class files and jars specific to
the project, is discarded between runs. The inner layers, however, can be
reused.
Starting with sbt 1.3.0, it is possible to configure the particular approach
that sbt takes to generate layered ClassLoader instances. It is specified via
the classLoaderLayeringStrategy. There are three possible values:
ScalaLibrary - The parent of the outermost layer is able to load the
scala standard library as well as the scala reflect library provided it is on
the application classpath. This is the default strategy. It is most similar to
the layered ClassLoaders provided by sbt versions < 1.3.0.
AllLibraryJars - Adds an additional layer for all of the dependency jars
between the scala library layer and the outermost layer. It is the default
strategy when turbo mode is enabled. This strategy can significantly improve the
startup and total runtime performance compared to ScalaLibrary. Results may be
inconsistent if any of the libraries have mutable global state because, unlike
ScalaLibrary, the global state persists between runs. When any libraries use
java serialization, AllLibraryJars should be avoided.
fullClasspath key of the task is loaded in the outermost layer. Consider using
as an alternative to fork if any issues are experienced with ScalaLibrary or
if the application requires all classes to be loaded in the same ClassLoader,
which may be the case for some uses of java serialization.
The classLoaderLayeringStrategy can be set in different configurations. For
example, to use the AllLibraryJars strategy in the Test configuration, add
Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars
to the build.sbt file. Assuming no other changes to the build.sbt file, The
run task will still use ScalaLibrary strategy.
Java reflection may cause issues when used with layered classloaders because it
is possible that the class method that loads another class via reflection may
not have access to that class to be loaded. This is particularly likely if the
class is loaded using Class.forName or
Thread.currentThread.getContextClassLoader.loadClass. Consider the following
example:
package example
import scala.concurrent.{ Await, Future }
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
object ReflectionExample {
def main(args: Array[String]): Unit = Await.result(Future {
val cl = Thread.currentThread.getContextClassLoader
println(cl.loadClass("example.Foo"))
}, Duration.Inf)
}
class Foo
If one runs ReflectionExample with sbt run using the sbt default ScalaLibrary
strategy, it will fail with a ClassNotFoundException because the context
classloader of the thread that backs the future is the scala library classloader
which is not able to load project classes. To work around this limitation
without changing the layering strategy to Flat, one can do the following:
Class.forName instead of ClassLoader.loadClass. The jvm implicitly
uses the loader of the calling class for loading classes using Class.forName.
In this case, ReflectionExample is the calling class and it will be in the
same classloader as Foo since they are both part of the project classpath.
val cl = Thread.currentThread.getContextClassLoader with val cl =
getClass.getClassLoader.
For case (2), if the name lookup is performed by a library, then a
ClassLoader parameter could be added to the library method that does the
lookup. For example,
object Library {
def lookup(name: String): Class[_] =
Thread.currentThread.getContextClassLoader.loadClass(name)
}
could be rewritten to
object Library {
def lookup(name: String): Class[_] =
lookup(name, Thread.currentThread.getContextClassLoader)
def lookup(name: String, loader: ClassLoader): Class[_] =
loader.loadClass(name)
}