Java Garbage Collection

Sp4rky

Cadet 4th Year
Registriert
März 2019
Beiträge
77
Hallo zusammen,
ich habe aktuell ein Problem, wo mein Code beim ausführen den Speicher nach einer Ausführung nicht wieder frei gibt.
Der Code ist etwas größer aber im Prinzip wird nach dem Auslösen eines Events ein neuer Thread gestartet der Inhalte des Events verarbeitet und sich danach beendet. Jetzt würde ich erwarten das der Speicher beim beenden des Threads wieder frei gegeben wird, allerdings scheint das nicht so wirklich der Fall zu sein, weshalb sich das ganze relativ zügig daran macht alles einzunehmen was verfügbar ist.
Direkt nach dem starten scheinen knapp 113MB Speicher reserviert zu sein (htop).
garbage-first heap total 485376K, used 16881K [0x0000000628c00000, 0x0000000800000000)
region size 1024K, 16 young (16384K), 3 survivors (3072K)
Metaspace used 11732K, capacity 12067K, committed 12288K, reserved 1060864K
class space used 1313K, capacity 1443K, committed 1536K, reserved 1048576K
Total Usage ( 140 loaders):

Non-Class: 343 chunks, 10.44 MB capacity, 10.21 MB ( 98%) used, 215.75 KB ( 2%) free, 296 bytes ( <1%) waste, 21.44 KB ( <1%) overhead, deallocated: 195 blocks with 71.71 KB
Class: 179 chunks, 1.41 MB capacity, 1.28 MB ( 91%) used, 118.98 KB ( 8%) free, 16 bytes ( <1%) waste, 11.19 KB ( <1%) overhead, deallocated: 37 blocks with 13.94 KB
Both: 522 chunks, 11.85 MB capacity, 11.49 MB ( 97%) used, 334.73 KB ( 3%) free, 312 bytes ( <1%) waste, 32.62 KB ( <1%) overhead, deallocated: 232 blocks with 85.65 KB

Virtual space:
Non-class space: 12.00 MB reserved, 10.50 MB ( 88%) committed
Class space: 1.00 GB reserved, 1.50 MB ( <1%) committed
Both: 1.01 GB reserved, 12.00 MB ( 1%) committed



Chunk freelists:
Non-Class:

specialized chunks: 2, capacity 2.00 KB
small chunks: 7, capacity 28.00 KB
medium chunks: (none)
humongous chunks: (none)
Total: 9, capacity=30.00 KB
Class:

specialized chunks: (none)
small chunks: (none)
medium chunks: (none)
humongous chunks: (none)
Total: 0, capacity=0 bytes

Waste (percentages refer to total committed size 12.00 MB):
Committed unused: 124.00 KB ( 1%)
Waste in chunks in use: 312 bytes ( <1%)
Free in chunks in use: 334.73 KB ( 3%)
Overhead in chunks in use: 32.62 KB ( <1%)
In free chunks: 30.00 KB ( <1%)
Deallocated from chunks in use: 85.65 KB ( <1%) (232 blocks)
-total-: 607.31 KB ( 5%)

MaxMetaspaceSize: 17179869184.00 GB
InitialBootClassLoaderMetaspaceSize: 4.00 MB
UseCompressedClassPointers: true
CompressedClassSpaceSize: 1.00 GB
Nach etwa 50 ausgelösten Events sind es scheinbar bereits knapp 400MB.
garbage-first heap total 485376K, used 104069K [0x0000000628c00000, 0x0000000800000000)
region size 1024K, 91 young (93184K), 14 survivors (14336K)
Metaspace used 118618K, capacity 121648K, committed 122624K, reserved 1159168K
class space used 12112K, capacity 13849K, committed 13952K, reserved 1048576K
Total Usage ( 867 loaders):

Non-Class: 3378 chunks, 105.27 MB capacity, 104.01 MB ( 99%) used, 1.05 MB ( 1%) free, 3.76 KB ( <1%) waste, 211.12 KB ( <1%) overhead, deallocated: 3490 blocks with 1.96 MB
Class: 1410 chunks, 13.52 MB capacity, 11.83 MB ( 87%) used, 1.61 MB ( 12%) free, 16 bytes ( <1%) waste, 88.12 KB ( <1%) overhead, deallocated: 541 blocks with 377.14 KB
Both: 4788 chunks, 118.80 MB capacity, 115.84 MB ( 98%) used, 2.66 MB ( 2%) free, 3.77 KB ( <1%) waste, 299.25 KB ( <1%) overhead, deallocated: 4031 blocks with 2.32 MB

