PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Java: dispose( ) und Speicher-Ärger


DocEW
2007-06-26, 16:41:15
Hallo zusammen,

mein Problem ist folgendes: Nach der Erzeugen eines JDialog rufe ich dispose( ) auf und erwarte, dass die Ressourcen wieder freigegeben werden. Das ist jedoch irgendwie nicht der Fall. Nachdem ich stundenlang nach irgendwelchen Objekt-Referenzen gesucht habe, die den GC am Aufräumen hindern könnten, bin ich bei folgendem Minimalbeispiel angelangt:


import javax.swing.JDialog;

public class MemTestDialog extends JDialog {

private int test[];

public MemTestDialog() {
this.setModal(true);
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
test = new int[15000000];
}

public static void main(String args[]) {
MemTestDialog memTest;

for(int i=0; i < 10; i++) {
memTest = new MemTestDialog();
memTest.setVisible(true);
memTest.dispose();

memTest = null;
System.runFinalization();
System.gc();
}
}
}

Ohne die letzten paar Zeilen "Nachhilfe" funktioniert es überhaupt nicht, aber selbst mit ihnen kommt ab und zu ein OutOfMemoryError ("Java heap space"). Was mache ich da falsch?

Vielen Dank & vG,

DocEW

Shink
2007-06-26, 16:50:25
dispose() bereinigt AFAIK die Swing/AWT-spezifischen Dinge; nicht aber die test-Variable.
Diese wird erst vom GC bereinigt, wenn er denn mal Zeit hat. System.gc() ruft diesen explizit auf (wobei ich nun nicht weiß, wie zuverlässig).

Ganon
2007-06-26, 17:02:49
Laut JConsole bereinigt er nicht sofort. Wenn du jetzt das Teil so oft hintereinander aufrufst, dann bist du schneller als der GarbageCollector. Wobei ich mich frage warum die Anwendung abstürzt und der GarbageCollector nicht erst mal aufgerufen wird und danach es noch mal probiert wird, wenn nicht genügend Speicher da ist. *grübel*

Aber hier kann ein Java Guru sicher mehr sagen :D

DocEW
2007-06-26, 17:19:16
Wenn du jetzt das Teil so oft hintereinander aufrufst, dann bist du schneller als der GarbageCollector.
Selbst wenn man mittels Thread.sleep(1000) eine Pause einfügt, klappt's nicht. :(

Wobei ich mich frage warum die Anwendung abstürzt und der GarbageCollector nicht erst mal aufgerufen wird und danach es noch mal probiert wird, wenn nicht genügend Speicher da ist. *grübel*
DAS frage ich mich nämlich auch! ;)

Monger
2007-06-26, 17:25:24
Afaik wird der GC nicht mitten in einem Methodenaufruf aktiv, sondern immer nur dazwischen. Damit kann er vermutlich zur Laufzeit von eben diesem Fall nie selbstständig aufräumen.

Aber ob das wirklich der Grund ist, bin ich momentan ein wenig im Zweifel. Möglicherweise ist der Speicherbereich den du mit test[] belegst, schlicht ein wenig zu groß.

Shink
2007-06-26, 17:27:15
Bin zwar kein Java Guru und kann die Frage wohl deshalb nicht wirklich beantworten, aber ich denke:

- Der GC läuft nicht los, wenn man grad mal 1 Sekunde idle ist und er vor Kurzem schon lief.
- Der GC benötigt selbst Speicher. Wenn der Speicher voll ist, ist der Speicher voll: Wenn man einen riesigen Array initialisieren will macht es wenig Sinn, den GC aufzurufen, wenn der Speicher vollzuwerden droht: Es ist einfach zu wenig davon da.

Aber davon abgesehen:
Wenn es dich beruhigt: Dein Problem in der Real-world-Anwendung ist vermutlich ohnehin ein Anderes. Gerade bei GUI-Geschichten ist es ein Leichtes, Memoryleaks einzubauen.

DocEW
2007-06-26, 17:52:21
Vielen Dank für eure Antworten.
Ich habe noch ein bisschen weiter getestet, und es sieht so aus, als wenn man den GC "überrollen" kann. Folgendes Beispiel
import javax.swing.JDialog;

public class MemTestDialog extends JDialog implements Runnable {

private int test[];

public MemTestDialog() {
this.setModal(true);
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
test = new int[2000000];
Thread myThread = new Thread(this);
myThread.start();
}

public void run() {
dispose();
}

public static void showMem() {
System.out.println("total = " + Runtime.getRuntime().totalMemory() + ", free = " + Runtime.getRuntime().freeMemory()
+ ", used = " + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
}

public static void main(String args[]) {
MemTestDialog memTest;

for(int i=0; i < 1000; i++) {
memTest = new MemTestDialog();
memTest.setVisible(true);
showMem();

}
}
}
geht bei mir irgendwann kaputt. Wenn man das int-Array allerdings halb so groß dimensioniert, kommt der GC noch gerade so hinterher.

Gut, soviel zu diesem "akademischen Beispiel"... es bleibt offen, warum das allererste Programm nicht funktioniert (warum räumt der GC nicht auf, wenn der Speicher nicht für die nächste Anforderung reicht?). Das würde mich auf jeden Fall noch interessieren.

Ansonsten @Shink: Nee, beruhigt mich überhaupt nicht, denn das heißt, dass ich jetzt weiter nach verloreren Referenzen suchen darf! ;) Nicht so spaßig, bei einem Dialog mit > 30 JFreeCharts ...

Ganon
2007-06-26, 17:57:29
- Der GC benötigt selbst Speicher. Wenn der Speicher voll ist, ist der Speicher voll: Wenn man einen riesigen Array initialisieren will macht es wenig Sinn, den GC aufzurufen, wenn der Speicher vollzuwerden droht: Es ist einfach zu wenig davon da.

Java hat aber zur Startzeit einen festen MaxHeap. D.h. wenn die Meldung kommt heißt es nicht das der ganze Arbeitsspeicher voll ist, sondern nur die Maximalgrenze der Java Sandbox überschritten wurde.

Ganon
2007-06-26, 18:02:55
Afaik wird der GC nicht mitten in einem Methodenaufruf aktiv, sondern immer nur dazwischen. Damit kann er vermutlich zur Laufzeit von eben diesem Fall nie selbstständig aufräumen.

Auch möglich, aber warum ruft intern der Allocator nicht einfach den GC mal auf, wenn die Exception geworfen wird und probiert es danach noch mal? Die VM hat doch ganz andere Gesetze als alles darüber.

Aber ob das wirklich der Grund ist, bin ich momentan ein wenig im Zweifel. Möglicherweise ist der Speicherbereich den du mit test[] belegst, schlicht ein wenig zu groß.

Naja, das sind im oberen Beispiel ca. 50 MB an Rohdaten + Overhead. Das Array wird sicher nur ein Beispiel sein, es kann ja auch ein Bild sein oder was anderes.

Das Thema finde ich immer interessanter :D

Shink
2007-06-26, 18:44:26
Java hat aber zur Startzeit einen festen MaxHeap. D.h. wenn die Meldung kommt heißt es nicht das der ganze Arbeitsspeicher voll ist, sondern nur die Maximalgrenze der Java Sandbox überschritten wurde.
Ich weiß. Macht aber keinen Unterschied, wenn der GC den Heap verwendet (nicht nur freiräumt); davon wäre ich eigentlich ausgegangen.

Wenn eine Exception geworfen wird, wäre es wohl etwas seltsam, das selbe nocheinmal zu probieren, oder?
Ich würde mir das genau so erwarten wie es ist:
Wenn meine Rohdaten xMB weit überschreiten, hab ich ein Problem. Wenn ich öfters mal y<xMB alloziiere und zwischendurch keine Zeit ist zum Aufräumen, hab ich auch ein Problem. Man sollte sich nicht auf den GC verlassen, das ist IMO eine der Dinge, die man in Java beachten sollte: Wenn er ungenutztes Zeug irgendwann aufräumt, gut; verlassen kann man sich aber nicht darauf und vielleicht macht ein User mal etwas, wo dies gar nicht möglich wäre.

Wenn mir der Speicher zu wenig ist (standardmäßig ist die maxHeapSize ja nicht gerade berauschend hoch), muss ich den maxHeapSize vergrößern und das OS soll sich darum kümmern, uns Speicher zuzuschieben, wenn wir mal mehr brauchen.

Ganon
2007-06-26, 18:53:03
Wenn eine Exception geworfen wird, wäre es wohl etwas seltsam, das selbe nocheinmal zu probieren, oder?

Wieso? Exceptions sind doch keine Abbruch-Fehler. Man kann die Exception auch fangen, versuchen etwas dagegen zu tun (GarbageCollector aufrufen) und es dann noch mal probieren. Wenn's danach immer noch nicht geht, dann halt abbrechen.

Bei mir ist die MaxHeapsize automatisch auf knapp 500 MB gestellt. Das sprengte auch weg, wenn ich die Schleife verlängert habe.

Ich will jetzt auch nicht sagen das es jetzt was fatales ist, weil das halt nur ein Extrem-Beispiel ist. Es wundert mich halt nur das Java da abbricht, obwohl nach einem GC-Durchlauf wieder massenhaft Speicher frei wäre.

System.gc() "bittet" ja nur mal den Speicher aufzuräumen. Ob es denn >wirklich< passiert, ist ja nicht gesagt. Aber die VM an sich kann das doch machen wann ihr es lieb ist.

Naja. Vllt. kommt ja noch jemand der ne Erklärung hat :D

PH4Real
2007-06-26, 19:28:56
Du brauchst eine aggressivere Heap Verwaltung (auf Kosten der Laufzeit des Programmes natürlich).

Das VM-Kommando lautet: -XX:+AggressiveHeap

Dann läuft ohne Probleme dieser Code durch:

public class MemTestDialog extends JDialog {

private int test[];

public MemTestDialog() {
this.setModal(true);
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
test = new int[15000000];
}

public static void main(String args[]) {
MemTestDialog memTest;

for (int i = 0; i < 10; i++) {
memTest = new MemTestDialog();
memTest.setVisible(true);
memTest.dispose();
}
}
}


Das ganze "Memory" Handling bringt nichts, da das nur "Bitten" an die VM sind.

EDIT: Hier steht auch noch ein bißchen mehr über das Heap-Management: http://kr.sun.com/developers/pages/STDpts/J2SEandJ2EEPerformanceKorea.pdf

Ach ja... und die JVM sollte im "Server-Mode" geschaltet sein: "-server"

Monger
2007-06-26, 19:51:15
Im Grunde ist das selbstverständlich, aber ich möchte es trotzdem noch einmal betonen: wenn der Heap Space überläuft, sollte man in erster Linie sein Programmkonzept überdenken. Am GC rumzufriemeln und einfach mehr Speicher freizuräumen, ist meistens nur Makulatur, und verschiebt das Problem nur statt es zu beheben.

Ich glaube, in dem Fall geht es, weil es nur eine Hand voll extrem großer Objekte sind. Viele kleine Objekte sind vermutlich schlimmer, weil selbst mit einem größeren Heap Space tut sich der GC schwer, die Speicherbereiche ordentlich zu verwalten.

PH4Real
2007-06-26, 19:59:04
Im Grunde ist das selbstverständlich, aber ich möchte es trotzdem noch einmal betonen: wenn der Heap Space überläuft, sollte man in erster Linie sein Programmkonzept überdenken. Am GC rumzufriemeln und einfach mehr Speicher freizuräumen, ist meistens nur Makulatur, und verschiebt das Problem nur statt es zu beheben.

Ich glaube, in dem Fall geht es, weil es nur eine Hand voll extrem großer Objekte sind. Viele kleine Objekte sind vermutlich schlimmer, weil selbst mit einem größeren Heap Space tut sich der GC schwer, die Speicherbereiche ordentlich zu verwalten.

In bestimmten Anwendungsfällen wird man nicht darum kommen den Heap zu vergrößern. Ich hatte auch schon viele viele kleine Objekte, welche dynamisch generiert und zerstört wurden (großen AST gebaut) und bis zu 500 MB groß wurden. Das klappte auch sehr gut, wenn man entsprechend den maxHeap erhöht.

Die "Aggressive" Option ist hingegen schon etwas übel...

Wer natürlich bspw. ein Videoverarbeitungsprogramm so programmiert, dass es alle Videostreams erstmal komplett in den Speicher lädt, sollte natürlich das Konzept überdenken :biggrin:.

Ein Memory Profiler hilft dabei die großen Memoryübeltäter zu finden und um auch den maximalen Speicherverbrauch abzuschätzen.

EDIT: Eclipse kommt ja auch mit "-Xms40m -Xmx256m" aus... ;)

PH4Real
2007-06-27, 11:10:06
Hmm... es ist wohl doch etwas komplizierter...

Also erstmal verbraucht ein Array aus knapp 15 Mio Integer etwas weniger als 60 MB. Die StandardHeapSize liegt bei 64 MB, so dass dies schon sehr nahe am Maximum ist.

Die Garbarge Collection läuft ja auch in einem eigenem Thread und ist wohl hier einfach zu langsam bzw. wird nicht zeitnah ausgelöst. Das kann auch mit dem SwingEventThread zu tun haben.... Wenn das Fenster erst gar nicht dargestellt wird und keine Methodenaufrufe auf diesen gesetzt werden, ist der Garbage Collector schnell genug. Wenn man den "Puffer" für den Garbage Collector erhöht und zum Beispiel die MaxHeapSize statisch auf 128 MB setzt, dann läuft der Heap nie über, auch wenn Zähler der Schleife beliebig erhöht wird. Im Durchschnitt hat die Applikation immer knapp 12 MB freien Speicher, also hält genau die alte und die aktuelle Instanz im Speicher.

Shink
2007-06-27, 13:38:40
Wieso? Exceptions sind doch keine Abbruch-Fehler. Man kann die Exception auch fangen, versuchen etwas dagegen zu tun (GarbageCollector aufrufen) und es dann noch mal probieren. Wenn's danach immer noch nicht geht, dann halt abbrechen.
Nun ja; Exceptions sind in Java eigentlich per Definition Abbruch-Fehler (ansonsten würde es heißen: On Exception blabla() und dann kehre zurück - war bei ein paar alten Sprachen so).
Was du da aufzählst sind Dinge, die man nach dem Abbruch tun kann. Und Dinge, von denen ich hoffe, dass sie die Runtime nicht macht (sonst ist der letzte Rest an Vorhersehbarkeit dahin) - die arbeitet doch schon mit compilierten Code (wenn es auch Bytecode ist) und soll keine Änderungen in der Logik vornehmen, oder?

Ganon
2007-06-27, 13:54:34
Was du da aufzählst sind Dinge, die man nach dem Abbruch tun kann.

Sicher. Er könnte jetzt auch diese Exception fangen und erst mal 2 Sekunden lang dauernd System.gc() aufrufen und dann es noch mal probieren. Nur gibt das immer noch keine Garantie das der GC aufgerufen wird.

Und Dinge, von denen ich hoffe, dass sie die Runtime nicht macht (sonst ist der letzte Rest an Vorhersehbarkeit dahin)

Kriegt man denn irgendwie Objekte manuell aus dem Speicher entfernt?

DocEW
2007-06-28, 12:17:52
Ich nochmal... schaut euch bitte mal folgende Abwandlung des Problems an:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JDialog;

public class MemTestDialog extends JDialog implements ActionListener {

private JButton okButton = new JButton("OK");

private int test[];

public MemTestDialog() {
this.setModal(true);
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
test = new int[2000000];

okButton = new JButton("OK");
okButton.addActionListener(this);

this.add(okButton);
this.pack();
}

@Override
public void dispose() {
super.dispose();
okButton.removeActionListener(this);
okButton = null;
}


@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize() was called.");
}

public void actionPerformed(ActionEvent e) {
dispose();
}

public static void showMem() {
System.out.println("total = " + Runtime.getRuntime().totalMemory() + ", free = " + Runtime.getRuntime().freeMemory()
+ ", used = " + (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()));
}

public static void main(String args[]) {
MemTestDialog memTest;

for(int i=0; i < 20; i++) {
memTest = new MemTestDialog();
memTest.setVisible(true);
showMem();
memTest = null;
}
}

}
Wenn man die Fenster alle mittels des Knopfes schließt, wird finalize niemals aufgerufen! Schließt man sie mittels des kleinen x rechts oben, geht's glatt durch. Ich hab den Verdacht, dass es irgendwie am ActionListener liegt, also habe ich versucht, ihn wieder zu entfernen - ohne Erfolg. :(

Das kann doch nicht so schwer sein?!

Ein verzweifelter DocEW

PH4Real
2007-06-28, 12:27:42
Habe dein Beispiel mit Java 1.6 eben mal ohne jegliche VM Argumente ausprobiert. Finalize wird bei mir aufgerufen, aber die Anwendung terminiert nicht... Du rufst "dispose()" auf das aktuelle Objekt auf, versuchst danach aber noch den ActionListener zu entfernen.

Lösch einfach komplett deine überschriebene dispose Methode, dann klappt es. Jedenfalls bei mir ;)...

EDIT: "-XX:+AggressiveHeap" noch an? ... dann schalte es aus, da diese Anwendung braucht es nicht. Mit 1.6 (glaube auch schon mit 1.5) kannst Du Dir übrigens mit JConsole, was beim JDK mitgeliefert wird, sehr schön grafisch den Speicherverbrauch angucken.

EDIT2: Sieh was Du angrichtet hast :D... Da mich das jetzt auch interessierte, habe ich den Kram mal schnell mit und ohne +AggressiveHeap in JConsole ausgegeben. Bei Agressive sieht man schön, wie der GC erst ganz am Schluß loslegt...

Standardeinstellung:
http://img72.imageshack.us/img72/311/standardpq7.th.png (http://img72.imageshack.us/my.php?image=standardpq7.png)

AggressiveHeap:
http://img72.imageshack.us/img72/9006/aggressiveheapmp1.th.png (http://img72.imageshack.us/my.php?image=aggressiveheapmp1.png)

DocEW
2007-06-28, 14:36:45
Kann ich leider überhaupt nicht bestätigen. :(
Terminiert bei mir auch mit überschriebenem dispose( ), finalize( ) wird nur aufgerufen, wenn ich das Fenster über das "x" schließe und ansonsten kommt der java.lang.OutOfMemoryError. :( Alles mit JDK 1.5.0_08 - kann das vielleicht nochmal jemand anderes mit dem 5er JDK verifizieren?

P.S.: Aber danke für den Tipp mit JConsole - sehr nett!

PH4Real
2007-06-28, 15:09:04
Kann ich leider überhaupt nicht bestätigen. :(
Terminiert bei mir auch mit überschriebenem dispose( ), finalize( ) wird nur aufgerufen, wenn ich das Fenster über das "x" schließe und ansonsten kommt der java.lang.OutOfMemoryError. :( Alles mit JDK 1.5.0_08 - kann das vielleicht nochmal jemand anderes mit dem 5er JDK verifizieren?

Kann ich leider nicht reproduzieren... auch mit JDK 1.5.0_10 nicht..:confused:

P.S.: Aber danke für den Tipp mit JConsole - sehr nett!

Immerhin :)... aber ich weiß nicht wie komfortable das Tool in 1.5 ist...

DocEW
2007-06-28, 15:55:31
Kann ich leider nicht reproduzieren... auch mit JDK 1.5.0_10 nicht..:confused:
Also mit dem 1.5.0_4 geht's bei mir auch! :eek: