Variable in Schleife deklarieren, Speicherplatz, Garbage Collector

texs89

Mitglied
Hallo,
ich habe mir die Frage gestellt ob es grundsätzlich immer sinnvoll ist Variablen außerhalb von Schleifen zu deklarieren oder ob das egal ist? Bezogen auf das Codebeispiel läuft das Programm ja ewig, bis es beendet wird. Wird dann mit jedem Schleifendurchlauf neuer Speicherplatz durch die Variablen und den Scanner usw. belegt, was ein Problem sein kann? Dann würde es ja Sinn machen diese vorher also außerhalb der while-Schleife zu deklarieren. Oder ist das egal? Wann werden denn Variablen grundsätzlich wieder freigegeben durch den Garbage Collector?

Vielen Dank für Eure Antworten!

Beispielcode:
Java:
   public static void main(String[] args) {

        while (true) {
            Scanner scan = new Scanner(System.in);
            // Eingabeaufforderungen für Mietkosten, Einkommen und Anzahl Kinder
            System.out.print("Bitte geben Sie die monatlichen Mietkosten ein: ");
            double mietkosten = scan.nextDouble();
            System.out.print("Bitte geben Sie das monatliche Einkommen ein: ");
            double einkommen = scan.nextDouble();
            System.out.print("Bitte geben Sie die Anzahl der Kinder ein: ");
            int anzahlKinder = scan.nextInt();

            // Berechnung des Wohngelds
            double wohngeld = berechneWohngeld(mietkosten, einkommen);
            // Berechnung des Kinderzuschlags
            double kinderzuschlag = berechneKinderzuschlag(anzahlKinder);
            // Pruefung Gesamtzahlung
            double gesamtzahlung = berechneGesamtzahlung(mietkosten, kinderzuschlag, wohngeld);
            // Ausgabe des berechneten Wohngelds (inkl. Kinderzuschlag)
            ausgabeWohngeld(gesamtzahlung);
        }
    }
 
Den Scanner solltest du schon nur ein einziges Mal außerhalb der Schleife deklarieren und initialisieren.
Bei Variablendeklarationen an sich ist es egal, wo du sie deklarierst. Da ist der Interpreter und Compiler selbstverständlich schlau genug, durch Reachability-Analyse zu sehen, wie lange eine Variable Gültigkeit hat und wann deren Speicherplatz wiederverwendet werden kann.
Das Problem ist nur beim dynamischen Initialisieren einer Variablen mit immer wieder einem neuen Objekt.
Sowas ist im Falle eines Scanners sogar eigentlich falsch, da er im Zweifel mehr seines Inputs einliest und puffert.
 
Evtl. der kurze Hinweis: Es macht Sinn, Variablen immer so nah wie möglich an der Benutzung zu deklarieren. Es ist also unüblich, dass man am Anfang alle Variablen deklariert um diese dann irgendwann zu nutzen. (Das ist / war so üblich bei Skriptsprachen. Davon ist man weg!)

Aber natürlich sollte man Dinge nicht öfters machen als unbedingt notwendig. In diesem Fall ist es so, dass Du EINEN Scanner brauchst. Daher erstellst Du diesen nur ein einziges Mal und daher wäre dies vor der Schleife.
 
Bei manchen Klassen könnte der Compiler sowas theoretisch wegoptimieren, zumindest wenn es bekannte Klassen sind (was bei Scanner der Fall wäre). Ich weiß aber nicht ob der Compiler das tatsächlich auch macht.

Ansonsten würde ich mir herzlich wenig Gedanken darum machen, wann du eine Variable deklarierst. Zum Thema Quellcodeoptimierung habe ich hier irgendwann mal was geschrieben: https://www.java-forum.org/thema/vom-sinn-quellcode-zu-optimieren-lesestoff-fuer-anfaenger.197081/

Normalerweise, nach althergebrachter Tradition, sollte man 'new' in Schleifen möglichst vermeiden, da der Compiler damit neuen Speicher belegt. Sehr wahrscheinlich, daß Java das früher auch mal so gemacht hat.
So weit ich weiß, alloziiert die JVM heute aber den Speicher nicht (mehr?) direkt vom Betriebssystem, sondern nimmt gleich einen größeren Block. Dein Programm benötigt z.B. jetzt 25MB RAM, dann fragt die JVM beim Betriebssystem direkt nach z.B. 100MB. Und wenn dein Programm nach einer Weile immer mehr braucht und irgendwann 80MB RAM belegt, alloziiert die JVM schon mehr Speicher noch bevor er wirklich nicht mehr reicht.
Und auch das Betriebssystem ist mittlerweile schlau genug, Speicherbereiche, die lange genug im RAM rumschimmeln ohne gelesen oder geändert zu werden, auf der Festplatte zwischenzuparken. In Windows heißt das Auslagerungsdatei.
Insofern sollte ein 'new' in einer Schleife heute eigentlich kein Problem sein. Wobei Scanner aber, wie gesagt, durchaus eine Ausnahme sein könnte, wobei der Scanner andererseits wieder so ein Spezialfall mit seinem Zugriff auf den Inputstream ist, andererseits blockiert der Scanner den Schleifendurchlauf und ob man so schnell etwas neues eingeben kann daß der alte Scanner das noch einlesen kann...

