Verflixtes Speicherleck .

Status
Nicht offen für weitere Antworten.
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:

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:

 

Marco13

Top Contributor
Mehr als ins blaue kann ich da jetzt nicht (vielleicht hat noch jemand mehr zu sagen) aber
Ich habe eigentlich Wert drauf gelegt, dass ich keine unnötigen Referenzen irgendwo rumliegen hab,...
das macht man eigentlich IMMER - hast du mal systematisch von der Erstellung an alle "Pfade" abgesucht, die die Referenzen gehen können?

Als pragmatischen Tipp, um sicherzustellen, dass du dir nicht unnötig den Kopf zerbrichst: Sichere der JVM mal z.B. mit -Xmx100m so wenig Speicher zu, dass er eigentlich gerade die geforderten Aktionen durchführen können müßte, er aber NICHT genug Platz hat, um alles aufzuheben, wovon du glaubst, dass es NICHT mehr gebraucht wird. Wenn er dann mit einem OutOfMemoryError abkachelt, hast du wirklich noch irgendwo unerwünschte Referenzen. (Zur Begründung: der GC läuft, wann und wie er will - selbst wenn man System.gc(); aufruft, kann es sein, dass der Speicher NICHT wieder freigegeben wird, weil einfach kein neuer Speicher gebraucht wird. Erst wenn neuer Speicher angefordert wird, und der Platz knapp wird, wird alles getan, um alten Speicher zu löschen und einen OutOfMemoryError zu verhindern)
 
T

tuxedo

Gast
Naja, ich glaub nicht dass ich den GC tunen muss. Ich hab sicher irgendwo was "falsch" gemacht. RMI kommt mit exakt den gleichen Daten ja prima klar. Es ist nicht nur schneller, es braucht auch *bedeutend* weniger Speicher.

Aber ich geh nochmal die Referenzen durch....

- Alex
 

Marco13

Top Contributor
Marco13 hat gesagt.:
Erst wenn neuer Speicher angefordert wird, und der Platz knapp wird, wird alles getan, um alten Speicher zu löschen und einen OutOfMemoryError zu verhindern
Wobei ich gerade in einem ganz anderen Zusammenhang gemerkt habe, dass das nciht stimmt: Mir ist er hier jetzt dauernd mit einem OutOfMemoryError abgeraucht, bis ich nach einer (etwas... "besonderen") Methode mal regelmäßig System.gc() aufgerufen habe, und dann ging's.... ???:L seltsame Sache....
 
T

tuxedo

Gast
Yeeehaaa.... Ich hab den "Fehler".
Das ist kein Speicherleck, das ist ein "ganz normales" Verhalten des Object*Streams.

Ich hole ein wenig aus:

Anfangs hab ich einmal ein byte[] mit der entsprechenden Größe erstellt, es mit zufallswerte initialisiert und dann exakt dieses byte[] 1000mal gesendet. Das ding sowas von super schnell, dass ich über ein 100Mbit Netzwerk mehr als 100Mbit erreicht habe. Das konnte nicht sein. Ich hab schon dran gedacht dass die JVM hier irgendwas optimiert. Konnte es mir aber nicht so ganz erklären.

Nun, der "Schuldige" ist wie gesagt der Object*Stream selbst. Dieser cached die übertragenen Objekte. Hier kann man's nachlesen.

Der Grund warum RMI das "besser" macht: Nun. RMI initialisiert für alles und jeden einen neuen Object*Stream.

Ich werd mich mal mit der im Link angegebenen reset() Methode vertraut machen. Mal sehen was ich damit so alles rauskitzeln kann.

- Alex
 

HLX

Top Contributor
Hatte gerade eine ähnliche Vermutung und wollte schon fragen, ob irgendwo close() aufgerufen wird...
 
T

tuxedo

Gast
Ich frage mich gerade was wohl besser ist?

Stream neu aufbauen oder regelmäßig den cache leeren... Hmm... Mal testen (und bei RMI abgucken ;-) ).

- Alex
 
Status
Nicht offen für weitere Antworten.
Ähnliche Java Themen

Ähnliche Java Themen

Neue Themen


Oben