Virtual space:
Non-class space: 108.00 MB reserved, 106.12 MB ( 98%) committed
Class space: 1.00 GB reserved, 13.62 MB ( 1%) committed
Both: 1.11 GB reserved, 119.75 MB ( 11%) committed



Chunk freelists:
Non-Class:

specialized chunks: 1, capacity 1.00 KB
small chunks: 192, capacity 768.00 KB
medium chunks: (none)
humongous chunks: (none)
Total: 193, capacity=769.00 KB
Class:

specialized chunks: 1, capacity 1.00 KB
small chunks: 3, capacity 6.00 KB
medium chunks: (none)
humongous chunks: (none)
Total: 4, capacity=7.00 KB

Waste (percentages refer to total committed size 119.75 MB):
Committed unused: 200.00 KB ( <1%)
Waste in chunks in use: 3.77 KB ( <1%)
Free in chunks in use: 2.66 MB ( 2%)
Overhead in chunks in use: 299.25 KB ( <1%)
In free chunks: 776.00 KB ( <1%)
Deallocated from chunks in use: 2.32 MB ( 2%) (4031 blocks)
-total-: 6.24 MB ( 5%)


MaxMetaspaceSize: 17179869184.00 GB
InitialBootClassLoaderMetaspaceSize: 4.00 MB
UseCompressedClassPointers: true
CompressedClassSpaceSize: 1.00 GB
Eigentlich würde ich einen Wert um 113MB erwarten, da alle gestarteten Threads wieder beendet wurden. Führe ich nun manuell eine GC aus (evtl wurde ja noch keine ausgeführt), wird zwar einiges entfernt (262 statt 400MB) allerdings eben nicht annähernd alles.
garbage-first heap total 116736K, used 31904K [0x0000000628c00000, 0x0000000800000000)
region size 1024K, 2 young (2048K), 0 survivors (0K)
Metaspace used 118620K, capacity 121648K, committed 122624K, reserved 1159168K
class space used 12112K, capacity 13849K, committed 13952K, reserved 1048576K
Total Usage ( 867 loaders):

Non-Class: 3378 chunks, 105.27 MB capacity, 104.01 MB ( 99%) used, 1.05 MB ( <1%) free, 3.76 KB ( <1%) waste, 211.12 KB ( <1%) overhead, deallocated: 3818 blocks with 2.04 MB
Class: 1410 chunks, 13.52 MB capacity, 11.83 MB ( 87%) used, 1.61 MB ( 12%) free, 16 bytes ( <1%) waste, 88.12 KB ( <1%) overhead, deallocated: 541 blocks with 377.14 KB
Both: 4788 chunks, 118.80 MB capacity, 115.84 MB ( 98%) used, 2.66 MB ( 2%) free, 3.77 KB ( <1%) waste, 299.25 KB ( <1%) overhead, deallocated: 4359 blocks with 2.41 MB

Virtual space:
Non-class space: 108.00 MB reserved, 106.12 MB ( 98%) committed
Class space: 1.00 GB reserved, 13.62 MB ( 1%) committed
Both: 1.11 GB reserved, 119.75 MB ( 11%) committed



Chunk freelists:
Non-Class:

specialized chunks: 1, capacity 1.00 KB
small chunks: 192, capacity 768.00 KB
medium chunks: (none)
humongous chunks: (none)
Total: 193, capacity=769.00 KB
Class:

specialized chunks: 1, capacity 1.00 KB
small chunks: 3, capacity 6.00 KB
medium chunks: (none)
humongous chunks: (none)
Total: 4, capacity=7.00 KB

Waste (percentages refer to total committed size 119.75 MB):
Committed unused: 200.00 KB ( <1%)
Waste in chunks in use: 3.77 KB ( <1%)
Free in chunks in use: 2.66 MB ( 2%)
Overhead in chunks in use: 299.25 KB ( <1%)
In free chunks: 776.00 KB ( <1%)
Deallocated from chunks in use: 2.41 MB ( 2%) (4359 blocks)
-total-: 6.32 MB ( 5%)


MaxMetaspaceSize: 17179869184.00 GB
InitialBootClassLoaderMetaspaceSize: 4.00 MB
UseCompressedClassPointers: true
CompressedClassSpaceSize: 1.00 GB
Das ganze läuft auf OpenJDK 11.0.3 ohne das ich beim starten weitere Flags setze.

Wie müsste ich das ganze konfigurieren damit es eben alles regelmäßig wieder aufräumt?

Viele Grüße
 
