8000 SI-9632 don't keep JarFile open in ZipArchive (with -Dscala.classpath.closeZip=true) by fommil · Pull Request #5592 · scala/scala · GitHub
[go: up one dir, main page]

Skip to content

SI-9632 don't keep JarFile open in ZipArchive (with -Dscala.classpath.closeZip=true) #5592

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from fommil:jar-open-close
Mar 22, 2017
Merged

Conversation

fommil
Copy link
Contributor
@fommil fommil commented Dec 11, 2016

This is a proposed fix for https://issues.scala-lang.org/browse/SI-9632

  • avoids a memory leak on all platforms (both OS level file handlers and JVM-level permgen/metaspace)
  • allows windows to delete jars that have been used in previous compiles

It is absolutely essential that this does not introduce a performance regression, so I'd like to hear how we can be sure about that.

Timings of the community build are one way to check, but that's expensive. So I wrote https://github.com/fommil/sbt-scala-monkey as a way to try this out for any sbt project (this file should work for 2.11). I've not noticed any performance slowdown on my projects (my work project is very big).

While working on this I noticed a few things that are worth mentioning here:

  1. this doesn't fix the larger problem https://issues.scala-lang.org/browse/SI-9682
  2. scalac assumes that jars are immutable, so if a project uses sbt's exportJars then a fresh scalaInstance is needed for each compile, see my workaround in https://github.com/fommil/sbt-big-project/issues/45
  3. if jars do mutate between scanning and access, the behaviour after this patch does subtely change... the time/size will (still) incorrectly report stale values, but this patch will load the new entries from the jar instead of the old ones.
  4. is it worth investigating aggressively loading the input stream into an Array[Byte] and returning it, to be sure that the jar file is closed? I could use BasicIO to readFully, but I never trusted that it is performant enough.

PS: I go one further than this patch when working on a very large windows codebase at work, where instead of opening the JarFile to read entries, I use the URL classloader and I apply https://github.com/fommil/class-monkey, which will cache entries and (other than timestamp checks) results in zero IO accesses for the second compile. I'm happy to raise a PR with that approach, to get an idea of the impact it has (when not using class-monkey), see https://gist.github.com/fommil/5709abc1678b346b6d927a13cd30b844#file-ziparchive-scala-L140-L223

@scala-jenkins scala-jenkins added this to the 2.12.2 milestone Dec 11, 2016
@retronym
Copy link
Member
retronym commented Dec 11, 2016

The JVM internally caches the underlying (native) datastructures for simultaneously open ZipFile instances over the same underlying file.

Under this patch, we wouldn't expect to benefit from this in the single-compiler case, however, we would benefit if multiple compilers happen to be reading some entry from the same JAR at the same time.

Interestingly, the JVM sometimes returns the cached internal datastructure in the sequential case, but I can't reconcile this with my understanding of the implementation.

scala> def getjzfile(zf: ZipFile) = { val f = classOf[ZipFile].getDeclaredField("jzfile"); f.setAccessible(true); f.get(zf).asInstanceOf[Long]}
getjzfile: (zf: java.util.zip.ZipFile)Long

scala> (1 to 10).foreach(_ => {{val zf = new ZipFile("/Users/jz/scala/2.11.7/lib/scala-library.jar"); println(getjzfile(zf)); Thread.sleep(100); zf.close() }})
140483009641856
140483009641856
140483009641856
140483008671488
140483008671488
140483007871120
140483008671488
140483010616320
140483007871120
140483010616320

scala> (1 to 10).foreach(_ => {{val zf = new ZipFile("/Users/jz/scala/2.11.7/lib/scala-library.jar"); println(getjzfile(zf)); zf.close() }})
140483009490784
140483009490784
140483009490784
140483009490784
140483009490784
140482984155232
140482984155232
140483005337856
140482984155232
140482984155232

Any JVM / JNI / C gurus reading that could explain that puzzle, based on ZIP_Put_In_Cache / ZIP_Get_From_Cache in zip_util.c?

