Cut Memory Usage by 90%: Spring Boot Streams Massive JSON from 220 MB to 22 MB
By replacing the naive List‑based deserialization with Jackson’s JsonParser streaming in Spring Boot 3.5.0, the article shows how processing ~300 k Student records can cut peak heap from 220 MB to 22 MB, avoiding OOM while filtering records on the fly.
Environment
Spring Boot 3.5.0
1. Introduction
In Spring Boot development, deserializing a huge JSON payload into a List at once can cause memory spikes and OOM. This article compares the naive List approach with a Jackson streaming ( JsonParser) solution, showing how incremental parsing of about 300 000 Student objects reduces memory from 220 MB to 22 MB.
2. Practical Example
2.1 Data preparation
Define a Student class with fields id, name, age, gender, className, email, phone, address, city, province, country.
public class Student {
private Integer id;
private String name;
private Integer age;
private String gender;
private String className;
private String email;
private String phone;
private String address;
private String city;
private String province;
private String country;
// getters, setters
}Sample JSON contains roughly 300 000 objects, each with the above fields.
2.2 Traditional handling
Receive the whole array as List<Student> via @RequestBody, then filter.
@PostMapping
public ResponseEntity<Map<String, Object>> processLargeJson(@RequestBody List<Student> students) throws Exception {
System.err.println("size: %s".formatted(students.size()));
System.in.read();
List<Student> result = students.stream()
.filter(s -> s.getAge() > 88)
.toList();
return ResponseEntity.ok(Map.of("code", 0, "data", result));
}When a single request is sent, memory peaks at ~220 MB; multiple concurrent requests can quickly lead to OOM.
2.3 Streaming processing
Use Jackson's JsonParser to read the InputStream token by token, construct Student objects only when needed, and keep only those that satisfy the filter.
@PostMapping
public ResponseEntity<Map<String, Object>> processLargeJson(InputStream inputStream) throws Exception {
System.in.read();
List<Student> students = new ArrayList<>();
try (JsonParser jsonParser = new JsonFactory().createParser(inputStream)) {
if (jsonParser.nextToken() == JsonToken.START_ARRAY) {
while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
Student student = readStudent(jsonParser);
if (student != null && student.getAge() > 88) {
students.add(student);
}
}
}
} catch (IOException e) {
return ResponseEntity.ok(Map.of("code", -1, "message", e.getMessage()));
}
return ResponseEntity.ok(Map.of("code", 0, "data", students));
}
private Student readStudent(JsonParser jsonParser) throws IOException {
Student student = new Student();
while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
String fieldName = jsonParser.currentName();
if (fieldName == null) {
jsonParser.nextToken();
continue;
}
JsonToken token = jsonParser.nextToken();
switch (fieldName) {
case "id" -> { if (token.isNumeric()) student.setId(jsonParser.getIntValue()); }
case "name" -> { if (token == JsonToken.VALUE_STRING) student.setName(jsonParser.getText()); }
case "age" -> { if (token.isNumeric()) student.setAge(jsonParser.getIntValue()); }
case "gender" -> { if (token == JsonToken.VALUE_STRING) student.setGender(jsonParser.getText()); }
case "className" -> { if (token == JsonToken.VALUE_STRING) student.setClassName(jsonParser.getText()); }
case "email" -> { if (token == JsonToken.VALUE_STRING) student.setEmail(jsonParser.getText()); }
case "phone" -> { if (token == JsonToken.VALUE_STRING) student.setPhone(jsonParser.getText()); }
case "address" -> { if (token == JsonToken.VALUE_STRING) student.setAddress(jsonParser.getText()); }
case "city" -> { if (token == JsonToken.VALUE_STRING) student.setCity(jsonParser.getText()); }
case "province" -> { if (token == JsonToken.VALUE_STRING) student.setProvince(jsonParser.getText()); }
case "country" -> { if (token == JsonToken.VALUE_STRING) student.setCountry(jsonParser.getText()); }
default -> jsonParser.skipChildren();
}
}
return student;
}Explanation
JsonParser: Streaming API reads tokens one by one, avoiding loading the whole file into memory.
Incremental processing: Deserializes Student objects in chunks and stores only those that meet the filter, reducing heap usage.
InputStream: The request body is consumed as a stream, suitable for massive JSON payloads.
Real‑time memory monitoring with JConsole shows the JVM heap staying around 22 MB after processing, demonstrating the advantage of streaming over the traditional approach.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