Am besten ist eigentlich immer noch, man probiert es einfach mal aus. Ich habe im Studium am liebsten in Laborübungen die Grenzen von allem möglichen direkt ausgetestet. In Laboren, also einer gesicherten Umgebung, kann man sowas machen, und bei Softwarespielereien kann sowieso kaum etwas kaputt gehen. Früher konnte man relativ leicht seinen kompletten Rechner zerballern und mußte dann das Betriebssystem neu installieren, ich meine das auch Hardwareschäden durch Software hervorgerufen werden konnten, aber diese Zeiten sind lange vorbei.

Also, bau doch mal eine Testklasse die in paar Strings verarbeitet (z.B. das erste Zeichen entfernt), damit auch etwas mehr Speicher benötigt wird als es bei einem lumpigen Integer der Fall wäre, und dann machst du zwei Schleifendurchläufe. Einmal erstellst du das Objekt einmal, und beim zweiten Mal erzeugst du das Objekt stets wieder neu, und stoppst die Zeit für beide Durchläufe. Die Apache Stopwatch-Klasse empfiehlt sich da. Und du stoppst die Zeit natürlich nicht für einen einzigen Durchlauf, sondern für z.B. 100.000 Durchläufe.
 
Es geht hier nicht um eine Optimierung von Code bezüglich Laufzeitverhalten! Es geht schlicht um Lesbarkeit! Und aus Sicht von Clean Code sollte man diesbezüglich immer darauf achten, dass der Code so lesbar wie nur irgendwie möglich ist.

Und wie @httpdigest ausgeführt hat, führt dies sogar ggf. zu einem unerwünschten Verhalten (Daher wird da ein Compiler schlicht nichts wegoptimieren! Scanner hat einen internen State, zu dem der Compiler nichts weiss. Wie soll er da irgend etwas optimieren?)
 
Es geht hier nicht um eine Optimierung von Code bezüglich Laufzeitverhalten! Es geht schlicht um Lesbarkeit!
Ja – dir geht es darum (zu Recht natürlich). Ich glaube aber nicht daß der TS das im Sinne hatte als er den Thread eröffnet hat, immerhin erkundigt er sich explizit nach dem Speicherverhalten.

Wie soll er da irgend etwas optimieren?
Das war etwas ins Blaue spekuliert. Ich habe mich nie mit den Innereien der Standardbibliothek beschäftigt und auch nicht mit de Innereien der JVM. Meine Überlegung war rein struktureller Natur: Am Ende wird immer wieder der Inputstream über das Scannerobjekt ausgelesen. Rein von außen betrachtet (d.h. solche internen Feinheiten, wie httpdigest sie angesprochen hat mal außen vor gelassen) macht es keinen Unterschied, ob der Scanner in- oder außerhalb der Schleife initialisiert wird.

Mir ist schon klar daß das so nicht allgemeingültig ist. Wenn ich beispielsweise eine Klasse nehme die einen statischen Zähler hat der bei jedem Konstruktoraufruf inkrementiert werden würde, wäre dieser Zählerstand am Ende ein anderer.
Ich würde allerdings, ohne es wirklich zu wissen, davon ausgehen daß die Compilerbauer für die Klassen der Standardbibliothek besondere Optimierungen eingebaut haben. Immerhin sind diese Klassen ja bekannt, im Gegensatz zu dem was sich die Programmierer selber zurechtschnitzen.
 
Eine Frage hätte ich mal noch @KonradN :
Evtl. der kurze Hinweis: Es macht Sinn, Variablen immer so nah wie möglich an der Benutzung zu deklarieren. Es ist also unüblich, dass man am Anfang alle Variablen deklariert um diese dann irgendwann zu nutzen. (Das ist / war so üblich bei Skriptsprachen. Davon ist man weg!)
Warum? Was gewinnt man, welche Last wird man los?
Ich habe das bisher immer genau so gemacht (Variablen am Anfang deklarieren), wahrscheinlich habe ich irgendwann mal gelesen daß es so gu wäre, und mir wäre bisher auch kein Nachteil dadurch aufgefallen. Aber du wirst das ja nicht ohne Grund schreiben.
 
Was gewinnt man, welche Last wird man los?
Die Last der "Metal Complexity".
  • Du möchtest nicht immer wieder zig Zeilen hoch und runter scrollen, um den Typ einer Variablen zu finden, wenn du eine Referenz auf die Variable im Code siehst; idealerweise ist die Deklaration nicht weit weg von der Nutzung (wenn du gerade mal keine IDE mit mouse-hover/intellisense verwendest) - es verbessert einfach das Verständnis beim Lesen des Codes (z.B. auch für Reviews an Merge/Pull Requests, die nicht immer unbedingt in einer IDE gemacht werden müssen)
  • Du möchtest, wenn du eine neue Variable brauchst, auch nicht wieder bis zum Anfang der Funktion scrollen, sie dort deklarieren und dann wieder zurück zur Nutzungsstelle
 
Von der Funktionalität her macht es keinen Unterschied. Lokale Variablen landen auf dem Stack, d.h. der Compiler muss eh alles zusammen suchen und dann direkt am Methodenanfang "hinterlegen". Es gibt da also zur Laufzeit keinen Unterschied.

Das ist einfach ein Punkt, der bei Clean Code üblich ist. Hintergrund dabei ist aus meiner Sicht einfach ein: Was zusammen gehört ist zusammen. Wenn Du die Variable am Kopf der Methode deklarierst, dann gibst Du da ja einen Typ an und so. Und bei der Nutzung weiter unten ist dann unklar, was es ist? Oder bei Anpassungen musst Du erst nach oben scrollen um dann wieder nach unten zu scrollen um da weiter zu machen?
Es ist einfach unpraktisch - gerade wenn es längere Abschnitte werden.

Im Bereich Clean Code kommt man aber eh zu kurzen Methoden. Da spielt es dann immer weniger eine Rolle, weil sich nur minimal verschiebt.

Das wäre so meine Begründung. Generell ist das aber ein Thema, das jeder so machen darf, wie es es möchte. Es gibt dann auch interessante Aussagen wie in https://kmtsandeepanie.medium.com/how-to-write-clean-code-82aa5db6889d

Erst: "Better to declare variables as much as close to their usage." - ok, das habe ich ja gesagt ... aber dann kommt:
"When considering local variables, it is better if they are declared at the top of each function," - das ist aber dann doch das gegenteil? Aber es geht ja weiter und er grenzt es noch ein:
"control variables used in for loops should be declared within the loop statement. Instance variable declaration should appear at the top of the class."

In Klassen auf jeden Fall irgend eine Ordnung haben. Welche ist egal.
 
Ich würde allerdings, ohne es wirklich zu wissen, davon ausgehen daß die Compilerbauer für die Klassen der Standardbibliothek besondere Optimierungen eingebaut haben. Immerhin sind diese Klassen ja bekannt, im Gegensatz zu dem was sich die Programmierer selber zurechtschnitzen.
Das haben sie ganz explizit nicht gemacht, um die Verantwortlichkeiten hier klar zu trennen und Interoperabilität zwischen einer Classlibrary und der JVM zu schaffen.
Aber erstmal: wenn wir hier von "Compiler" reden, dann meinen wir zwei verschiedene Dinge:
  • javac , bzw. der Java Quellcode -> JVM Bytecode Übersetzer (der ist wirklich unfassbar struntzdumm und guckt sich nicht den Code von aufgerufenen Methoden oder andere Klassen an als die gerade übersetzte)
  • die JVM (wie z.B. HotSpot) bzw. der Interpreter oder auch der JIT Laufzeitcompiler: Dieser übersetzt einfach Methoden nach bestimmten Metriken (häufig aufgerufen, oder enthält häufig iterierte Schleifen, etc.) und "guckt" dabei auch in aufgerufene Methoden mittels "Inlining". ABER er hat kein Wissen oder macht eine Unterscheidung zwischen JRE Klassenbibliotheks-Klassen oder nutzerdefinierten Klassen. Diese Unterscheidung macht höchstens nur der Bootstrap Classloader aus Sicherheitsgründen, damit User nicht alles mögliche in java.* überschreiben können.
Das einzige, was eine JVM aus Performancegründen macht, ist, bestimmte übliche und kleine Methoden wie etwa Math.sin(), Math.sqrt(), etc. mit handgeschriebenem Assembly-Code oder einfach einzelnen Prozessorinstruktionen bei der Kompilierung zu ersetzen, da es keinen Sinn ergibt, für solche Methoden auch noch eine Java-Implementierung bereitzustellen. Hier wird entweder also eine in C geschriebene Funktion (die mit der Classlibrary (also OpenJDK oder GNU Classpath) mitgeliefert wird) aufgerufen, oder aber die JVM (wie eben angesprochen) hat ein "Intrinsics" dafür und weiß einfach, dass der Prozessor, auf dem der Code gerade ausgeführt wird, dafür eine oder wenige schnellere Instruktionen nativ hat.
 
Was gewinnt man, welche Last wird man los?
Ich würde noch ergänzen, dass du auf diese Weise, den Gültigkeitsbereich einer Variable so klein wie möglich hältst.

Das erhöht die Les- und Wartbarbeit und reduziert Fehlermöglichkeiten, siehe auch Thema 57 aus "Effective Java" von Bloch.

Im Gegensatz dazu, könntest du alle Variablen global machen... (nicht gut).
 

Zurück
Oben