Attempt to fix memory leak in JarFile class
Create a new `JarFileWrapper` class so that we can wrap and existing `JarFile` and offer a version that can be safely closed. Prior to this commit, we provided wrapper functionality in the `JarFile` class itself. Unfortunately, because we override `close` and also create a lot of wrappers this caused memory issues when running on Java 11. With Java 11 `java.util.zip.ZipFile` class uses `FinalizableResource` for any implementation that overrides `close()`. This means that any wrapper classes will not be garbage collected until the JVM finalizer thread runs. Closes gh-22991
This commit is contained in:
@@ -171,11 +171,12 @@ class HandlerTests {
|
||||
TestJarCreator.createTestJar(testJar);
|
||||
URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler);
|
||||
JarURLConnection connection = (JarURLConnection) url.openConnection();
|
||||
JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile());
|
||||
try {
|
||||
assertThat(connection.getJarFile().getRootJarFile().getFile()).isEqualTo(testJar);
|
||||
assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar);
|
||||
}
|
||||
finally {
|
||||
connection.getJarFile().close();
|
||||
jarFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,11 +186,12 @@ class HandlerTests {
|
||||
TestJarCreator.createTestJar(testJar);
|
||||
URL url = new URL(null, "jar:" + testJar.toURI().toURL() + "!/nested.jar!/3.dat", this.handler);
|
||||
JarURLConnection connection = (JarURLConnection) url.openConnection();
|
||||
JarFile jarFile = JarFileWrapper.unwrap(connection.getJarFile());
|
||||
try {
|
||||
assertThat(connection.getJarFile().getRootJarFile().getFile()).isEqualTo(testJar);
|
||||
assertThat(jarFile.getRootJarFile().getFile()).isEqualTo(testJar);
|
||||
}
|
||||
finally {
|
||||
connection.getJarFile().close();
|
||||
jarFile.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -218,10 +218,10 @@ class JarFileTests {
|
||||
URL url = this.jarFile.getUrl();
|
||||
assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/");
|
||||
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
|
||||
assertThat(jarURLConnection.getJarFile().getParent()).isSameAs(this.jarFile);
|
||||
assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile);
|
||||
assertThat(jarURLConnection.getJarEntry()).isNull();
|
||||
assertThat(jarURLConnection.getContentLength()).isGreaterThan(1);
|
||||
assertThat(((JarFile) jarURLConnection.getContent()).getParent()).isSameAs(this.jarFile);
|
||||
assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) jarURLConnection.getContent())).isSameAs(this.jarFile);
|
||||
assertThat(jarURLConnection.getContentType()).isEqualTo("x-java/jar");
|
||||
assertThat(jarURLConnection.getJarFileURL().toURI()).isEqualTo(this.rootJarFile.toURI());
|
||||
}
|
||||
@@ -231,7 +231,7 @@ class JarFileTests {
|
||||
URL url = new URL(this.jarFile.getUrl(), "1.dat");
|
||||
assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/1.dat");
|
||||
JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
|
||||
assertThat(jarURLConnection.getJarFile().getParent()).isSameAs(this.jarFile);
|
||||
assertThat(JarFileWrapper.unwrap(jarURLConnection.getJarFile())).isSameAs(this.jarFile);
|
||||
assertThat(jarURLConnection.getJarEntry()).isSameAs(this.jarFile.getJarEntry("1.dat"));
|
||||
assertThat(jarURLConnection.getContentLength()).isEqualTo(1);
|
||||
assertThat(jarURLConnection.getContent()).isInstanceOf(InputStream.class);
|
||||
@@ -285,7 +285,7 @@ class JarFileTests {
|
||||
URL url = nestedJarFile.getUrl();
|
||||
assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar!/");
|
||||
JarURLConnection conn = (JarURLConnection) url.openConnection();
|
||||
assertThat(conn.getJarFile().getParent()).isSameAs(nestedJarFile);
|
||||
assertThat(JarFileWrapper.unwrap(conn.getJarFile())).isSameAs(nestedJarFile);
|
||||
assertThat(conn.getJarFileURL().toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/nested.jar");
|
||||
assertThat(conn.getInputStream()).isNotNull();
|
||||
JarInputStream jarInputStream = new JarInputStream(conn.getInputStream());
|
||||
@@ -314,7 +314,8 @@ class JarFileTests {
|
||||
|
||||
URL url = nestedJarFile.getUrl();
|
||||
assertThat(url.toString()).isEqualTo("jar:" + this.rootJarFile.toURI() + "!/d!/");
|
||||
assertThat(((JarURLConnection) url.openConnection()).getJarFile().getParent()).isSameAs(nestedJarFile);
|
||||
JarURLConnection connection = (JarURLConnection) url.openConnection();
|
||||
assertThat(JarFileWrapper.unwrap(connection.getJarFile())).isSameAs(nestedJarFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.loader.jar;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.jar.JarOutputStream;
|
||||
import java.util.zip.ZipEntry;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link JarFileWrapper}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class JarFileWrapperTests {
|
||||
|
||||
private JarFile parent;
|
||||
|
||||
private JarFileWrapper wrapper;
|
||||
|
||||
@BeforeEach
|
||||
void setup(@TempDir File temp) throws IOException {
|
||||
this.parent = spy(new JarFile(createTempJar(temp)));
|
||||
this.wrapper = new JarFileWrapper(this.parent);
|
||||
}
|
||||
|
||||
private File createTempJar(File temp) throws IOException {
|
||||
File file = new File(temp, "temp.jar");
|
||||
new JarOutputStream(new FileOutputStream(file)).close();
|
||||
return file;
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUrlDelegatesToParent() throws MalformedURLException {
|
||||
this.wrapper.getUrl();
|
||||
verify(this.parent).getUrl();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getTypeDelegatesToParent() {
|
||||
this.wrapper.getType();
|
||||
verify(this.parent).getType();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPermissionDelegatesToParent() {
|
||||
this.wrapper.getPermission();
|
||||
verify(this.parent).getPermission();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getManifestDelegatesToParent() throws IOException {
|
||||
this.wrapper.getManifest();
|
||||
verify(this.parent).getManifest();
|
||||
}
|
||||
|
||||
@Test
|
||||
void entriesDelegatesToParent() {
|
||||
this.wrapper.entries();
|
||||
verify(this.parent).entries();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getJarEntryDelegatesToParent() {
|
||||
this.wrapper.getJarEntry("test");
|
||||
verify(this.parent).getJarEntry("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEntryDelegatesToParent() {
|
||||
this.wrapper.getEntry("test");
|
||||
verify(this.parent).getEntry("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getInputStreamDelegatesToParent() throws IOException {
|
||||
this.wrapper.getInputStream();
|
||||
verify(this.parent).getInputStream();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getEntryInputStreamDelegatesToParent() throws IOException {
|
||||
ZipEntry entry = new ZipEntry("test");
|
||||
this.wrapper.getInputStream(entry);
|
||||
verify(this.parent).getInputStream(entry);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getCommentDelegatesToParent() {
|
||||
this.wrapper.getComment();
|
||||
verify(this.parent).getComment();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sizeDelegatesToParent() {
|
||||
this.wrapper.size();
|
||||
verify(this.parent).size();
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringDelegatesToParent() {
|
||||
assertThat(this.wrapper.toString()).endsWith("/temp.jar");
|
||||
}
|
||||
|
||||
@Test // gh-22991
|
||||
void wrapperMustNotImplementClose() {
|
||||
// If the wrapper overrides close then on Java 11 a FinalizableResource
|
||||
// instance will be used to perform cleanup. This can result in a lot
|
||||
// of additional memory being used since cleanup only occurs when the
|
||||
// finalizer thread runs. See gh-22991
|
||||
assertThatExceptionOfType(NoSuchMethodException.class)
|
||||
.isThrownBy(() -> JarFileWrapper.class.getDeclaredMethod("close"));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -62,14 +62,14 @@ class JarURLConnectionTests {
|
||||
void connectionToRootUsingAbsoluteUrl() throws Exception {
|
||||
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/");
|
||||
Object content = JarURLConnection.get(url, this.jarFile).getContent();
|
||||
assertThat(((JarFile) content).getParent()).isSameAs(this.jarFile);
|
||||
assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
void connectionToRootUsingRelativeUrl() throws Exception {
|
||||
URL url = new URL("jar:file:" + getRelativePath() + "!/");
|
||||
Object content = JarURLConnection.get(url, this.jarFile).getContent();
|
||||
assertThat(((JarFile) content).getParent()).isSameAs(this.jarFile);
|
||||
assertThat(JarFileWrapper.unwrap((java.util.jar.JarFile) content)).isSameAs(this.jarFile);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -226,7 +226,7 @@ class JarURLConnectionTests {
|
||||
void openConnectionCanBeClosedWithoutClosingSourceJar() throws Exception {
|
||||
URL url = new URL("jar:" + this.rootJarFile.toURI().toURL() + "!/");
|
||||
JarURLConnection connection = JarURLConnection.get(url, this.jarFile);
|
||||
JarFile connectionJarFile = connection.getJarFile();
|
||||
java.util.jar.JarFile connectionJarFile = connection.getJarFile();
|
||||
connectionJarFile.close();
|
||||
assertThat(this.jarFile.isClosed()).isFalse();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user