T
tuxedo
Gast
Hallo zusammen,
auf der Suche nach dem Bottleneck meines SIMON-Projekts (siehe links in meiner Signatur), bin ich auf einen Interessanten Punkt gestoßen:
ich hab mittels des "Yourkit Java Profilers" (cooles Teil ...) mal mein Projekt unter die Lupe genommen und festgestellt dass es
a) seeehr viel Speicher frisst
b) den Speicher offensichtlich nicht mehr hergibt solange ein Client verbunden ist
c) der GC viel zu tun hat.
Nun, ich muss etwas ausholen .... SIMON funktioniert in etwa so:
Der Client sendet eine Anfrage bzgl. des Ausführens einer Methode zum Server. Diese Anfrage beinhaltet den Namen des Serverobjekts, einen HashWert der die Methode im Serverobjekt identifiziert und die dazugehörigen Parameter. All das wird über einen ObjectOutputStream serialisiert.
Der Server erkennt die Art der eingehenden Pakete (schicke vor jedem Paketbündel eine entsprechende ID raus), holt die Daten im ObjectInputStream und füttert damit ein Runnable das dann in einem ThreadPool ausgeführt wird. Soweit so gut. Das Ergebnis des Aufrufs, bzw. der Return-Wert, wird ebenfalls serialisiert zum Client zurückgeschickt.
Die Sendemethode des Clients wird (bzw. der Thread in der diese ausgeführt wurde) nach dem abschicken der Daten schlafen gelegt. Parallel dazu gibt es einen Empfangsthread der auf Daten vom Server wartet. Dieser erkennt dass da ein Return-Wert vom Server kommt, ließt den Wert aus und speichert ihn in einer HashMap, zusammen mit der Anfrage-ID die der Client sich vor dem wegschicken der Daten gemerkt hat.
Direkt danach wird der Thread am Client, der die Anfrage an den Server geschickt hat und nun schlafend auf die Antwort wartet, geweckt und ihm mitgeteilt dass seine Daten nun da sind. Er holt sich nun den ReturnWert anhand der Anfrage-ID aus der HashMap und ist nun fertig.
Soooo...
Laut meinem Profiler geht am Server 99% des Speichers für das, was ObjectInputStream.readObject() produziert drauf. Das ist natürlich unschön. Ist halt die Frage: Wieso?
Damit ich nicht immer Objekte und Primitive gleichermaßen als Objekte serialisiere, habe ich, wie RMI auch, Methoden die je nach Typ das Objekt anders verschicken bzw. empfangen.
Zum Empfangen sieht die Methode so aus:
"type" bekomme ich von der aufzurufenden Methode. D.h. ich weiß ganz genau was ich lesen will und lese dann mit der entsprechenden read Methode.
Der Profiler bemängelt die "ObjectInputStream.readObject()" Methode innerhalb der "unwrapValue" Methode.
Nun, in meinem Test habe ich keine Primitiven eingesetzt, so dass wirklich nur echte Objekte behandelt werden müssen.
Aufgerufen wird die unwrapValue Methode im Server wie folgt:
Wie man vielleicht erkennen kann, werte ich hier am Server den Methodenaufruf vom Client aus. Die Daten werden gelesen, die Argumente werden mittels der unwrap Methode gefüllt, und das ganze geht dann ab in den ThreadPool wo die Methode ann letztendlich ausgeführt wird.
Ich habe eigentlich Wert drauf gelegt, dass ich keine unnötigen Referenzen irgendwo rumliegen hab, so dass der GC nicht aufräumen kann. Trotzdem krieg ich im Profiler extrem viele meiner im Argument enthaltenen byte[]s angezeigt. --> unschön. Nach Beendigung der Ausführung sollte eigtl alles im Nirvana verschwinden.
Ich mache in meinem Benchmark 1000 Testaufrufe einer Servermethode. Übergeben wird ein primitives Transportobjekt:
Das byte[], dessen Größe im letzten Testfall 65535 beträgt, wird per Zufallsgenerator pro Aufruf immer wieder neu befüllt. Die Servermethode macht nix anderes als:
So. Und jetzt die Preisfrage:
Wieso sammeln sich beim Server immer und immer mehr byte[] an, die exakt der Größe entsprechend die ich im Transport-Objekt übermittle? Und wieso räumt der GC diese nicht auf? Erst wenn ich die Client-Verbindung trenne und der Thread, der für den Client zuständig war beendet wird, erst dann verschwinden die ganzen byte[]s
Würde mich freuen wenn jemand
a) den ellen langen Text bis hierher gelesen hat
und
b) mit nen Tipp geben kann.
Code kann ich klar nachreichen wenn bedarf besteht.
Gruß
Alex
P.S.
Ich hab nicht nur nahezu alle byte[]'s die im Transportobjekt stecken, nein, ich hab auch die ganzen Transportobjekte an sich noch im Speicher liegen. hmmpf.
P.P.S.
Hier mal ein Screenshot des Profiler. Ich hab 17 Testfälle mit je 1000 Methodenaufrufen. Wie man sehen kann liegt _alles_ im Speicher des Servers. Dabei macht der ja nix anderes wie zurückreichen der Daten an den Client:
auf der Suche nach dem Bottleneck meines SIMON-Projekts (siehe links in meiner Signatur), bin ich auf einen Interessanten Punkt gestoßen:
ich hab mittels des "Yourkit Java Profilers" (cooles Teil ...) mal mein Projekt unter die Lupe genommen und festgestellt dass es
a) seeehr viel Speicher frisst
b) den Speicher offensichtlich nicht mehr hergibt solange ein Client verbunden ist
c) der GC viel zu tun hat.
Nun, ich muss etwas ausholen .... SIMON funktioniert in etwa so:
Der Client sendet eine Anfrage bzgl. des Ausführens einer Methode zum Server. Diese Anfrage beinhaltet den Namen des Serverobjekts, einen HashWert der die Methode im Serverobjekt identifiziert und die dazugehörigen Parameter. All das wird über einen ObjectOutputStream serialisiert.
Der Server erkennt die Art der eingehenden Pakete (schicke vor jedem Paketbündel eine entsprechende ID raus), holt die Daten im ObjectInputStream und füttert damit ein Runnable das dann in einem ThreadPool ausgeführt wird. Soweit so gut. Das Ergebnis des Aufrufs, bzw. der Return-Wert, wird ebenfalls serialisiert zum Client zurückgeschickt.
Die Sendemethode des Clients wird (bzw. der Thread in der diese ausgeführt wurde) nach dem abschicken der Daten schlafen gelegt. Parallel dazu gibt es einen Empfangsthread der auf Daten vom Server wartet. Dieser erkennt dass da ein Return-Wert vom Server kommt, ließt den Wert aus und speichert ihn in einer HashMap, zusammen mit der Anfrage-ID die der Client sich vor dem wegschicken der Daten gemerkt hat.
Direkt danach wird der Thread am Client, der die Anfrage an den Server geschickt hat und nun schlafend auf die Antwort wartet, geweckt und ihm mitgeteilt dass seine Daten nun da sind. Er holt sich nun den ReturnWert anhand der Anfrage-ID aus der HashMap und ist nun fertig.
Soooo...
Laut meinem Profiler geht am Server 99% des Speichers für das, was ObjectInputStream.readObject() produziert drauf. Das ist natürlich unschön. Ist halt die Frage: Wieso?
Damit ich nicht immer Objekte und Primitive gleichermaßen als Objekte serialisiere, habe ich, wie RMI auch, Methoden die je nach Typ das Objekt anders verschicken bzw. empfangen.
Zum Empfangen sieht die Methode so aus:
Code:
/**
* unwrap the value with the according read method
*/
protected Object unwrapValue(Class<?> type, ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
if (type.isPrimitive()) {
if (type == boolean.class) {
return Boolean.valueOf(objectInputStream.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(objectInputStream.readByte());
} else if (type == char.class) {
return Character.valueOf(objectInputStream.readChar());
} else if (type == short.class) {
return Short.valueOf(objectInputStream.readShort());
} else if (type == int.class) {
return Integer.valueOf(objectInputStream.readInt());
} else if (type == long.class) {
return Long.valueOf(objectInputStream.readLong());
} else if (type == float.class) {
return Float.valueOf(objectInputStream.readFloat());
} else if (type == double.class) {
return Double.valueOf(objectInputStream.readDouble());
} else {
throw new IOException("Unknown primitive: " + type);
}
} else {
return objectInputStream.readObject();
}
}
"type" bekomme ich von der aufzurufenden Methode. D.h. ich weiß ganz genau was ich lesen will und lese dann mit der entsprechenden read Methode.
Der Profiler bemängelt die "ObjectInputStream.readObject()" Methode innerhalb der "unwrapValue" Methode.
Nun, in meinem Test habe ich keine Primitiven eingesetzt, so dass wirklich nur echte Objekte behandelt werden müssen.
Aufgerufen wird die unwrapValue Methode im Server wie folgt:
Code:
final String serverObject = objectInputStream.readUTF();
final long methodHash = objectInputStream.readLong();
final Method method = lookupTable.getMethod(serverObject, methodHash);
final Class<?>[] parameterTypes = method.getParameterTypes();
final Object[] args = new Object[parameterTypes.length];
// unwrapping the arguments
for (int i = 0; i < args.length; i++) {
args[i]=unwrapValue(parameterTypes[i], objectInputStream);
}
// put the data into a runnable
threadPool.execute(new ProcessMethodInvocationRunnable(this,requestID, serverObject, method, args));
Wie man vielleicht erkennen kann, werte ich hier am Server den Methodenaufruf vom Client aus. Die Daten werden gelesen, die Argumente werden mittels der unwrap Methode gefüllt, und das ganze geht dann ab in den ThreadPool wo die Methode ann letztendlich ausgeführt wird.
Ich habe eigentlich Wert drauf gelegt, dass ich keine unnötigen Referenzen irgendwo rumliegen hab, so dass der GC nicht aufräumen kann. Trotzdem krieg ich im Profiler extrem viele meiner im Argument enthaltenen byte[]s angezeigt. --> unschön. Nach Beendigung der Ausführung sollte eigtl alles im Nirvana verschwinden.
Ich mache in meinem Benchmark 1000 Testaufrufe einer Servermethode. Übergeben wird ein primitives Transportobjekt:
Code:
public class TransportObject implements Serializable {
String id = null;
byte[] b = null;
public TransportObject(byte[] b, String id) {
this.id = id;
this.b = b;
}
}
Das byte[], dessen Größe im letzten Testfall 65535 beträgt, wird per Zufallsgenerator pro Aufruf immer wieder neu befüllt. Die Servermethode macht nix anderes als:
Code:
public TransportObject benchmark(TransportObject b) throws SimonRemoteException {
return b;
}
So. Und jetzt die Preisfrage:
Wieso sammeln sich beim Server immer und immer mehr byte[] an, die exakt der Größe entsprechend die ich im Transport-Objekt übermittle? Und wieso räumt der GC diese nicht auf? Erst wenn ich die Client-Verbindung trenne und der Thread, der für den Client zuständig war beendet wird, erst dann verschwinden die ganzen byte[]s
Würde mich freuen wenn jemand
a) den ellen langen Text bis hierher gelesen hat
und
b) mit nen Tipp geben kann.
Code kann ich klar nachreichen wenn bedarf besteht.
Gruß
Alex
P.S.
Ich hab nicht nur nahezu alle byte[]'s die im Transportobjekt stecken, nein, ich hab auch die ganzen Transportobjekte an sich noch im Speicher liegen. hmmpf.
P.P.S.
Hier mal ein Screenshot des Profiler. Ich hab 17 Testfälle mit je 1000 Methodenaufrufen. Wie man sehen kann liegt _alles_ im Speicher des Servers. Dabei macht der ja nix anderes wie zurückreichen der Daten an den Client:
