Skip to content

Commit c1e6f9e

Browse files
committed
Avoid lock contention in classpath
1 parent 45e5c24 commit c1e6f9e

File tree

1 file changed

+48
-21
lines changed

1 file changed

+48
-21
lines changed

src/reflect/scala/reflect/io/ZipArchive.scala

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,13 @@ package io
1717
import java.net.URL
1818
import java.io.{ByteArrayInputStream, FilterInputStream, IOException, InputStream}
1919
import java.io.{File => JFile}
20+
import java.util.concurrent.{ArrayBlockingQueue, TimeUnit}
2021
import java.util.zip.{ZipEntry, ZipFile, ZipInputStream}
2122
import java.util.jar.Manifest
22-
2323
import scala.collection.mutable
2424
import scala.collection.JavaConverters._
2525
import scala.annotation.tailrec
2626
import scala.reflect.internal.JDK9Reflectors
27-
2827
import ZipArchive._
2928

3029
/** An abstraction for zip files and streams. Everything is written the way
@@ -146,6 +145,31 @@ abstract class ZipArchive(override val file: JFile, release: Option[String]) ext
146145
/** ''Note: This library is considered experimental and should not be used unless you know what you are doing.'' */
147146
final class FileZipArchive(file: JFile, release: Option[String]) extends ZipArchive(file, release) {
148147
def this(file: JFile) = this(file, None)
148+
private object zipFilePool {
149+
private[this] val zipFiles = new ArrayBlockingQueue[ZipFile](Runtime.getRuntime.availableProcessors())
150+
151+
def acquire: ZipFile = {
152+
val zf = zipFiles.poll(0, TimeUnit.MILLISECONDS)
153+
zf match {
154+
case null =>
155+
openZipFile()
156+
case _ =>
157+
zf
158+
}
159+
}
160+
161+
def release(zf: ZipFile): Unit = {
162+
if (!zipFiles.offer(zf, 0, TimeUnit.MILLISECONDS))
163+
zf.close()
164+
}
165+
166+
def close(): Unit = {
167+
val zipFilesToClose = new java.util.ArrayList[ZipFile]
168+
zipFiles.drainTo(zipFilesToClose)
169+
zipFilesToClose.iterator().forEachRemaining(_.close())
170+
}
171+
}
172+
149173
private[this] def openZipFile(): ZipFile = try {
150174
release match {
151175
case Some(r) if file.getName.endsWith(".jar") =>
@@ -175,18 +199,24 @@ final class FileZipArchive(file: JFile, release: Option[String]) extends ZipArch
175199
override def sizeOption: Option[Int] = Some(size) // could be stale
176200
}
177201

178-
// keeps a file handle open to ZipFile, which forbids file mutation
179-
// on Windows, and leaks memory on all OS (typically by stopping
180-
// classloaders from being garbage collected). But is slightly
181-
// faster than LazyEntry.
202+
// keeps file handle(s) open to ZipFile in the pool this.zipFiles,
203+
// which forbids file mutation on Windows, and leaks memory on all OS (typically by stopping
204+
// classloaders from being garbage collected). But is slightly faster than LazyEntry.
182205
private[this] class LeakyEntry(
183-
zipFile: ZipFile,
184-
zipEntry: ZipEntry,
185-
name: String
206+
name: String,
207+
time: Long,
208+
size: Int
186209
) extends Entry(name) {
187-
override def lastModified: Long = zipEntry.getTime
188-
override def input: InputStream = zipFile.getInputStream(zipEntry)
189-
override def sizeOption: Option[Int] = Some(zipEntry.getSize.toInt)
210+
override def lastModified: Long = time // could be stale
211+
override def input: InputStream = {
212+
val zipFile = zipFilePool.acquire
213+
val entry = zipFile.getEntry(name) // with `-release`, returns the correct version under META-INF/versions
214+
val delegate = zipFile.getInputStream(entry)
215+
new FilterInputStream(delegate) {
216+
override def close(): Unit = { zipFilePool.release(zipFile) }
217+
}
218+
}
219+
override def sizeOption: Option[Int] = Some(size) // could be stale
190220
}
191221

192222
private[this] val dirs = new java.util.HashMap[String, DirEntry]()
@@ -200,10 +230,6 @@ final class FileZipArchive(file: JFile, release: Option[String]) extends ZipArch
200230
while (enum.hasMoreElements) {
201231
val zipEntry = enum.nextElement
202232
if (!zipEntry.getName.startsWith("META-INF/versions/")) {
203-
val zipEntryVersioned = if (release.isDefined) {
204-
// JARFile will return the entry for the corresponding release-dependent version here under META-INF/versions
205-
zipFile.getEntry(zipEntry.getName)
206-
} else zipEntry
207233
if (!zipEntry.isDirectory) {
208234
val dir = getDir(dirs, zipEntry)
209235
val f =
@@ -213,15 +239,17 @@ final class FileZipArchive(file: JFile, release: Option[String]) extends ZipArch
213239
zipEntry.getTime,
214240
zipEntry.getSize.toInt)
215241
else
216-
new LeakyEntry(zipFile, zipEntryVersioned, zipEntry.getName)
242+
new LeakyEntry(zipEntry.getName,
243+
zipEntry.getTime,
244+
zipEntry.getSize.toInt)
217245

218246
dir.entries(f.name) = f
219247
}
220248
}
221249
}
222250
} finally {
223-
if (ZipArchive.closeZipFile) zipFile.close()
224-
else closeables ::= zipFile
251+
if (!ZipArchive.closeZipFile)
252+
zipFilePool.release(zipFile)
225253
}
226254
root
227255
}
@@ -242,9 +270,8 @@ final class FileZipArchive(file: JFile, release: Option[String]) extends ZipArch
242270
case x: FileZipArchive => file.getAbsoluteFile == x.file.getAbsoluteFile
243271
case _ => false
244272
}
245-
private[this] var closeables: List[java.io.Closeable] = Nil
246273
override def close(): Unit = {
247-
closeables.foreach(_.close)
274+
zipFilePool.close()
248275
}
249276
}
250277
/** ''Note: This library is considered experimental and should not be used unless you know what you are doing.'' */

0 commit comments

Comments
 (0)