mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Reduce garbage of repeated WritableJson.toByteArray() calls
Update the default implementation of `WritableJson.toByteArray()` to reduce the amount of garbage created from repeated calls. Prior to this commit, each call would create a new `ByteArrayOutputStream` and `OutputStreamWriter` to create the byte array. Writing structured JSON results in many calls to the `toByteArray()` method, which means we repeatedly create and destroy the `ByteArrayOutputStream` and `OutputStreamWriter` objects. Furthermore, both contain buffers that are often expanded and will overlap with each other. The updated implementation uses a custom `Appendable` implementation that uses a single `ByteBuffer` buffer. It also has a `ThreadLocal` cache so that repeated calls from the same thread can reuse the buffer. The cache uses a `SoftReference` to ensure that the JVM can reclaim space if needed (for example, if a large JSON line was written). Closes gh-49428
This commit is contained in:
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.ref.SoftReference;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.CharsetEncoder;
|
||||
import java.nio.charset.CoderResult;
|
||||
import java.nio.charset.CodingErrorAction;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link Appendable} implementation that can be used to return a byte array. Designed to
|
||||
* reduce memory pressure for {@link WritableJson#toByteArray(Charset)} by using a single
|
||||
* cached buffer scoped to the thread.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AppendableByteArray implements Appendable {
|
||||
|
||||
private static ThreadLocal<SoftReference<AppendableByteArray>> cache = new ThreadLocal<>();
|
||||
|
||||
private static final int DEFAULT_INITIAL_SIZE = 8192;
|
||||
|
||||
private static final int DEFAULT_EXPANSION_SIZE = 8192;
|
||||
|
||||
private static final byte[] NO_BYTES = {};
|
||||
|
||||
private final Charset charset;
|
||||
|
||||
private final CharsetEncoder encoder;
|
||||
|
||||
private final int expansionSize;
|
||||
|
||||
private ByteBuffer out;
|
||||
|
||||
AppendableByteArray(Charset charset) {
|
||||
this(charset, DEFAULT_INITIAL_SIZE, DEFAULT_EXPANSION_SIZE);
|
||||
}
|
||||
|
||||
AppendableByteArray(Charset charset, int initialSize, int expansionSize) {
|
||||
this.charset = charset;
|
||||
this.encoder = charset.newEncoder()
|
||||
.onMalformedInput(CodingErrorAction.REPLACE)
|
||||
.onUnmappableCharacter(CodingErrorAction.REPLACE);
|
||||
this.out = ByteBuffer.allocate(initialSize);
|
||||
this.expansionSize = expansionSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppendableByteArray append(CharSequence charSequence, int start, int end) throws IOException {
|
||||
return append(((charSequence != null) ? charSequence : "null").subSequence(start, end));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppendableByteArray append(CharSequence charSequence) throws IOException {
|
||||
return append(String.valueOf(charSequence).toCharArray());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AppendableByteArray append(char ch) throws IOException {
|
||||
return append(new char[] { ch });
|
||||
}
|
||||
|
||||
private AppendableByteArray append(char[] chars) throws IOException {
|
||||
return (chars.length != 0) ? append(CharBuffer.wrap(chars)) : this;
|
||||
}
|
||||
|
||||
private AppendableByteArray append(CharBuffer in) throws IOException {
|
||||
CoderResult result = this.encoder.encode(in, this.out, false);
|
||||
if (result.isUnderflow()) {
|
||||
return this;
|
||||
}
|
||||
if (result.isOverflow()) {
|
||||
ByteBuffer out = this.out;
|
||||
this.out = ByteBuffer.allocate(out.capacity() + this.expansionSize);
|
||||
out.flip();
|
||||
this.out.put(out);
|
||||
return append(in);
|
||||
}
|
||||
result.throwException();
|
||||
return this;
|
||||
}
|
||||
|
||||
byte[] toByteArray() {
|
||||
this.out.flip();
|
||||
int limit = this.out.limit();
|
||||
int position = this.out.position();
|
||||
int size = limit - position;
|
||||
if (size <= 0) {
|
||||
return NO_BYTES;
|
||||
}
|
||||
byte[] result = new byte[size];
|
||||
System.arraycopy(this.out.array(), this.out.arrayOffset() + position, result, 0, size);
|
||||
reset();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void reset() {
|
||||
this.out.clear();
|
||||
this.encoder.reset();
|
||||
}
|
||||
|
||||
static AppendableByteArray get(Charset charset) {
|
||||
Assert.notNull(charset, "'charset' must not be null");
|
||||
SoftReference<AppendableByteArray> cached = cache.get();
|
||||
AppendableByteArray result = (cached != null) ? cached.get() : null;
|
||||
if (result == null || !result.charset.equals(charset)) {
|
||||
result = new AppendableByteArray(charset);
|
||||
cache.set(new SoftReference<>(result));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.boot.json;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
@@ -76,9 +75,10 @@ public interface WritableJson {
|
||||
*/
|
||||
default byte[] toByteArray(Charset charset) {
|
||||
Assert.notNull(charset, "'charset' must not be null");
|
||||
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
toWriter(new OutputStreamWriter(out, charset));
|
||||
return out.toByteArray();
|
||||
try {
|
||||
AppendableByteArray appendable = AppendableByteArray.get(charset);
|
||||
to(appendable);
|
||||
return appendable.toByteArray();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2012-present 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.json;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.function.ThrowingConsumer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link AppendableByteArray}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AppendableByteArrayTests {
|
||||
|
||||
private static String string = """
|
||||
This is a long(ish) string.
|
||||
At least it's longer that the initial size and the overflow size.
|
||||
We can write it out and test if the bytes match.
|
||||
""";
|
||||
|
||||
@Test
|
||||
void writesLargeStringWithExpandingBuffer() throws Exception {
|
||||
assertByteArray(StandardCharsets.UTF_8, (appendable) -> appendable.append(string));
|
||||
assertByteArray(StandardCharsets.UTF_16, (appendable) -> appendable.append(string));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writesLargeStringWithLargeBuffer() throws Exception {
|
||||
assertByteArray(string.length() * 10, 10, StandardCharsets.UTF_8, (appendable) -> appendable.append(string));
|
||||
assertByteArray(string.length() * 10, 10, StandardCharsets.UTF_16, (appendable) -> appendable.append(string));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writesMultipleSmallStrings() throws Exception {
|
||||
assertByteArray(StandardCharsets.UTF_8, (appendable) -> appendable.append("{").append("hello").append("}"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeUsingCache() throws IOException {
|
||||
assertByteArray(StandardCharsets.UTF_8, AppendableByteArray::get, (appendable) -> appendable.append(string));
|
||||
assertByteArray(StandardCharsets.UTF_8, AppendableByteArray::get, (appendable) -> appendable.append(string));
|
||||
assertByteArray(StandardCharsets.UTF_16, AppendableByteArray::get, (appendable) -> appendable.append(string));
|
||||
assertByteArray(StandardCharsets.UTF_16, AppendableByteArray::get, (appendable) -> appendable.append(string));
|
||||
assertByteArray(StandardCharsets.UTF_8, AppendableByteArray::get, (appendable) -> appendable.append(string));
|
||||
}
|
||||
|
||||
private void assertByteArray(Charset charset, ThrowingConsumer<Appendable> action) throws Exception {
|
||||
assertByteArray(4, 4, charset, action);
|
||||
}
|
||||
|
||||
private void assertByteArray(int initialSize, int expansionSize, Charset charset,
|
||||
ThrowingConsumer<Appendable> action) throws IOException {
|
||||
assertByteArray(charset, (cs) -> new AppendableByteArray(charset, initialSize, expansionSize), action);
|
||||
}
|
||||
|
||||
private void assertByteArray(Charset charset, Function<Charset, AppendableByteArray> factory,
|
||||
ThrowingConsumer<Appendable> action) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
try (OutputStreamWriter writer = new OutputStreamWriter(out, charset)) {
|
||||
action.accept(writer);
|
||||
}
|
||||
AppendableByteArray appendableByteArray = factory.apply(charset);
|
||||
action.accept(appendableByteArray);
|
||||
assertThat(appendableByteArray.toByteArray()).isEqualTo(out.toByteArray());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user