Speicherleck bei Entityklassen

megaflop

Mitglied
Hallo,

Ich bin im Moment dabei, mit JavaEE eine Webanwendung zu entwickeln. Vor ein paar Tagen hat mir Glassfish nach einem ausgiebigen und verteilten Test (die Glassfish-Instanz stand für alle Projektmitglieder mehrere Tage zur Verfügung) den Dienst mit einer OutOfMemoryException quittiert.

Ich hab also angefangen, mich mit Memory Leaks auseinander zu setzen. Mit dem Netbeans Profiler hab ich herrausgefunden, das die Anzahl der Surviving generations stetig steigt, also irgendwo ein Loch sein muss. Mit dem Profiler von Netbeans komme ich nicht weit. Schon allein das deployen dauert 5 Minuten und während der Profiler läuft, ist Netbeans so gut wie unbenutzbar.

Also ging ich weiter und hab mir einen Heap Dump erstellt. Ergebnis:

- java.util.HashMap$Entry
- org.eclipse.persistence.internal.identitymaps.UnitOfWorkCacheKey
- org.eclipse.persistence.internal.descriptors.changetracking.AttributeChangeListener
- sowie eine Entityklasse aus meinem Projekt

waren in cirka gleicher Anzahl vorhanden, etwa 620000 Stück. Die HashMap-Entries etwas mehr. Dieser Heap Dump entstand während eines Tests, in dem etwa 5000 Entity-Instanzen persistiert werden sollten. (nach 2000 war Schluss, es kam zur OutOfMemoryException.)

Was ich soweit verstanden habe: Der Entity-Manager behält für alle verwalteten Instanzen eine Referenz, alle diese Instanzen werden also nicht von der Garbage Collection erfasst. Was ich allerdings nicht verstehe, ist, wie bitteschön 620000(!) Instanzen existieren können (die auch im EntityManager eingetragen sind - das schließe ich aus der Anzahl vorhandener HashMap$Entry-Instanzen), obwohl nur 2000 erstellt werden sollten.

Ich habe keine Idee, wo ich da mit der Fehlersuche ansetzen kann, bzw welche Werkzeuge ich einsetzen könnte. Für jeden noch so kleinen Hinweis wäre ich dankbar.

Ich setze Netbeans 6.9.1 zusammen mit glassfish 3.0.1 ein (also mit Eclipselink als Persistenz-Provider).

Grüße,
megaflop
 

Cage Hunter

Aktives Mitglied
"Oha" ist da wohl weit untertrieben, aber ohne Kaffee fällt mir daruafhin echt nur das ein :oops:

Also, dass es beim Deployen von WebApps zu Speicherproblemen kommen kann ist mir auch schon oft passiert. Bei mir muss ich nur nach ca 50 mal deployen den virtuellen Server neustarten weil der nicht schnell genug ausmistet, aber auf solche Zahlen bin ich glaube ich noch nie gekommen^^

Mich würde interessieren was bei deiner Dateneinspeisung alles angesprochen wird...also wird da definitiv nur die Persistenz benutzt oder kommen da noch etwaiige Sessions hinzu? Die können manchmal auch interessante Effekte hervorrufen wenn man mal was vergisst :)

Was mir gerade spontan auffällt, ist : Du redest vom EntityManager der diese Daten behält. Ich bin mir nicht zu 100% sicher, aber ich denke den solltest du sowieso nicht laufen lassen. Die Factory sollte bleiben weil die ordentlich Ressourcen braucht um gestartet zu werden, aber nen Manager erzeugen kann soviel nicht sein...
Sieh dir mal einen generierten JpaController von NetBeans an, der instanziiert den Manager, macht dann die Transaktionen und schließt dann wieder den Manager. So dürften eigentlich keine sinnlosen Referenzen bestehen bleiben und das Problem ist gelöst...

Jedenfalls sofern ich dich richtig verstanden habe :)
 

megaflop

Mitglied
Wegen Entitymanager: Naja, nachdem ich zB eine neue Entity-Instanz persistiert habe, überwacht der JPA-provider diese ja, um Änderungen in die DB zu schreiben - dazu brauch er Referenzen auf die Entity-Instanzen. Und ich benutze einen Container-managed EntityManager, kümmere mich also nicht un die Instanziierung.

Ich geh mal etwas ins Detail, vielleicht mach ich ja grundlegend etwas falsch. Bei der besagten Entityklasse, die da 600000-mal im Dump lag (GPMObservation) handelt es sich um eine Assoziationsklasse in einer ternären Beziehung zwischen GPMStation, GPMPhase und GPMPlantType:

GPMStation (1:n) GPMObservation
GPMPlantType (1:n) GPMObservation
GPMPhase (1:n) GPMObservation

Die folgende Methode soll ~5000 aufgerufen werden (aus einem Servlet herraus, also wird imho jeder Aufruf in eine eigene Transaktion verpackt), um Daten zu importieren:

Java:
@Stateless
public class ObservationBean {

    @PersistenceContext
    private EntityManager em;

    /*...*/

    //returns true, if successful.
    public boolean addObservation(GPMPlantType plantType, GPMStation station, GPMPhase phase, int year, Integer dayOfTheYear, String comment) {


        if (dayOfTheYear == null || dayOfTheYear <= 0 || dayOfTheYear > 365) {
            return false;
        }

        if (plantType == null || station == null || phase == null) {
            return false;
        }

        GPMObservation observation = em.find(GPMObservation.class, new GPMObservationId(phase.getBbch(), plantType.getNumber(), station.getNumber(), year));
        if (observation == null) {
            //Create a new observation, if none was found.
            observation = new GPMObservation();
            observation.setDayOfTheYear(dayOfTheYear);
            observation.setPhase(phase);
            observation.setPlantType(plantType);
            observation.setStation(station);
            observation.setTheYear(year);
            observation.setComment(comment);
            phase.getObservations().add(observation);
            plantType.getObservations().add(observation);
            station.getObservations().add(observation);
            em.persist(observation);
            em.merge(station);
            em.merge(phase);
            em.merge(plantType);
        } else {
            //Overwrite the old observation.
            observation.setComment(comment);
            observation.setDayOfTheYear(dayOfTheYear);
            em.merge(observation);
        }
        return true;
    }
    /*...*/
}

Und eben während der 5000 Aufrufe kommts nach ~2000 zu Speicherproblemen. Hier noch die Entity an sich...

Java:
@Entity
@IdClass(GPMObservationId.class)
public class GPMObservation implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @Column(insertable = false, updatable = false)
    private Long phaseBbch;

    @Id
    @Column(insertable = false, updatable = false)
    private Long plantTypeNumber;
    
    @Id
    @Column(insertable = false, updatable = false)
    private Long stationNumber;

    @ManyToOne
    @JoinColumn(name = "PHASEBBCH")
    private GPMPhase phase;

    @ManyToOne
    @JoinColumn(name = "PLANTTYPENUMBER")
    private GPMPlantType plantType;

    @ManyToOne
    @JoinColumn(name = "STATIONNUMBER")
    private GPMStation station;

    @Id
    private Integer theYear;

    private Integer dayOfTheYear;

    @Lob
    private String comment;

    /* Getter und Setter */
}
 

Cage Hunter

Aktives Mitglied
Ich will Dir hier wirklich nix Falsches erzählen, aber meiner Meinung nach benötigt JPA keine Referenzen auf Objekte, denn das wird alles über die ID geklärt. Sobald du ein Objekt persistieren lässt, hat es eine ID und wird auch über diese - bei Bedarf - geupdatet.

Zumindest funktioniert mein "Application-Managed Entity Manager" offenbar besser weil gar keine Referenzen ürbig bleiben können^^