Without this caching, the JNI implementation of ZipFile.open needs to read the Zip listing and allocate and initialize the datastructures to represent them. It may be that the cost of this is acceptable when compared with the subsequent work we do to deflate an entry and parse the class file into a Symbol. But we should measure the overhead itself in a microbenchmark (read all bytes of all entries in a JAR in a single ZipFile session vs reading them with an open/close in between.)

We could also add a -X option to control the behaviour, which would give people an escape hatch if there are unforseen problems in the new approach. We can provide assistance, or even an implementation, here if you're unfamiliar with that part of the compiler

val name = zipEntry.getName()
val time = zipEntry.getTime()
val size = zipEntry.getSize().toInt
class FileEntry() extends Entry(name) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest moving this class out to an enclosing scope where it can see openZipFile but not zipEntry and zipFile. (Basically, to make the capture explicit as constructor arguments.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup I can do that. In fact I do it that way with the other approach.

There's an argument to interning the names here... it is a considerable proportion of the heap in my builds.

Copy link
Member
@retronym retronym Dec 11, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might profit from -XX:+UseStringDeduplication (JEP-192), which lazily deduplicates the backing Array[Char] of long lived Strings as part of GC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use that, it don't seem to do very much. I've been a bit disappointed by it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, g1 seems down my work compile significantly. Default flags win.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

slows*

new FilterInputStream(delegate) {
override def close(): Unit = {
delegate.close()
zipFile.close()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

zipFile.close will automatically close delegate, but it is harmless to do it explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what do you think about using a bytearray to just read and close here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone happened to be using this code to read zip entries and stream the contents into a file, or some other processing, that would be counter-productive. For the known user of this code in the compiler, it would be slightly counter productive, as the caller would just copy that Array immediately. We could of course change the caller (AbstractFileReader.<init>'s call to AbstractFile.toByteArray), but in the interests of keeping this change small I'd avoid starting down that path.

@fommil
Copy link
Contributor Author
fommil commented Dec 11, 2016

is the build failure relevant?

error: error while loading Comparable, class file '/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/rt.jar(java/lang/Comparable.class)' is broken
(class java.lang.NullPointerException/entry)
error: scala.MatchError: <error> (of class scala.reflect.internal.Types$ErrorType$)
	at scala.reflect.internal.Definitions$DefinitionsClass.fixupAsAnyTrait(Definitions.scala:261)
	at scala.reflect.internal.Definitions$DefinitionsClass.$anonfun$ComparableClass$1(Definitions.scala:375)
	at scala.reflect.internal.Symbols$Symbol.modifyInfo(Symbols.scala:1540)
	at scala.reflect.internal.Definitions$DefinitionsClass.ComparableClass$lzycompute(Definitions.scala:375)
	at scala.reflect.internal.Definitions$DefinitionsClass.ComparableClass(Definitions.scala:375)
	at scala.reflect.internal.Definitions$DefinitionsClass.hijackedCoreClasses$lzycompute(Definitions.scala:1387)
	at scala.reflect.internal.Definitions$DefinitionsClass.hijackedCoreClasses(Definitions.scala:1386)
	at scala.reflect.internal.Definitions$DefinitionsClass.symbolsNotPresentInBytecode$lzycompute(Definitions.scala:1395)
	at scala.reflect.internal.Definitions$DefinitionsClass.symbolsNotPresentInBytecode(Definitions.scala:1395)
	at scala.reflect.internal.Definitions$DefinitionsClass.init(Definitions.scala:1449)
	at scala.tools.nsc.Global$Run.<init>(Global.scala:1140)
	at scala.tools.nsc.MainClass.doCompile(Main.scala:24)
	at scala.tools.nsc.Driver.process(Driver.scala:55)
	at scala.tools.nsc.Main.process(Main.scala)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at sbt.compiler.RawCompiler.apply(RawCompiler.scala:33)
	at sbt.compiler.AnalyzingCompiler$$anonfun$compileSources$1$$anonfun$apply$2.apply(AnalyzingCompiler.scala:159)
	at sbt.compiler.AnalyzingCompiler$$anonfun$compileSources$1$$anonfun$apply$2.apply(AnalyzingCompiler.scala:155)
	at sbt.IO$.withTemporaryDirectory(IO.scala:344)
	at sbt.compiler.AnalyzingCompiler$$anonfun$compileSources$1.apply(AnalyzingCompiler.scala:155)
	at sbt.compiler.AnalyzingCompiler$$anonfun$compileSources$1.apply(AnalyzingCompiler.scala:152)
	at sbt.IO$.withTemporaryDirectory(IO.scala:344)
	at sbt.compiler.AnalyzingCompiler$.compileSources(AnalyzingCompiler.scala:152)

@retronym
Copy link
Member

That seems to be caused by this change.

@retronym
Copy link
Member
diff --git a/src/reflect/scala/reflect/io/ZipArchive.scala b/src/reflect/scala/reflect/io/ZipArchive.scala
index f58780580a..bd21e149eb 100644
--- a/src/reflect/scala/reflect/io/ZipArchive.scala
+++ b/src/reflect/scala/reflect/io/ZipArchive.scala
@@ -140,14 +140,14 @@ final class FileZipArchive(file: JFile) extends ZipArchive(file) {
         if (zipEntry.isDirectory) dir
         else {
           // allows zipEntry to be garbage collected
-          val name = zipEntry.getName()
-          val time = zipEntry.getTime()
-          val size = zipEntry.getSize().toInt
-          class FileEntry() extends Entry(name) {
-            override def lastModified: Long = time // could be stale
+          val entryName = zipEntry.getName()
+          val entryTime = zipEntry.getTime()
+          val entrySize = zipEntry.getSize().toInt
+          class FileEntry() extends Entry(entryName) {
+            override def lastModified: Long = entryTime // could be stale
             override def input: InputStream = {
               val zipFile  = openZipFile()
-              val entry    = zipFile.getEntry(name)
+              val entry    = zipFile.getEntry(entryName)
               val delegate = zipFile.getInputStream(entry)
               new FilterInputStream(delegate) {
                 override def close(): Unit = {
@@ -156,7 +156,7 @@ final class FileZipArchive(file: JFile) extends ZipArchive(file) {
                 }
               }
             }
-            override def sizeOption: Option[Int] = Some(size) // could be stale
+            override def sizeOption: Option[Int] = Some(entrySize) // could be stale
           }
           val f = new FileEntry()
           dir.entries(f.name) = f

@fommil
Copy link
Contributor Author
fommil commented Dec 12, 2016

Oh I bet this is about visibility of variables in early initialisers or something. I've seen subtle changes result in nulls before. The proposed class refactor should fix that. There are loads of puzzler topics in this part of the code...

@retronym
Copy link
Member
retronym commented Dec 12, 2016 via email

@fommil
Copy link
Contributor Author
fommil commented Dec 12, 2016

ah, that could be the cause all along. Another reason might be because I removed the getArchive impl... thinking nobody used it (the super says it only exists to oblige the sbt api)

@fommil
Copy link
Contributor Author
fommil commented Dec 12, 2016

ok, updated, let's see if this fixes it.

@retronym
Copy link
Member

Any JVM / JNI / C gurus reading that could explain that puzzle, based on ZIP_Put_In_Cache / ZIP_Get_From_Cache in zip_util.c?

I was interpreting the output as meaning that native ZIP handling code was getting a cache hit for a brief period after the java ZipFile was closed. But a more plausible theory is that we're getting a cache miss, and a new entry is allocated in the spot that was just freed, so we see the same pointer.

@fommil
Copy link
Contributor Author
fommil commented Dec 20, 2016

Oh, I didn't realise the test was run again. I'll investigate shortly.

My suspicion is that something is using the method that i removed, which leaks jarfiles.

@retronym
Copy link
Member

The test failure isn't your fault this time. Our binary compatbility tool, MiMa, briefly had a bug that reported a false positive for changes like yours.

Rebasing on #5608 (which updates the version of MiMa used in the 2.12.x branch), and shuffling the code around (https://github.com/scala/scala/compare/2.12.x...retronym:review/5592?expand=1), should give a green build. In my branch, I've guarded the new code with a system property. I think this it makes sense to get some real world reports about how this works before turning it on by default.

@fommil
Copy link
Contributor Author
fommil commented Dec 20, 2016

Is there a way to get a community build timing? I think that's a good metric. I don't think my work project is representative of the "real world" in this sense.

@retronym
Copy link
Member

Most of the time is spent resolving dependencies and executing tests. We don't have a way to extract performance metrics of just the compilation.

Sorry, something went wrong.

@retronym
Copy link
Member
retronym commented Dec 20, 2016

Here's the impact on a microbenchmark:

[info]
[info] Benchmark                                                               Mode  Cnt  Score   Error  Units
[info] ZipFileBenchmark.testOpenShutEachEntry                                sample  228  0.141 ± 0.010   s/op
[info] ZipFileBenchmark.testOpenShutEachEntry:testOpenShutEachEntry·p0.00    sample       0.125           s/op
[info] ZipFileBenchmark.testOpenShutEachEntry:testOpenShutEachEntry·p0.50    sample       0.128           s/op
[info] ZipFileBenchmark.testOpenShutEachEntry:testOpenShutEachEntry·p0.90    sample       0.147           s/op
[info] ZipFileBenchmark.testOpenShutEachEntry:testOpenShutEachEntry·p0.95    sample       0.221           s/op
[info] ZipFileBenchmark.testOpenShutEachEntry:testOpenShutEachEntry·p0.99    sample       0.446           s/op
[info] ZipFileBenchmark.testOpenShutEachEntry:testOpenShutEachEntry·p0.999   sample       0.480           s/op
[info] ZipFileBenchmark.testOpenShutEachEntry:testOpenShutEachEntry·p0.9999  sample       0.480           s/op
[info] ZipFileBenchmark.testOpenShutEachEntry:testOpenShutEachEntry·p1.00    sample       0.480           s/op
[info] ZipFileBenchmark.testSingleOpenShut                                   sample  296  0.108 ± 0.003   s/op
[info] ZipFileBenchmark.testSingleOpenShut:testSingleOpenShut·p0.00          sample       0.101           s/op
[info] ZipFileBenchmark.testSingleOpenShut:testSingleOpenShut·p0.50          sample       0.103           s/op
[info] ZipFileBenchmark.testSingleOpenShut:testSingleOpenShut·p0.90          sample       0.108           s/op
[info] ZipFileBenchmark.testSingleOpenShut:testSingleOpenShut·p0.95          sample       0.160           s/op
[info] ZipFileBenchmark.testSingleOpenShut:testSingleOpenShut·p0.99          sample       0.178           s/op
[info] ZipFileBenchmark.testSingleOpenShut:testSingleOpenShut·p0.999         sample       0.195           s/op
[info] ZipFileBenchmark.testSingleOpenShut:testSingleOpenShut·p0.9999        sample       0.195           s/op
[info] ZipFileBenchmark.testSingleOpenShut:testSingleOpenShut·p1.00          sample       0.195           s/op

This suggests a 30% slowdown as penalty for reading the JAR metadata for each entry, rather than just once. I just threw the benchmark together, so the usual caveats apply.

While the time spent reading JARs might be a small fraction of the compiler performance, I'm still uncomfortable taking this hit without giving the new code some time to be battle tested.

@fommil
Copy link
Contributor Author
fommil commented Dec 20, 2016

I suspect this will disproportionately impact smaller files where more time is spent in I/O, but on bigger projects with lots of type inference it's unlikely to have an impact. Feature flagging it is a wise choice.

@fommil
Copy link
Contributor Author
fommil commented Dec 26, 2016

updated, I implemented the property switch... but I have no idea how to put this behind a -Y flag, because we don't have access to the compiler options.

@fommil
Copy link
Contributor Author
fommil commented Dec 26, 2016

mmm, I don't like the name EagerEntry... that implies it loads the contents straight away. I'll change that to LeakyEntry

@fommil
Copy link
Contributor Author
fommil commented Jan 18, 2017

I'm struggling to see where the failure is in the jenkins log, could somebody please help?

@SethTisue
Copy link
Member

I'm struggling to see where the failure is in the jenkins log,

Found 3 binary incompatibilities (32 were filtered out)
=======================================================
 * class scala.reflect.io.FileZipArchive#LazyEntry does not have a
   correspondent in current version
 * method closeZipFile()Boolean in object scala.reflect.io.ZipArchive does
   not have a correspondent in current version
 * class scala.reflect.io.FileZipArchive#LeakyEntry does not have a
   correspondent in current version

@fommil
Copy link
Contributor Author
fommil commented Jan 18, 2017

ah, so failing forwards compatibility. I guess if I make these private it will be ok?

@SethTisue
Copy link
Member
SethTisue commented Jan 18, 2017

they're already private enough as far as Scala is concerned (see lightbend-labs/mima#34 and lightbend-labs/mima#53), so it would be fine to whitelist them

@fommil
Copy link
Contributor Author
fommil commented Jan 18, 2017

is there a doc on how to do that?

@dwijnand
Copy link
Member

Should be add these to bincompat-forward.whitelist.conf (found at the root of the repo)

    filter {
        problems=[
            {
                matchName="scala.reflect.io.FileZipArchive$LazyEntry"
                problemName=MissingClassProblem
            },
            {
                matchName="scala.reflect.io.ZipArchive.closeZipFile"
                problemName=DirectMissingMethodProblem
            },
            {
                matchName="scala.reflect.io.FileZipArchive$LeakyEntry"
                problemName=MissingClassProblem
            }
        ]
    }

@fommil
Copy link
Contributor Author
fommil commented Jan 19, 2017

Awesome, thanks!

@fommil
Copy link
Contributor Author
fommil commented Jan 20, 2017

done

@fommil
Copy link
Contributor Author
fommil commented Jan 21, 2017

green light, green light! 😺

@SethTisue if this is merged, could you please run a version of the community build with and without this flag enabled? There is an environment variable that java accepts for global flags... I'm trying my best to remember it.

@fommil
Copy link
Contributor Author
fommil commented Feb 11, 2017

darn, conflicts I didn't notice. I'll try to clean this up.

@fommil
Copy link
Contributor Author
fommil commented Feb 11, 2017

rebased

@SethTisue
Copy link
Member
SethTisue commented Feb 11, 2017

/rebuild

(scala/scala-dev#296)

@fommil
Copy link
Contributor Author
fommil commented Feb 22, 2017

rebased

@dwijnand
Copy link
Member

is #5654 likely to be merged soon? until it is, a big chunk of the community build is disabled. after it's merged, we'll get better results from a test run here

It's merged as of 8 days ago, FYI.

@SethTisue
Copy link
Member
SethTisue commented Feb 24, 2017

now that the regular 2.12 community build is finally green again, here's a test run against this PR: https://scala-ci.typesafe.com/job/scala-2.12.x-integrate-community-build/1284/consoleFull see below

@fommil
Copy link
Contributor Author
fommil commented Feb 24, 2017

Thanks Seth! Does this have the flag enabled?

@SethTisue
Copy link
Member

Is there a way to get a community build timing? I think that's a good metric.

I just sort of robotically triggered a run for you because you asked, but now that I read through this PR in more detail, I see that you actually just want the runs to see how long they take, not just to see whether they pass.

I'm afraid that won't work.

first, the variability in the run timings is too large, because the Jenkins workers might or might not be doing something else. if your thing gives a 10x slowdown, we'll find out, but we won't get finer-grained information than that.

you could run the community build locally and reduce the variability that way, but it still wouldn't give good information for a more fundamental reason, which is that the community build runtime isn't actually dominated by running the compiler. it's a chunk of the overall time, sure, but resolving dependencies is also a huge chunk, and compiling build definitions (with Scala 2.10, not the compiler being tested) is a chunk, and running tests is a huge chunk. we don't know the relative sizes of these chunks and we don't currently have a way of measuring that.

basically, the community build isn't a compiler benchmark and I don't think there's any way to use it as one.

it so happens that Jason and Lukas have been developing an actual compiler-benchmarking system lately; I'll leave it to Jason to comment whether that might be applicable here.

@fommil
Copy link
Contributor Author
fommil commented Mar 1, 2017

It's also good to see if this triggers any regressions with the flag enabled. Is there anything I need to do?

@lrytz
Copy link
Member
lrytz commented Mar 1, 2017

@fommil I started a test run with -Dscala.classpath.closeZip=true at https://scala-ci.typesafe.com/job/scala-2.12.x-validate-test/4377/parameters, but I think this doesn't pass the system property to forked jvms (used for running junit and partest tests). We'd also need to add set javaOptions in ThisBuild += "-Dscala.classpath.closeZip=true", not sure how to do that through jenkins.

A simple way to test it would be to push a commit to this PR which enables it by default, which triggers tests. That could also be used for a community build. That commit can be removed afterwards.

@lrytz
Copy link
Member
lrytz commented Mar 1, 2017

For running the compiler benchmarks, we're still setting things up. We don't have a way yet to run it on PRs, but that's coming soon.

@fommil
Copy link
Contributor Author
fommil commented Mar 1, 2017

@lrytz @SethTisue time for a bit of magic 😉 ... if you use _JAVA_OPTIONS (with the underscore) it will be used by all java processes. Very useful thing to have in CI, e.g. for setting a global tmp dir or javaagent.

@dwijnand
Copy link
Member
dwijnand commented Mar 3, 2017

Now that is interesting magic. More info.

@fommil
Copy link
Contributor Author
fommil commented Mar 12, 2017

Anything I need to do?

@SethTisue
1241 Copy link
Member

Anything I need to do?

I’m only speculating, but perhaps the comments here got so extensive, with so many tangents (e.g. into community build stuff), that for Jason to review all that and swap the current state back in is daunting, especially given how many other PRs there are in our queue. perhaps you could try to add a comment here summarizing the current status as you see it?

@fommil
Copy link
Contributor Author
fommil commented Mar 21, 2017

I think this is good to go and I'm not aware of any more review comments to address. I'd like to see a community build with the envvar I referenced above, to test correctness of this new option, if that's possible.

@retronym
Copy link
Member

Yep, let's get this one in. The new feature is not on by default, and the refactorings around the existing code path appear safe.

@retronym retronym merged commit b68cc6c into scala:2.12.x Mar 22, 2017
@SethTisue SethTisue added the release-notes worth highlighting in next release notes label Mar 22, 2017
@fommil fommil deleted the jar-open-close branch March 22, 2017 07:02
@fommil
Copy link
Contributor Author
fommil commented Mar 22, 2017

Thanks! I can delete our ensime hack now :-)

if (zipEntry.isDirectory) dir
else {
class FileEntry() extends Entry(zipEntry.getName) {
override def getArchive = zipFile
Copy link
Member
@dwijnand dwijnand Mar 22, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both new impls don't override this, leaving it default to null. Is that ok? The comment says:

// have to keep this name for compat with sbt's compiler-interface
def getArchive: ZipFile = null

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup, intentional. It isn't used and is leaky even if it was.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's not been used since 2011 / sbt 0.11.0:

sbt/sbt@ff95799#diff-97684450c7452ac0dbc1eace52decd97L154

👍

@SethTisue SethTisue changed the title SI-9632 don't keep JarFile open in ZipArchive SI-9632 don't keep JarFile open in ZipArchive (with -Dscala.classpath.closeZip=true) Apr 14, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
release-notes worth highlighting in next release notes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants
0