Reduce the amount of garbage produced by JarFile

Previously, working with a JarFile created a large amount of garbage
that was allocated on the thread local allocation buffer (TLAB).
The TLAB allocations made a significant contribution to GC pressure
and slowed down startup. This commit reduces the amount of garbage
by making a number of changes.

Reading from a RandomAccessDataFile has been reworked to avoid
creating new RandomAccessFile instances. A single RandomAccessFile
is now created for an entire jar file and it is used to read data from
anywhere in that jar file, including entries in nested jar files. To
ensure that reads remain thread-safe, a lock is taken on the
RandomAccessFile that is shared by all RandomAccessDataFile instances
that are provided access to (portions of) the same jar file.

Reading all of the bytes from a RandomAccessData has been reworked to
avoid the use of an InputStream that was created, used to read the
data, and then thrown away. In place of the InputStream-based
mechanism a method has been introduced that returns all of the
RandomAccessData as a byte[]. Building on this change, a method has
also been introduced to read a portion of a RandomAccessData as a
byte[]. This avoids the need to create a new RandomAccessData
subsection where the subsection was only used to read its entire
contents and then thrown away.

Decoding of an MS-DOS datetime has been reworked to use LocalDataTime
rather than GregorianCalendar. The former produces less garbage than
the latter.

Closes gh-12226
This commit is contained in:
Andy Wilkinson
2018-02-23 21:31:54 +00:00
parent 569bad16da
commit 60ac2e5c09
16 changed files with 116 additions and 423 deletions

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2012-2017 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
*
* http://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.data;
import java.io.InputStream;
import org.junit.Test;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ByteArrayRandomAccessData}.
*
* @author Phillip Webb
*/
public class ByteArrayRandomAccessDataTests {
@Test
public void testGetInputStream() throws Exception {
byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 };
RandomAccessData data = new ByteArrayRandomAccessData(bytes);
InputStream inputStream = data.getInputStream(ResourceAccess.PER_READ);
assertThat(FileCopyUtils.copyToByteArray(inputStream)).isEqualTo(bytes);
assertThat(data.getSize()).isEqualTo(bytes.length);
}
@Test
public void testGetSubsection() throws Exception {
byte[] bytes = new byte[] { 0, 1, 2, 3, 4, 5 };
RandomAccessData data = new ByteArrayRandomAccessData(bytes);
data = data.getSubsection(1, 4).getSubsection(1, 2);
InputStream inputStream = data.getInputStream(ResourceAccess.PER_READ);
assertThat(FileCopyUtils.copyToByteArray(inputStream))
.isEqualTo(new byte[] { 2, 3 });
assertThat(data.getSize()).isEqualTo(2L);
}
}

View File