Aber leider hast Du mich echt auf dem falschen Fuß erwischt...ich habe keine Ahnung was man da machen könnte um das zu beheben und mit einem einfachen : "Steig doch auf einen Application-Managed Entity Manager" will ich Dich nun nicht abwürgen^^

ABER ausprobieren könntest Du es ja mal :)
 

Cage Hunter

Aktives Mitglied
Im jeweiligen Controller...
Jede Methode, ob insert, drop, update macht ne Transaktion auf, verrichtet ihr Werk und schliesst die Transaktion wieder...
Ich sehe eigentlich keinen Grund warum man das mit größeren Transaktionen nicht auch so machen sollte, alles in eine Methode zu packen :)

Java:
public void insertData(Data input)
{
    EntityManager em = null;
    em = getEntityManager();
    em.getTransaction().begin();
    em.persist(input);
    em.getTransaction().commit();
    em.close();
}

[edit]Mir fällt gerade noch ein, dass du, um dein Problem zu lösen, evtl auch "einfach" was an deinen Properties schrauben musst. Ich hab diesbezüglich auf die Schnelle nix gefunden, aber es muss ja welche geben, wenn der Container alles managen soll, muss er ja schließlich auch wissen wie :)[/edit]
 
Zuletzt bearbeitet:
M

maki

Gast
@Cagehunter
Die Frage war an den TS mit dem Problem gerichtet ;)

Dir würde ich dekleratives (@Transactional) Transaktionshandling empfehlen, spart boilerplate Code und ist um einiges flexibler da es die Wiederverwendbarkeit fördert ;)

Nebenbei, die JPA Implementierung hält Referenzen zu Entites, in der sog. IdentityMap, sonst müsste wirklich jedesmal eine Db Query ausgeführt werden.
 

megaflop

Mitglied
Transaktionen steuere ich über den container, also mit @TransactionAttribute. Hab ich jetzt beim copy'n'pasten unterschlagen, die Methode ist mit @TransactionAttribute(TransactionType=REQUIRES_NEW) annotiert.

So langsam wird mir die Sache aber unheimlich :p

Hab mal aus der unten gezeigten methode die em.merge()-Aufrufe entfernt, jetzt läufts durch. Nach dem Durchlauf hat die jvm nur ein bischen mehr speicher als vorher belegt.

Woran das liegen könnte würd mich aber mal interessieren...
 

Cage Hunter

Aktives Mitglied
Nunja, vermutlich weil's "doppelt-gemoppelt" ist :)
Zum persisten gehören doch sämtliche Member der Klasse dazu, d.h. dass er sowieso die ID's auf die Zugehörigen station's, plant's und phase's setzt :)
Aber DAS "Warum"...nö keine Ahnung^^
 

musiKk

Top Contributor
Ich weiß nicht, ob das hilft, aber einen Gedanken habe ich bzgl. Deiner im ersten Post erwähnten Klassen.

Eclipselink verwendet Maps für den Cache. Standardmäßig sollte eine vernünftige Implementierung eingestellt sein (offenbar WeakSoft) aber vielleicht wurde bei Dir ja etwas anderes konfiguriert.
Der Vorteil bei Implementierungen mit Weak- und SoftReferences ist, dass der Cache funktioniert, der Garbage Collector aber trotzdem aufräumen darf (WeakReferences dürfen immer, SoftReferences erst bei Speicherknappheit abgeräumt werden). Du kannst ja mal schauen, ob es in die Richtung etwas Aussichtsreiches gibt. Hilfreich könnte z. B. Introduction to Cache sein.
 

megaflop

Mitglied
Also an der von dir genannten Chache-Einstellung wurde nichts geändert, steht auf default. Danke trotzdem, ich werd mir das mit dem Cache mal in Ruhe anschauen.

Das Problem hab ich für mich erstmal abgeschlossen - ich werd aber weiter beobachten wie sich der Speicherbedarf so entwickelt mit der Zeit.
 

Neue Themen


Oben