© Swill Klitch/Shutterstock.com
I/O Stream Memory Overhead

Von Speicherfressern und viel leerem Nichts


Vor ein paar Wochen haben mein Kollege John Green und ich mit virtuellen Threads experimentiert (Projekt Loom). Unser Server empfing Textnachrichten, änderte ihren Case und sendete sie als Echo zurück. Unser Client simulierte jede Menge Benutzer.

Unser Experiment hatten wir auf 100 000 Sockets pro JVM hochgefahren, was einer Gesamtzahl von 200 000 virtuellen Threads entsprach. Sowohl die Server- als auch die Clientkomponenten liefen gut, aber wir bemerkten, dass der Speicherverbrauch auf dem Client um ein Vielfaches höher war. Aber warum? Der Server-Task sah aus, wie in Listing 1 gezeigt.

Listing 1

import java.io.*; import java.net.*; class TransmogrifyTask implements Runnable { private final Socket socket; public TransmogrifyTask(Socket socket) throws IOException { this.socket = socket; } public void run() { try (socket; InputStream in = socket.getInputStream(); OutputStream out = socket.getOutputStream() ) { while (true) { int val = in.read(); if (Character.isLetter(val)) val ^= ' '; // change case of all letters out.write(val); } } catch (IOException e) {  // connection closed } } }

Der clientseitige Task verwendete bequem PrintStream und BufferedReader zur Kommunikation mit dem Server (Listing 2).

Listing 2

import java.io.*; import java.net.*; import java.util.concurrent.*; class ClientTaskWithIOStreams implements Runnable { private final Socket socket; private final boolean verbose; public ClientTaskWithIOStreams(Socket socket, boolean verbose) { this.socket = socket; this.verbose = verbose; } private static final String message = "John 3:16"; public void run() { try (socket; BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream())); PrintStream out = new PrintStream( socket.getOutputStream(), true) ) { while (true) { out.println(message); TimeUnit.SECONDS.sleep(2); String reply = in.readLine(); if (verbose) System.out.println(reply); TimeUnit.SECONDS.sleep(2); } } catch (Exception consumeAndExit) {} } }

Nachdem wir das Histogramm von jmap auf beiden JVMs ausgeführt hatten, stellten wir fest, dass der größte Speicherfresser der PrintStream war, gefolgt vom BufferedReader. Wir änderten daher den Client Task, um stattdessen einzelne Bytes zu senden und zu empfangen. Nicht alle Clients sind ausführlich, daher erstellen wir einen StringBuilder nur, wenn es erforderlich ist. Darüber hinaus verwendet standardmäßig jeder ClientTask dasselbe statische Appendable, das einen StringBuilder zurückgibt, wenn es sich um einen au...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang