# Viele Bilder -> Speicher ausgelastet? / (De-)serialisierung geht nicht mehr richtig



## Shiwayari (20. Aug 2009)

Ich habe hier ein Programm geschrieben, mit dem sich Objekte zur Laufzeit instanzieren lassen, denen ein paar Eigenschaften und jeweils ein Bild zugeordnet werden, welche dann in Labels und Panels verpackt auf einem Hauptpanel bzw. im Hauptfenster angezeigt werden.
Die instanzierten Objekte lassen sich in eine Datei speichern, wobei alle Eigenschaften außer das zugeordnete Bild (ist als transient deklariert) mitgespeichert werden. Die Bilder werden dann bei der Deserialisierung allesamt neu geladen und angezeigt.
Alles funktioniert genau so wie es soll.., aber nur solange ich nur wenige Objekte instanziert habe.

Bei ca. 30 - 40 Objekten fängt das Programm an zu hängen. Wenn ich das Fenster skaliere und nicht klein genug mache, dann bleibt ein schwarzer Rand und das Programm hängt und nimmt keine Eingaben mehr auf. Skaliere ich das Fenster kleiner, geht wieder alles. Die Größe bei der alles geht nimmt mit steigender Anzahl an instanzierten Objekten ab. Wenn ich z.B. ein neues Objekt instanziere und schon zu viele vorhanden sind, kann ich kein Bild mehr laden/anzeigen. Skaliere ich das Fenster ein Stück kleiner, kann ich wieder ein paar mehr Bilder laden/anzeigen.

Zudem funktioniert bei zu vielen Objekten die Serialisierung nicht mehr richtig. 
Habe versucht 50 Objekte zu serialisieren und wieder zu deserialisieren, dabei kam eine EOF Exception heraus..
Wenn ich jedoch die 50 Objekte serialisiere ohne vorher die Bilder zu laden, dann funktioniert auch die Deserialisierung; aber die Bilder sollten doch damit nichts zu tun haben, werden ja nicht mit serialisiert, da sie als transient deklariert sind?
Außerdem haben die bis dahin geladenen 50 Bilder zusammen weniger als 6MB.. das sollte doch so oder so zu schaffen sein?

Habe mich bis jetzt noch kein bisschen mit Speicher-Management beschäftigt und davon überhaupt keine Ahnung, da das Programm nicht sehr komplex ist und auch nicht wird.. gibts vielleicht eine Speicher-Grenze, die man erhöhen kann? Und wie bekomme ich das Bilder-Laden speicherfreundlich hin (lade mit ImageIO.read(File))?

Welche Code-Ausschnitte ich jetzt noch posten soll weis ich auch nicht ganz..
Zur Serialisierung vielleicht noch was: Habe alle Objekte in einem Array, und speichere alles in eine Datei.

Naja, hilfe ^^


----------



## Shiwayari (21. Aug 2009)

Habe gerade nochmal ein paar Objekte erstellt ohne irgendwelche Bilder zu laden und serialisiert. Beim Laden funktioniert alles, solange keine Bilder im angegebenen Ordner vorhanden sind. Sind welche da, werden sie nach dem Deserialisieren automatisch geladen und ich bekomme bei dieser Zeile:

```
cover = ImageIO.read(getClass().getResource("\\covers\\"+name+".jpg"));
```
das hier:


> Exception in thread "AWT-EventQueue-0" java.lang.OutOfMemoryError: Java heap space



Also steht nur nicht genug Speicher für die ganzen Bilder zur Verfügung? Problem, da ich hunderte von Objekten mit jeweils einem zugeordneten Bild habe.

Wohlbemerkt, es werden höchstens immer 10 Bilder gleichzeitig angezeigt, da die Panels mit den Daten und dem Bild der Objekte ca 1/10 des Fensters ausfüllen. Alle "DatenPanels" sind in einem ScrollPane und die Objekte lassen sich durch Such- und Sortierfunktionen unterschiedlich anordnen bzw ein- oder ausblenden. Ein Bild nach dem Anzeigen wieder aus dem Speicher zu löschen geht denke ich mal nicht, da es bewegt bzw ein-/ausgeblendet werden kann.
Bräuchte etwas, was sich nur die Dateipfade merkt; nur Bilder anzeigt, die auch wirklich zu sehen sind und alle aus dem Speicher entfernt, die nicht zu sehen sind.
Sowas gibts doch bestimmt? Suchen in google/forum/docu/.. hat mich aber noch nicht weitergebracht..


----------



## André Uhres (22. Aug 2009)

Shiwayari hat gesagt.:


> ...da das Programm nicht sehr komplex ist und auch nicht wird..


Bitte zeig den kompletten Quellcode, da das Problem wahrscheinlich nicht da ist, wo du suchst.


----------



## Shiwayari (22. Aug 2009)

Naja, das ist vielleicht doch etwas viel, insgesamt ca. 2000 Zeilen. Ich poste erstmal ein paar Ausschnitte -

Die Klasse des Fensters, das alle Objekte anzeigt:

Das (De-)Serialisieren:

```
public void saveAs(){
		File saveDir = new File(System.getProperty("user.dir"));
	    saveDir.mkdir();
	    JFileChooser fc = new JFileChooser(saveDir);
	    switch (fc.showOpenDialog(null)) {
	        case JFileChooser.APPROVE_OPTION:
	        	saveFile = fc.getSelectedFile(); // Klassenvariable
	        	save();
	            break;
	        case JFileChooser.CANCEL_OPTION:
	            break;
	    }
	}
	
	public void save(){
		if(saveFile!=null){
			try{
				FileOutputStream fos = new FileOutputStream(saveFile);
				ObjectOutputStream oos = new ObjectOutputStream(fos);
				oos.writeInt(entrys.length);
				for (int i = 0; i < entrys.length; i++) {
					oos.writeObject(entrys[i]);
        			entrys[i].deleteTextFieldBorders();
				}
				oos.writeInt(groups.length);
				for (int i = 0; i < groups.length; i++) {
					oos.writeObject(groups[i]);
				}
				oos.close();				
			}catch(Exception e){}		
		} else {
			saveAs();
		}
	}
	
	public void load(){	
		File loadDir = new File(System.getProperty("user.dir"));
	    loadDir.mkdir();
	    JFileChooser fc = new JFileChooser(loadDir);
	    switch (fc.showOpenDialog(null)) {
	        case JFileChooser.APPROVE_OPTION:
	        	try{
	        		saveFile = fc.getSelectedFile();
	        		FileInputStream fis = new FileInputStream(saveFile);
	        		ObjectInputStream ois = new ObjectInputStream(fis);
	        		int entryCount = ois.readInt();
	        		entrys = new Entry[entryCount];
	        		for (int i = 0; i < entryCount; i++) {
	        			entrys[i]=(Entry) ois.readObject();
					}	        		
	        		int groupCount = ois.readInt();
	        		groups = new Group[groupCount];
	        		for (int i = 0; i < groupCount; i++) {
						groups[i]=(Group) ois.readObject();
					}
	        		ois.close();
	        		for (int i = 0; i < entrys.length; i++) {
	        			entrys[i].loadCover();
	        			entrys[i].deleteTextFieldBorders();						
					}
	        		refreshView();
	        	}catch(Exception e){e.printStackTrace();}
	            break;
	        case JFileChooser.CANCEL_OPTION:
	            break;
	    }
	}
```

Layout des Fensters:


```
private static JScrollPane pScroll = new JScrollPane();
	private static GridBagLayout gridBag = new GridBagLayout();
	private static GridBagConstraints gridConst = new GridBagConstraints();
	private static JPanel main = new JPanel(gridBag);

        //...

        //Konstruktor:

	public Database(String title){		
		super(title);
		setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); 
		setSize(screenSize.width, screenSize.height-30);
		setLocation(0, 0);
		setJMenuBar(createMenuBar());
		setLayout(new GridLayout(1,1));
		add(pScroll);
		pScroll.setViewportView(main);
		pScroll.getVerticalScrollBar().setUnitIncrement(30);
		main.setBackground(BGCOLOR);
		gridConst.anchor=GridBagConstraints.WEST;
		gridConst.fill=GridBagConstraints.NONE;
		gridConst.gridx=0;
		gridConst.insets=new Insets(0, 0, 10, 10);
		setResizable(true);
		setVisible(true);
	}
```


Das Anzeigen der Entry Objekte:


```
public static void refreshView() {		
		//Sort ...
							//Die sortierten Objekte werden im Array sorted gespeichert
		//Sort End	
		//Display Start
		main.removeAll();
								//Die Standartansicht (einzige mit Bildern)
		if(view==STANDART){
			Entry[] sequel = new Entry[0];
			Entry[] first = new Entry[0];
			for (int i = 0; i < sorted.length; i++) {
				if(sorted[i].hasPrequel()){
					sequel=addEntry(sequel,sorted[i]);
				} else {
					first=addEntry(first,sorted[i]);
				}
			}
			for (int i = 0; i < first.length; i++) {
				JPanel row = new JPanel(new GridLayout(1,0,20,0));
				row.setBackground(SEQUELCONNECTCOLOR);
				row.add(first[i].getVisual());  						// getVisual() gibt das Panel mit den Daten und dem Bild des Objekts aus.
				addSequel(row, sequel, first[i].getName());
				gridConst.gridy=main.getComponentCount();
				gridBag.setConstraints(row, gridConst);
				main.add(row);			
			}		
		}
								// Andere Ansichten..
								// ..
		main.repaint();
		pScroll.validate();
		//Display End
	}	


	public static Entry[] addEntry(Entry[] list, Entry toAdd){
		int a = list.length;
		Entry[] temp = new Entry[a+1];
		for (int i = 0; i < a; i++) {
			temp[i]=list[i];
		}
		list=temp;
		list[a]=toAdd;
		return list;
	}	

	public static void addSequel(JPanel row, Entry[] sequel, String prequelName){
		for (int i = 0; i < sequel.length; i++) {
			if(sequel[i].getPrequelName().equals(prequelName)){
				row.add(sequel[i].getVisual());
				addSequel(row, sequel, sequel[i].getName());
				break;
			}
		}		
	}
```

Glaube aber nicht das es an dem Anzeigen liegt, habs halt mal gepostet. Die anderen Ansichten hab ich weggelassen, da das Problem auch auftritt, wenn ich diese garnicht benutzte. Also wenn es am Anzeigen liegt dann hieran.





Die Klasse der Objekte:


```
public class Entry implements ActionListener, KeyListener, MouseListener, Serializable {
	
	final static long serialVersionUID = 114942L;
	
	private GridBagLayout gridBag = new GridBagLayout();
	private GridBagConstraints gridConst = new GridBagConstraints();
	private JPanel data = new JPanel(gridBag);
	
	private JLabel lName = new JLabel();
        //Mehr Labels...
	JLabel[] labelList = {lName, .., ....};

	private JTextField tE = new JTextField(5);
        //Mehr Textfelder..
	JTextField[] textFieldList = { tE, .., ..... };

	private JButton save = new JButton("Save");
        //Mehr Butttons...

	private transient BufferedImage cover;
	public JPanel pImage = new JPanel() {
		public void paintComponent(Graphics g) {
			super.paintComponent(g);
			try {
				int w = cover.getWidth(this);
				int h = cover.getHeight(this);
				g.drawImage(cover, 0, 0, COVERWIDTH, (COVERWIDTH * h) / w, this);
			}catch(NullPointerException e){}
		}
	};

        //..


        //Hinzufügen der Textfelder, Labels, Unterpanels zum Data Panel
public void visualize() {
		format();
		
		gridConst.weightx=100;
		gridConst.weighty=100;
		gridConst.insets=new Insets(0,10,0,10);
		makeVis(lName, 0, 0, 6, 2);

               //usw.. was anderes passiert hier nicht ..

		gridConst.insets = new Insets(0, 0, 0, 0);
		gridConst.anchor = GridBagConstraints.NORTHWEST;
		makeVis(pImage, 0, 2, 3, 9);
	}

	public void format() {
		showName(name);
		
		int[] height = new int[20];
		for (int i = 0; i < 20; i++) {
			height[i] = 20;
		}
		gridBag.rowHeights = height;
		
		data.setBackground(BGCOLOR);
		data.setBorder(BorderFactory.createEtchedBorder());
		for (int i = 0; i < textFieldList.length; i++) {
			textFieldList[i].setHorizontalAlignment(JTextField.CENTER);
		}
		deleteTextFieldBorders();
		pImage.setBackground(BGCOLOR);
		pImage.setMinimumSize(new Dimension(COVERWIDTH, 50));
		pImage.setPreferredSize(new Dimension(COVERWIDTH, 50));
		pImage.setMaximumSize(new Dimension(COVERWIDTH, 50));

		//...
	}


	
	public void showName(String name){
		lName.setText(name);
		int fontSize = 20;
		lName.setFont(new Font("Book Antiqua", Font.BOLD, fontSize));
		while(lName.getPreferredSize().getWidth()>300){
			fontSize--;
			lName.setFont(new Font("Book Antiqua", Font.BOLD, fontSize));
		}
	}
	
	public void deleteTextFieldBorders(){
		for (int i = 0; i < textFieldList.length; i++) {
			textFieldList[i].setBorder(null);
		}
	}

        public void makeVis(Component comp, int x, int y, int width, int height) {
		gridConst.gridx = x;
		gridConst.gridy = y;
		gridConst.gridheight = height;
		gridConst.gridwidth = width;
		gridBag.setConstraints(comp, gridConst);
		data.add(comp);
	}


        //Laden des Bildes:
	
	public void loadCover() {
		try {
			String absPath = new File("").getAbsolutePath();
			cover = ImageIO.read(new File(absPath + "\\covers\\" + name + ".jpg"));              //zum testen als jar
			//cover = ImageIO.read(getClass().getResource("\\covers\\"+name+".jpg"));                    // Zum testen aus Eclipse heraus
			int w = cover.getWidth(pImage);
			int h = cover.getHeight(pImage);
			pImage.setMinimumSize(new Dimension(COVERWIDTH, (COVERWIDTH * h) / w));
			pImage.setPreferredSize(new Dimension(COVERWIDTH, (COVERWIDTH * h) / w));
			pImage.setMaximumSize(new Dimension(COVERWIDTH, (COVERWIDTH * h) / w));
			Database.refreshView();
		} catch (Exception e) {
			cover = null;
		}
	}
```

Das sollte alles sein, was irgendwie mit meinem Problem zu tun haben könnte..


Habe eine funktionierende Speicherdatei mit ca 50 Objekten. Wenn ich diese lade, ohne dass ein Ordner covers mit Bildern vorhanden ist, gibts keine Probleme. Lade ich wenn der Ordner covers vorhanden ist, werden auch alle Objekte (auch alle Group Objekte) geladen, es wird ab ca 40 geladenen Bildern jedoch die for-Schleife mit loadCover und deleteTextFieldBorders übersprungen, da vorher wohl der OutOfMemory Error fliegt.
Ansonsten lässt sich das Programm dann normal weiterbenutzen, solange ich nicht das Fenster skaliere, da es sich sonst aufhängt, bis ich das Fenster klein genug sklaiere. Dann lassen sich wieder ein paar Bilder laden usw..
Daher denke ich schon dass es an den Bildern/am Speicher liegt; naja, hoffe der Code hilft jetzt weiter.

Danke schonmal für die Hilfe


----------



## André Uhres (22. Aug 2009)

Ein grosses Problem liegt vermutlich schon in der Methode loadCover(), obschon aus den Fragmenten nicht hervorgeht, wo sie aufgerufen wird. Dort wird zwar erstmal nur ein einziges Bild geladen, dann aber über die Methode Database.refreshView() das gesamte main-Panel gelöscht und neu aufgebaut, was sehr leistungsmindernd ist:

```
//Laden des Bildes:
    
    public void loadCover() {
        try {
            String absPath = new File("").getAbsolutePath();
            cover = ImageIO.read(new File(absPath + "\\covers\\" + name + ".jpg"));              //zum testen als jar
            //cover = ImageIO.read(getClass().getResource("\\covers\\"+name+".jpg"));                    // Zum testen aus Eclipse heraus
            int w = cover.getWidth(pImage);
            int h = cover.getHeight(pImage);
            pImage.setMinimumSize(new Dimension(COVERWIDTH, (COVERWIDTH * h) / w));
            pImage.setPreferredSize(new Dimension(COVERWIDTH, (COVERWIDTH * h) / w));
            pImage.setMaximumSize(new Dimension(COVERWIDTH, (COVERWIDTH * h) / w));
            Database.refreshView();//<--------------------------------------------------
        } catch (Exception e) {
            cover = null;
        }
    }
```


----------



## sliwalker (22. Aug 2009)

Wieviel Speicher bekommt die Anwendung denn?
Bei sovielen Bilder kommste mit 128 MB auch nicht aus.
Manchmal liegst dann doch an der Hardware.

Beim Starten der Anwendung gib ihr folgende VM-Argumente mit:
-Xms256m  (256 MB als Startwert für die JVM) 
-Xmx1024m (1024 MB als Maximalwert für die VM)


----------



## Shiwayari (22. Aug 2009)

@André Uhres

Die loadCover Methode wird entweder direkt nach dem Deserialisieren in der for-Schleife danach für jedes Entry Objekt aufgerufen; oder per Button aus einem PopupMenu des "DatenPanels" des jeweiligen Objekts.

.. aber du hast recht, wenn ich das so sehe, ist es ziemlich sinnlos an der Stelle alles neu anzuzeigen, ein pImage.repaint() langt ja auch.
Das hat zwar die Ladezeiten etwas verkürzt, aber die Bilder sind immer noch begrenzt anzeigbar.
--


@sliwalker

Hmm, gute Frage wieviel Speicher die Anwendung bekommt.., habe daran nichts geändert, also 'Standart Speicher', wieviel auch immer das ist.

Mit den VM-Argumenten werden aber alle Bilder angezeigt!
Habe jetzt fast alle Daten eingetragen und schon mehrere 100 Bilder.. wird alles angezeigt, auch wenns nicht so schön ist, dass dafür gerade 550MB Speicher draufgehn (für insgesamt ~50mb Bilder). Ist nicht sehr schlimm, etwas Speicherfreundlicher wärs aber trotzdem schöner ^^

Jetzt müsste ich nurnoch wissen, wie ich die gepackte jar standartmäßig mit den Argumenten starte? (Hab sie gerade nur in Eclipse eingegeben)


----------



## André Uhres (23. Aug 2009)

Shiwayari hat gesagt.:


> gute Frage wieviel Speicher die Anwendung bekommt.., habe daran nichts geändert, also 'Standart Speicher', wieviel auch immer das ist.


Ein Java Bild braucht 10 mal mehr Platz als die ensprechende Datei. 
Bei grossen Mengen an Bildern arbeiten wir daher stets mit Thumbnails, 
dann genügen meist die Standardwerte für den Java HeapSpace.
Das geht u.U. auch dynamisch, leistungsstark mit dieser Methode:

```
public static Image getScaledInstance(Image image, int width, int height) {
//create scaled BufferedImage:
    double scaleFactor = 0;
    double w = image.getWidth(null);
    double h = image.getHeight(null);
    if (width < 0) {
        scaleFactor = height / h;
    }
    if (height < 0) {
        scaleFactor = width / w;
    }
    int w1 = (int) (image.getWidth(null) * scaleFactor);
    int h1 = (int) (image.getHeight(null) * scaleFactor);
    BufferedImage scaledImage = new BufferedImage(w1, h1, BufferedImage.TYPE_INT_RGB);
//draw image on it:
    Graphics2D g2 = scaledImage.createGraphics();
    g2.drawImage(image, 0, 0, w1, h1, null);
    return scaledImage;
}
```
Beispiel:

```
img = getScaledInstance(img, dimension.width, -1);//landscape
```
oder:

```
img = getScaledInstance(img, -1, dimension.height);//portrait
```

Die "initial heap size" hängt von der jeweiligen JVM Implementation ab, 
ebenso die "maximum heap size". Du kannst aber einen oder beide Werte anders setzen indem
du die -Xms und -Xmx Optionen beim Starten der JVM nimmst, wobei die -Xms Option die
"initial heap size" und -Xmx die "maximum heap size" angibt.
Zum Beispiel für 300MB "initial heap size" und 300MB "maximum heap size":
java -Xms300m -Xmx300m MeinProg 

Defaults:
initial heap size: 2 MB
maximum heap size: 64 MB (das Limit liegt bei 2GB)
Die optimale heap size ist stark anwendungsabhängig, daher ist es eine Empfehlung nicht so einfach.

Die heap size sollte jedenfalls kleiner sein als der physische Hauptspeicher.
Wenn mehrere Java Anwendungen geichzeitig laufen, dann sollte die Summe der Java heaps 
ebenfalls nicht die Grösse des Hauptspeichers überschreiten.
Ein grosser Server könnte gut 1000 MB heap size benötigen.
Das gibt dir in etwa eine Vorstellung wo sich die 300 MB auf der Skala befinden. 

Du kannst aus dem Programm heraus den HeapSpace festlegen mit:

```
public static void startWithMoreMemory(int min, int max) {
    if (Runtime.getRuntime().freeMemory() < 60000000) {
        String userdir = System.getProperty("user.dir");
        File[] files = new File(userdir).listFiles();
        String app = null;
        for (File file : files) {
            if (file.getName().endsWith(".jar")) {
                app = file.getName();
            }
        }
        if (app != null) {
            String cmd = "java -Xms" + min + "m -Xmx" + max + "m -jar \"" + userdir + "\\" + app + "\"";
            try {
                Runtime.getRuntime().exec(cmd);
            } catch (IOException ex) {
                ex.printStackTrace();
            }
            System.exit(0);
        }
    }
}
```
Beispiel:

```
public static void main(final String[] args) {
    Runnable gui = new Runnable() {

        public void run() {
            startWithMoreMemory(300, 300);
            new NewJFrame().setVisible(true);
        }
    };
    //GUI must start on EventDispatchThread:
    SwingUtilities.invokeLater(gui);
}
```


----------



## Shiwayari (23. Aug 2009)

Also Thumbnails zu benutzen hat den benötigten Speicher um 300 MB reduziert  Hab garnicht dran gedacht, dass ich ja immer nur kleiner gezeichnet habe, aber das Original im Speicher war..
Die heap size im Programm zu setzen hab ich noch nicht probiert, da ich mir erstmal eine Batch genommen habe, die gleich mein Save mitlädt. Trotzdem gut zu wissen, vielleicht mach ich das später doch noch so.

Also Problem gelöst, vielen Dank für die Hilfe!


----------

