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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Cut Memory Usage by 90%: Spring Boot Streams Massive JSON from 220 MB to 22 MB

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaMemory OptimizationSpring BootJacksonJsonParserLarge JSON
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.