Reduce garbage created when loading fat jars
Refactor fat jar loader classes so that less `char[]` instances are created. This is primarily achieved by adding a new `StringSequence` class that can chop up Strings without needing to copy the underlying array. Since Java 8, calls to `String.subString(...)` always copy the underlying char array. For many of the operations that we need, this is unnecessary. Fixes gh-11405pull/11365/merge
parent
c024313141
commit
aa66d5dfb8
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* 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.jar;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A {@link CharSequence} backed by a single shared {@link String}. Unlike a regular
|
||||
* {@link String}, {@link #subSequence(int, int)} operations will not copy the underlying
|
||||
* character array.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class StringSequence implements CharSequence {
|
||||
|
||||
private final String source;
|
||||
|
||||
private final int start;
|
||||
|
||||
private final int end;
|
||||
|
||||
private int hash;
|
||||
|
||||
StringSequence(String source) {
|
||||
this(source, 0, (source == null ? -1 : source.length()));
|
||||
}
|
||||
|
||||
StringSequence(String source, int start, int end) {
|
||||
Objects.requireNonNull(source, "Source must not be null");
|
||||
if (start < 0) {
|
||||
throw new StringIndexOutOfBoundsException(start);
|
||||
}
|
||||
if (end > source.length()) {
|
||||
throw new StringIndexOutOfBoundsException(end);
|
||||
}
|
||||
this.source = source;
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public StringSequence subSequence(int start) {
|
||||
return subSequence(start, length());
|
||||
}
|
||||
|
||||
@Override
|
||||
public StringSequence subSequence(int start, int end) {
|
||||
int subSequenceStart = this.start + start;
|
||||
int subSequenceEnd = this.start + end;
|
||||
if (subSequenceStart > this.end) {
|
||||
throw new StringIndexOutOfBoundsException(start);
|
||||
}
|
||||
if (subSequenceEnd > this.end) {
|
||||
throw new StringIndexOutOfBoundsException(end);
|
||||
}
|
||||
return new StringSequence(this.source, subSequenceStart, subSequenceEnd);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return length() == 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int length() {
|
||||
return this.end - this.start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public char charAt(int index) {
|
||||
return this.source.charAt(this.start + index);
|
||||
}
|
||||
|
||||
public int indexOf(char ch) {
|
||||
return this.source.indexOf(ch, this.start) - this.start;
|
||||
}
|
||||
|
||||
public int indexOf(String str) {
|
||||
return this.source.indexOf(str, this.start) - this.start;
|
||||
}
|
||||
|
||||
public int indexOf(String str, int fromIndex) {
|
||||
return this.source.indexOf(str, this.start + fromIndex) - this.start;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.source.substring(this.start, this.end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int hash = this.hash;
|
||||
if (hash == 0 && length() > 0) {
|
||||
for (int i = this.start; i < this.end; i++) {
|
||||
hash = 31 * hash + this.source.charAt(i);
|
||||
}
|
||||
this.hash = hash;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
StringSequence other = (StringSequence) obj;
|
||||
int n = length();
|
||||
if (n == other.length()) {
|
||||
int i = 0;
|
||||
while (n-- != 0) {
|
||||
if (charAt(i) != other.charAt(i)) {
|
||||
return false;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
@ -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.jar;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.boot.loader.jar.JarURLConnection.JarEntryName;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link JarEntryName}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class JarEntryNameTests {
|
||||
|
||||
@Test
|
||||
public void basicName() {
|
||||
assertThat(new JarEntryName("a/b/C.class").toString()).isEqualTo("a/b/C.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nameWithSingleByteEncodedCharacters() {
|
||||
assertThat(new JarEntryName("%61/%62/%43.class").toString())
|
||||
.isEqualTo("a/b/C.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nameWithDoubleByteEncodedCharacters() {
|
||||
assertThat(new JarEntryName("%c3%a1/b/C.class").toString())
|
||||
.isEqualTo("\u00e1/b/C.class");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void nameWithMixtureOfEncodedAndUnencodedDoubleByteCharacters() {
|
||||
assertThat(new JarEntryName("%c3%a1/b/\u00c7.class").toString())
|
||||
.isEqualTo("\u00e1/b/\u00c7.class");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 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.jar;
|
||||
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link StringSequence}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class StringSequenceTests {
|
||||
|
||||
@Rule
|
||||
public ExpectedException thrown = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void createWhenSourceIsNullShouldThrowException() {
|
||||
this.thrown.expect(NullPointerException.class);
|
||||
this.thrown.expectMessage("Source must not be null");
|
||||
new StringSequence(null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWithIndexWhenSourceIsNullShouldThrowException() {
|
||||
this.thrown.expect(NullPointerException.class);
|
||||
this.thrown.expectMessage("Source must not be null");
|
||||
new StringSequence(null, 0, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenStartIsLessThanZeroShouldThrowException() {
|
||||
this.thrown.expect(StringIndexOutOfBoundsException.class);
|
||||
new StringSequence("x", -1, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createWhenEndIsGreaterThanLengthShouldThrowException() {
|
||||
this.thrown.expect(StringIndexOutOfBoundsException.class);
|
||||
new StringSequence("x", 0, 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void creatFromString() {
|
||||
assertThat(new StringSequence("test").toString()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSequenceWithJustStartShouldReturnSubSequence() {
|
||||
assertThat(new StringSequence("smiles").subSequence(1).toString())
|
||||
.isEqualTo("miles");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSequenceShouldReturnSubSequence() {
|
||||
assertThat(new StringSequence("hamburger").subSequence(4, 8).toString())
|
||||
.isEqualTo("urge");
|
||||
assertThat(new StringSequence("smiles").subSequence(1, 5).toString())
|
||||
.isEqualTo("mile");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSequenceWhenCalledMultipleTimesShouldReturnSubSequence() {
|
||||
assertThat(new StringSequence("hamburger").subSequence(4, 8).subSequence(1, 3)
|
||||
.toString()).isEqualTo("rg");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSequenceWhenEndPastExistingEndShouldThrowException() {
|
||||
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
|
||||
assertThat(sequence.toString()).isEqualTo("bcd");
|
||||
assertThat(sequence.subSequence(2, 3).toString()).isEqualTo("d");
|
||||
this.thrown.expect(IndexOutOfBoundsException.class);
|
||||
sequence.subSequence(3, 4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subSequenceWhenStartPastExistingEndShouldThrowException() {
|
||||
StringSequence sequence = new StringSequence("abcde").subSequence(1, 4);
|
||||
assertThat(sequence.toString()).isEqualTo("bcd");
|
||||
assertThat(sequence.subSequence(2, 3).toString()).isEqualTo("d");
|
||||
this.thrown.expect(IndexOutOfBoundsException.class);
|
||||
sequence.subSequence(4, 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEmptyWhenEmptyShouldReturnTrue() {
|
||||
assertThat(new StringSequence("").isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void isEmptyWhenNotEmptyShouldReturnFalse() {
|
||||
assertThat(new StringSequence("x").isEmpty()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void lengthShouldReturnLength() {
|
||||
StringSequence sequence = new StringSequence("hamburger");
|
||||
assertThat(sequence.length()).isEqualTo(9);
|
||||
assertThat(sequence.subSequence(4, 8).length()).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void charAtShouldReturnChar() {
|
||||
StringSequence sequence = new StringSequence("hamburger");
|
||||
assertThat(sequence.charAt(0)).isEqualTo('h');
|
||||
assertThat(sequence.charAt(1)).isEqualTo('a');
|
||||
assertThat(sequence.subSequence(4, 8).charAt(0)).isEqualTo('u');
|
||||
assertThat(sequence.subSequence(4, 8).charAt(1)).isEqualTo('r');
|
||||
}
|
||||
|
||||
@Test
|
||||
public void indexOfCharShouldReturnIndexOf() {
|
||||
StringSequence sequence = new StringSequence("aabbaacc");
|
||||
assertThat(sequence.indexOf('a')).isEqualTo(0);
|
||||
assertThat(sequence.indexOf('b')).isEqualTo(2);
|
||||
assertThat(sequence.subSequence(2).indexOf('a')).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void indexOfStringShouldReturnIndexOf() {
|
||||
StringSequence sequence = new StringSequence("aabbaacc");
|
||||
assertThat(sequence.indexOf("a")).isEqualTo(0);
|
||||
assertThat(sequence.indexOf("b")).isEqualTo(2);
|
||||
assertThat(sequence.subSequence(2).indexOf("a")).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void indexOfStringFromIndexShouldReturnIndexOf() {
|
||||
StringSequence sequence = new StringSequence("aabbaacc");
|
||||
assertThat(sequence.indexOf("a", 2)).isEqualTo(4);
|
||||
assertThat(sequence.indexOf("b", 3)).isEqualTo(3);
|
||||
assertThat(sequence.subSequence(2).indexOf("a", 3)).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hashCodeShouldBeSameAsString() {
|
||||
assertThat(new StringSequence("hamburger").hashCode())
|
||||
.isEqualTo("hamburger".hashCode());
|
||||
assertThat(new StringSequence("hamburger").subSequence(4, 8).hashCode())
|
||||
.isEqualTo("urge".hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void equalsWhenSameContentShouldMatch() {
|
||||
StringSequence a = new StringSequence("hamburger").subSequence(4, 8);
|
||||
StringSequence b = new StringSequence("urge");
|
||||
StringSequence c = new StringSequence("urgh");
|
||||
assertThat(a).isEqualTo(b).isNotEqualTo(c);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue