1. 进程内类加载

进程内类加载 

默认情况下,sbt 在它自己的 JVM 实例中执行 runtest 任务。它通过在隔离的 ClassLoader 中调用任务来模拟运行外部 java 命令。与分叉相比,这种方法减少了启动延迟和总运行时间。仅仅重用 JVM 所带来的性能优势并不大。应用程序依赖项的类加载和链接占用了许多应用程序启动时间的很大一部分。sbt 通过在运行之间重用一些已加载的类来减少这种启动延迟。它通过按照 java ClassLoader 的标准委托模型创建分层 ClassLoader 来实现。最外层始终包含特定于项目的类文件和 jar 文件,并在运行之间被丢弃。但是,内部层可以被重用。

从 sbt 1.3.0 开始,可以配置 sbt 用于生成分层 ClassLoader 实例的特定方法。它是通过 classLoaderLayeringStrategy 指定的。有三个可能的值

  1. ScalaLibrary - 最外层的父层能够加载 scala 标准库以及 scala 反射库,前提是它在应用程序类路径上。这是默认策略。它与 sbt 版本 < 1.3.0 提供的分层 ClassLoaders 最相似。
  2. AllLibraryJars - 在 scala 库层和最外层之间添加一个额外的层,用于所有依赖 jar 文件。当启用 turbo 模式时,这是默认策略。与 ScalaLibrary 相比,这种策略可以显着提高启动和总运行时间性能。如果任何库具有可变全局状态,则结果可能不一致,因为与 ScalaLibrary 不同,全局状态在运行之间保持不变。当任何库使用 java 序列化时,应避免使用 AllLibraryJars
  3. Flat - 不使用分层。由任务的 fullClasspath 键指定的所有类路径都在最外层加载。如果在使用 ScalaLibrary 时遇到任何问题,或者应用程序要求所有类在同一个 ClassLoader 中加载,则可以考虑将其用作分叉的替代方案,对于某些 java 序列化的使用情况可能就是这种情况。

classLoaderLayeringStrategy 可以设置在不同的配置中。例如,要在 Test 配置中使用 AllLibraryJars 策略,请添加

Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars

build.sbt 文件中。假设 build.sbt 文件没有其他更改,run 任务将继续使用 ScalaLibrary 策略。

故障排除 

当与分层类加载器一起使用时,Java 反射可能会导致问题,因为通过反射加载另一个类的类方法可能无法访问要加载的类。如果使用 Class.forNameThread.currentThread.getContextClassLoader.loadClass 加载类,则这种情况尤其可能发生。请考虑以下示例

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

如果使用 sbt 默认的 ScalaLibrary 策略以 sbt run 运行 ReflectionExample,它将失败并出现 ClassNotFoundException,因为支持 future 的线程的上下文类加载器是 scala 库类加载器,它无法加载项目类。为了解决此限制而不将分层策略更改为 Flat,可以执行以下操作

  1. 使用 Class.forName 而不是 ClassLoader.loadClass。jvm 隐式使用调用类的加载器来加载使用 Class.forName 的类。在这种情况下,ReflectionExample 是调用类,并且它将与 Foo 在同一个类加载器中,因为它们都是项目类路径的一部分。
  2. 提供用于加载的类加载器。在上面的示例中,可以通过用 val cl = getClass.getClassLoader 替换 val cl = Thread.currentThread.getContextClassLoader 来做到这一点。

对于情况 (2),如果名称查找是由库执行的,则可以向库方法添加 ClassLoader 参数,该方法执行查找。例如,

object Library {
  def lookup(name: String): Class[_] =
    Thread.currentThread.getContextClassLoader.loadClass(name)
}

可以重写为

object Library {
  def lookup(name: String): Class[_] =
    lookup(name, Thread.currentThread.getContextClassLoader)
  def lookup(name: String, loader: ClassLoader): Class[_] =
    loader.loadClass(name)
}