Verwendest du in deinem Code Frameworks, was meinst du mit "Events"? Beim Start der Java VM werden sicherlich nicht sämtliche Dinge fertig initialisiert gewesen sein. Der Startwert, wird nie dem Wert nach Zeit x im Betrieb entsprechen, sofern nicht alles darauf ausgelegt ist. Wie sind die Threads implementiert, was machen diese? Mit welchen Klassen interagieren die Threads und was machen diese Klassen. Werden hier irgendwo Objektreferenzen aufgehoben oder gecached?

Die Garbage Collection kann keine Wunder vollführen, sie kann nur das Aufräumen was am Ende als "Müll" identifiziert werden kann. Ohne den Code zu kennen ist es schwierig Tips zu geben, an welchen Stellen man ansetzen könnte. Vom ständigen erzwingen der Garbage Collection würde ich aus Erfahrung aber abraten, da dies durchaus einen Impact auf Performance und mehr hat - ja man kann da ganz viel Tweaken, aber wenn man das am Ende muss weil der Arbeitsspeicher nicht reicht, stellt sich eher die Frage ob Java die richtige Wahl war.
 
Ich denke nicht, dass es sich hier um ein Konfigurationsproblem handelt. Das Problem dürfte vermutlich eher der Quellcode sein. Ohne den Quellcode selbst zu kennen, wird dir die Frage wahrscheinlich keiner beantworten können.
 
(Edit: Bezogen auf #2) ... Oder, ob man alles richtig programmiert hat ;)

Ich bin gerade auf dem Handy und kann mir deshalb nur schlecht Code anschauen, aber ich habe schon mit ein paar Java-Projekten zu tun gehabt, die teilweise während der Laufzeit mehrere Hundert GB an Daten verarbeitet haben und da muss kein einziges Mal der GC manuell ausgeführt werden.

Lg
 
  • Gefällt mir
Reaktionen: hroessler, JP-M, Burfi und eine weitere Person
So kleine Heaps kann man schnell dumpen und analysieren. Einfach einen dump mit z.B. "jmap" erzeugen und dann in MAT laden. Findet man dort GC Roots für die Threads, dann hat man die Ursache gefunden.
 
Im ganzen ist das ein Discord Bot für Musik und so Zeug wobei ich da JDA, slf4j und lavaplayer und deren Abhänigkeiten nutze.
Z.Bsp wird ein onGuildMessageReceived event (von einer Nachricht über einen TextChannel) aufgefangen und dann über einen neuen Thread in einer weiteren Klasse ausgewertet (ob es ein Befehl zum steuern ist etc) welcher dann auch ggf eine resultierende Aktion auslöst (Wiedergabe starten, weitergeben des Events an Erweiterungen etc).
Es gibt da relativ viele Stellen an denen ich denken würde könnte es hängen, aber da eigentlich nichts zurück gegeben wird an das was vor dem starten des Threads läuft sollte eigentlich auch sehr wenig überbleiben.
Ich habe den Code mal auf Github hochgeladen da das etwas mehr ist.
Der Code ist nicht unbedingt schön oder die beste Möglichkeit etwas zu lösen, aber es funktioniert [main ist bei core/Init.java]( und braucht übermäßig Speicher :D ).

// das mit den dumps kucke ich mir gleich nochmal an
// edit: scheinbar bleibt ziemlich viel am URLClassLoader hängen... allerdings ist der heap scheinbar nur 23mb groß auch das ganze breits deutlich mehr als nur 23mb benötigt
 
Zuletzt bearbeitet:
Sämtliche fragliche Ressourcen „nullen“, dann kümmert sich der GC darum...

greetz
hroessler
 
  • Gefällt mir
Reaktionen: FranzvonAssisi
Alle Stellen mit static Variablen sind potientielle GC Roots und können Referenzen halten. Das Music Command ist z.B. ein Kandidat. Dort sind zwei static Variablen MANAGER und PLAYERS. Hier muss man genau verstehen wie viele gecachte Player man erwartet und ob die Tracks vielleicht im Memory bleiben.
Wie oben schon erwähnt sollte man sich den Heapdump mal in MAT ansehen.
 
Bei dem Musik Command schaue ich später mal drüber was man da anpassen kann. Normale Nachrichten kommen allerdings immer bei den Modulen an, wo es scheinbar laut Dump auch hängen bleibt.
794175

Ich vermute Grau ist all das was übrig bleibt.
Leaks werden mir bei java.net.URLClassLoader, java.util.PropertyResourceBundle, java.lang.Class und java.lang.String loaded by "<system class loader>" angezeigt. Also vermutlich aus den Klassen von unter dcb/modulemanagement.
Was müsst ich z.Bsp in GuildModuleProcessor.java anpassen also welche Objekte schließen, löschen, etc damit das sich eben nicht mehr so verhält?
 
Zuletzt bearbeitet:
URLClassLoader müssen immer nach der Benutzung geschlossen werden damit sie collected werden können. In beiden ModuleProcessor-Klassen fehlt also ein URLClassLoader.close().
 
  • Gefällt mir
Reaktionen: Sp4rky
JarFile und URLClassLoader haben eine close()-Methode, die du aufrufen solltest.

Ein paar Kleinigkeiten:
In der Klasse Config wir der Inputstream im Fehlerfall nicht geschlossen.
GuildModuleProcessor und PrivateModuleProcessor verwenden matches(). Das kannst du verbessern, indem du den regex außerhalb der Schleife vorkompilierst
Code:
Pattern pattern = Pattern.compile(".*jar$");
Ansonsten wird der jedes Mal neu erstellt. Oder du verwendest einfach endsWith().

Generell ist es normal, dass der Speicher erst mal anwächst. Erst wenn der GC es für nötig hält (z.B. wenig freier Speicher) wir nicht mehr benötigter Speicher freigegeben. Garbace collection ist "teuer", daher wird das nicht ständig gemacht.
 
  • Gefällt mir
Reaktionen: Sp4rky
Danke für die Hilfe bisher :)
Ich habe die entsprechenden Teile des Codes angepasst, allerdings bin ich nicht wirklich sicher ob das nun richtig funktioniert, da mir in neuen heap-dumps immer noch Leak Suspects angezeigt werden. Diese sind zwar etwas kleiner, aber immer noch zu groß / noch da.
794272

Übersehe ich noch etwas?
 
Zuletzt bearbeitet:
Gibt es z.B. in der Klasse PrivateCoreModuleProcessor einen tieferen Grund warum mit jedem Aufruf von Handle erneut das JarFile coremodule.jar geladen wird? Die anderen *Processor Klassen folgen einem ähnlichen Muster. Sofern es nicht erforderlich ist, dass dieses / diese Jar-Files / Klassen zur Laufzeit getauscht werden sollen, würde ich das coremodule.jar direkt zum Classpath hinzufügen und direkt mit den benötigten Klassen arbeiten.
 
Es würde sicherlich reichen beim starten die einmalig alle zu laden und dann nur noch drauf zu zu greifen.
Das ganze sollte so einfach wie möglich zu benutzen und für mich umzusetzen sein, deswegen funktioniert es so, da ist kein wirklich tieferer Gedanke dabei.
Sonst bräuchte ich sicherlich eine Map oder ähnliches wo dann die Klassen drinnen sind, aber ob das dann noch funktioniert wenn das ganze übergreifend mit weiteren Klassen arbeitet bin ich nicht sicher, habe das nie getestet.
 
Diese "Map" hält der Classloader. Für den Fall, dass die Klassen bereits beim Start bekannt sind (also im Classpath vorhanden sind), wird sichergestellt, dass diese über den Standard Classloader verwendet werden können. Das ist tausend mal einfacher, als das immer wiederkehrende erzeugen neuer Classloader mit all den möglichen Memory-Leaks etc. Die bereits geladenen Klassen können ganz normal über imports verwendet werden.
 
Ich glaube ich kann dir da nicht ganz folgen.
Ich weiß beim starten nicht was und wie viel da geladen wird (kenne ja höchstens die Dateinamen). Ich hoffe auf eine Klasse mit meinen benötigten Funktionen um dann ggf diese zu benutzen., unabhängig davon was diese selbst noch an Klassen und anderem benötigen.
Ich müsste also all meine Klassen mit einem urlclassloader laden; die "main" Klassen mit meinen Funktionen in ner liste Speichern damit ich später weiß auf was ich zugreifen möchte; und dann über handle() halt die Liste über den ClassLoader durch gehen um meine Funktionen aufzurufen.
Wenn ich das so richtig verstanden habe müsste das ganze in etwa so funktionieren (habe es noch nicht getestet):
Java:
public class GuildModuleProcessor {
    private GuildMessageReceivedEvent event;
    private List<String> modules = new ArrayList<String>();
    private boolean modex = false;
    //
    private static URLClassLoader urlcl;
    private static List<String> classname = new ArrayList<String>();

    public GuildModuleProcessor(GuildMessageReceivedEvent event){
        this.event = event;

        // check if urlcl is not null
        if(urlcl == null){
            //get files
            //Check if dir exists
            File directory = new File("./modules/");
            if (!directory.exists()) {
                directory.mkdir();
            }
            //Check if module exist, if load into array
            File[] listOfFiles = directory.listFiles();
            if(listOfFiles != null && listOfFiles.length > 0){
                modex = true;
                int x = 0;
                for (File f: listOfFiles){
                    if(f.getName().endsWith(".jar")){  // just find .jar files
                       modules.add(f.getName());
                    }
                }
            }
            // create array of urls
            if(modex){
                List<URL> urllist = new ArrayList<URL>();
                for(String module : modules){
                    try{
                        //Get main class from file
                        JarFile jfile = new JarFile("./modules/"+module);
                        Manifest mf = jfile.getManifest();
                        Attributes atr = mf.getMainAttributes();
                        String maincp = atr.getValue("Main-Class");
                        jfile.close();
                        //add to list
                        classname.add(maincp);
                        //get url
                        urllist.add(new URL("file:./modules/"+module));
                    }catch (Exception ignore){}
                }
                //create urlclassloader
                URL[] urls = urllist.toArray(new URL[urllist.size()]);
                urlcl = new URLClassLoader(urls, this.getClass().getClassLoader());
            }
        }
    }

    public boolean handle(){
        boolean handled = false;
        if(modex){
            try{
                for(String name : classname){
                    if(handled){
                        break;
                    }
                    Class<?> classToLoad = Class.forName(name, true, urlcl);
                    // execute permission() -> bool
                    Method method_permission = classToLoad.getDeclaredMethod("permission", Member.class); // Permission lvl to module
                    Object instance_permission = classToLoad.getConstructor().newInstance();
                    Object result_permission = method_permission.invoke(instance_permission, event.getMember());
                    // check if permission() -> true
                    if((Boolean) result_permission){
                        // execute module
                        Method method_exec = classToLoad.getDeclaredMethod("guild_execute", GuildMessageReceivedEvent.class, Member.class); // MessageReceivedEvent event, int currentpermission
                        Object instance_exec = classToLoad.getConstructor().newInstance();
                        Object result_exec = method_exec.invoke(instance_exec, event, event.getMember());

                        if(result_exec != null){
                            handled = (boolean) result_exec;
                        }
                    }
                }
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        return handled;
    }

    public String listmodules(){
        //list modules
        return ""; //Arrays.toString(modules);
    }
}
 
Zuletzt bearbeitet:
Der Ansatz, der derzeit im Objektkonstruktor steckt, geht schon einmal in die richtige Richtung. Zumindest wird so vermieden, dass eben dies wieder und wieder passiert. Da anderen Klassen, aber auch so wiederum benötigte Module laden wäre die Frage ob man diesen Teil nicht als Utility-Klasse auslagern könnte.

Sp4rky schrieb:
Ich hoffe auf eine Klasse mit meinen benötigten Funktionen um dann ggf diese zu benutzen.

Wo kommen diese Klassen denn her? Hast du diese ebenfalls geschrieben, oder stammen diese aus einer anderen Quelle. Statt darauf zu hoffen, dass eine Klasse bestimmte Methoden implementiert, würde es sich hier anbieten - sofern möglich - ein passendes Interface zu definieren und dagegen zu implementieren. Aber genug davon.. das geht zu weit vom ursprünglichen Thema weg.

Edit:

Gerade erst gesehen.. du hattest da ja schon ein Interface. Wenn du eine Klasse geladen hast, solltest du prüfen ob diese Klasse von dem jeweiligen Interface (z.B. GuildCommand) abstammt und neue Objekt Instanzen entsprechend casten. Dann musst du nicht so viel "Reflection Magic" betreiben ;-).
 
Zuletzt bearbeitet:
  • Gefällt mir
Reaktionen: Sp4rky
Das war ein überraschend guter Tipp das Zeug nicht ständig neu zu laden. Aktuell scheint sich damit mein Speicher Problem erledigt zu haben, ich beobachte das die nächsten Tage noch aber das was ich bisher testen konnte sieht vielversprechend aus.
JP-M schrieb:
Da anderen Klassen, aber auch so wiederum benötigte Module laden wäre die Frage ob man diesen Teil nicht als Utility-Klasse auslagern könnte.

Gerade erst gesehen.. du hattest da ja schon ein Interface. Wenn du eine Klasse geladen hast, solltest du prüfen ob diese Klasse von dem jeweiligen Interface (z.B. GuildCommand) abstammt und neue Objekt Instanzen entsprechend casten. Dann musst du nicht so viel "Reflection Magic" betreiben ;-).
Das klingt nach etwas was ich mir als nächstes ansehen sollte :)

Vielen Dank für eure Hilfe :)
 
Zurück
Oben