@@ -18,14 +18,10 @@ package org.springframework.boot.loader.data;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@@ -36,23 +32,14 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.junit.rules.TemporaryFolder;
import org.mockito.Mockito;
import org.springframework.boot.loader.data.RandomAccessData.ResourceAccess;
import org.springframework.boot.loader.data.RandomAccessDataFile.FilePool;
import org.springframework.test.util.ReflectionTestUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.spy;
/**
* Tests for {@link RandomAccessDataFile}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class RandomAccessDataFileTests {
@@ -84,7 +71,7 @@ public class RandomAccessDataFileTests {
outputStream.write(BYTES);
outputStream.close();
this.file = new RandomAccessDataFile(this.tempFile);
this.inputStream = this.file.getInputStream(ResourceAccess.PER_READ);
this.inputStream = this.file.getInputStream();
}
@After
@@ -109,22 +96,6 @@ public class RandomAccessDataFileTests {
new RandomAccessDataFile(file);
}
@Test
public void fileNotNullWithConcurrentReads() {
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage("File must not be null");
new RandomAccessDataFile(null, 1);
}
@Test
public void fileExistsWithConcurrentReads() {
File file = new File("/does/not/exist");
this.thrown.expect(IllegalArgumentException.class);
this.thrown.expectMessage(
String.format("File %s must exist", file.getAbsolutePath()));
new RandomAccessDataFile(file, 1);
}
@Test
public void inputStreamRead() throws Exception {
for (int i = 0; i <= 255; i++) {
@@ -224,8 +195,7 @@ public class RandomAccessDataFileTests {
@Test
public void subsectionZeroLength() throws Exception {
RandomAccessData subsection = this.file.getSubsection(0, 0);
assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read())
.isEqualTo(-1);
assertThat(subsection.getInputStream().read()).isEqualTo(-1);
}
@Test
@@ -245,14 +215,13 @@ public class RandomAccessDataFileTests {
@Test
public void subsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 1);
assertThat(subsection.getInputStream(ResourceAccess.PER_READ).read())
.isEqualTo(1);
assertThat(subsection.getInputStream().read()).isEqualTo(1);
}
@Test
public void inputStreamReadPastSubsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
InputStream inputStream = subsection.getInputStream();
assertThat(inputStream.read()).isEqualTo(1);
assertThat(inputStream.read()).isEqualTo(2);
assertThat(inputStream.read()).isEqualTo(-1);
@@ -261,7 +230,7 @@ public class RandomAccessDataFileTests {
@Test
public void inputStreamReadBytesPastSubsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
InputStream inputStream = subsection.getInputStream();
byte[] b = new byte[3];
int amountRead = inputStream.read(b);
assertThat(b).isEqualTo(new byte[] { 1, 2, 0 });
@@ -271,7 +240,7 @@ public class RandomAccessDataFileTests {
@Test
public void inputStreamSkipPastSubsection() throws Exception {
RandomAccessData subsection = this.file.getSubsection(1, 2);
InputStream inputStream = subsection.getInputStream(ResourceAccess.PER_READ);
InputStream inputStream = subsection.getInputStream();
assertThat(inputStream.skip(3)).isEqualTo(2L);
assertThat(inputStream.read()).isEqualTo(-1);
}
@@ -293,7 +262,7 @@ public class RandomAccessDataFileTests {
for (int i = 0; i < 100; i++) {
results.add(executorService.submit(() -> {
InputStream subsectionInputStream = RandomAccessDataFileTests.this.file
.getSubsection(0, 256).getInputStream(ResourceAccess.PER_READ);
.getSubsection(0, 256).getInputStream();
byte[] b = new byte[256];
subsectionInputStream.read(b);
return Arrays.equals(b, BYTES);
@@ -304,45 +273,4 @@ public class RandomAccessDataFileTests {
}
}
@Test
public void close() throws Exception {
this.file.getInputStream(ResourceAccess.PER_READ).read();
this.file.close();
Field filePoolField = RandomAccessDataFile.class.getDeclaredField("filePool");
filePoolField.setAccessible(true);
Object filePool = filePoolField.get(this.file);
Field filesField = filePool.getClass().getDeclaredField("files");
filesField.setAccessible(true);
Queue<?> queue = (Queue<?>) filesField.get(filePool);
assertThat(queue).isEmpty();
}
@Test
public void seekFailuresDoNotPreventSubsequentReads() throws Exception {
FilePool filePool = (FilePool) ReflectionTestUtils.getField(this.file,
"filePool");
FilePool spiedPool = spy(filePool);
ReflectionTestUtils.setField(this.file, "filePool", spiedPool);
willAnswer((invocation) -> {
RandomAccessFile originalFile = (RandomAccessFile) invocation
.callRealMethod();
if (Mockito.mockingDetails(originalFile).isSpy()) {
return originalFile;
}
RandomAccessFile spiedFile = spy(originalFile);
willThrow(new IOException("Seek failed")).given(spiedFile).seek(anyLong());
return spiedFile;
}).given(spiedPool).acquire();
for (int i = 0; i < 5; i++) {
try {
this.file.getInputStream(ResourceAccess.PER_READ).read();
fail("Read should fail due to exception from seek");
}
catch (IOException ex) {
}
}
}
}

View File

@@ -186,7 +186,7 @@ public class JarFileTests {
@Test
public void close() throws Exception {
RandomAccessDataFile randomAccessDataFile = spy(
new RandomAccessDataFile(this.rootJarFile, 1));
new RandomAccessDataFile(this.rootJarFile));
JarFile jarFile = new JarFile(randomAccessDataFile);
jarFile.close();
verify(randomAccessDataFile).close();