Zoeken…


Opmerkingen

In Java worden objecten toegewezen aan de heap en wordt het heapgeheugen teruggewonnen door automatische garbage collection. Een toepassingsprogramma kan een Java-object niet expliciet verwijderen.

De basisprincipes van het ophalen van Java-afval worden beschreven in het voorbeeld van de afvalinzameling . Andere voorbeelden beschrijven het voltooien, de handmatige activering van de vuilnisman en het probleem van opslaglekken.

afronding

Een Java-object kan een finalize declareren. Deze methode wordt aangeroepen net voordat Java het geheugen voor het object vrijgeeft. Het ziet er meestal als volgt uit:

public class MyClass {
  
    //Methods for the class

    @Override
    protected void finalize() throws Throwable {
        // Cleanup code
    }
}

Er zijn echter enkele belangrijke kanttekeningen bij het gedrag van Java-voltooiing.

  • Java geeft geen garanties over wanneer een methode finalize() wordt aangeroepen.
  • Java kan niet eens garanderen dat een methode finalize() enige tijd tijdens de levensduur van de actieve toepassing wordt aangeroepen.
  • Het enige dat gegarandeerd is, is dat de methode wordt aangeroepen voordat het object wordt verwijderd ... als het object wordt verwijderd.

De bovenstaande waarschuwingen betekenen dat het een slecht idee is om te vertrouwen op de finalize methode om opruimacties (of andere) acties uit te voeren die tijdig moeten worden uitgevoerd. Te veel vertrouwen op finalisatie kan leiden tot opslaglekken, geheugenlekken en andere problemen.

Kortom, er zijn maar weinig situaties waarin afronding eigenlijk een goede oplossing is.

Finalizers worden slechts eenmaal uitgevoerd

Normaal gesproken wordt een object verwijderd nadat het is voltooid. Dit gebeurt echter niet altijd. Beschouw het volgende voorbeeld 1 :

public class CaptainJack {
    public static CaptainJack notDeadYet = null;

    protected void finalize() {
        // Resurrection!
        notDeadYet = this;
    }
}

Wanneer een instantie van CaptainJack onbereikbaar wordt en de garbage collector deze probeert terug te vorderen, zal de methode finalize() een verwijzing naar de instantie toewijzen aan de variabele notDeadYet . Dat maakt het exemplaar opnieuw bereikbaar en de vuilnisman zal het niet verwijderen.

Vraag: Is Captain Jack onsterfelijk?

Antwoord: Nee.

De vangst is dat de JVM slechts eenmaal in zijn levensduur een finalizer op een object uitvoert. Als u null notDeadYet aan notDeadYet waardoor een opnieuw opgestart exemplaar opnieuw onbereikbaar wordt, zal de garbage collector niet finalize() aanroepen op het object.

1 - Zie https://en.wikipedia.org/wiki/Jack_Harkness .

GC handmatig activeren

U kunt de Garbage Collector handmatig activeren door te bellen

System.gc();

Java kan echter niet garanderen dat de Garbage Collector is uitgevoerd wanneer het gesprek terugkomt. Deze methode "suggereert" eenvoudigweg aan de JVM (Java Virtual Machine) dat u de vuilnisman wilt laten draaien, maar dwingt hem niet om dit te doen.

Het wordt over het algemeen als een slechte gewoonte beschouwd om te proberen de afvalinzameling handmatig te activeren. De JVM kan worden uitgevoerd met de optie -XX:+DisableExplicitGC om oproepen naar System.gc() te schakelen. Het System.gc() afvalinzameling door System.gc() aan te roepen System.gc() kan de normale afvalbeheer- / objectpromotieactiviteiten van de specifieke afvalverzamelaarimplementatie die door de JVM wordt gebruikt verstoren.

Garbage collection

De C ++ -benadering - nieuw en verwijderen

In een taal zoals C ++ is het applicatieprogramma verantwoordelijk voor het beheer van het geheugen dat wordt gebruikt door dynamisch toegewezen geheugen. Wanneer een object in de C ++ -heap wordt gemaakt met behulp van de new operator, moet er een overeenkomstig gebruik van de delete operator zijn om het object te verwijderen:

  • Als het programma vergeet een object te delete en het gewoon "vergeet", gaat het bijbehorende geheugen verloren voor de toepassing. De term voor deze situatie is een geheugenlek en er is teveel geheugenlekkage. Een toepassing kan steeds meer geheugen gebruiken en uiteindelijk vastlopen.

  • Aan de andere kant, als een toepassing hetzelfde object twee keer probeert te delete of een object gebruikt nadat het is verwijderd, kan de toepassing crashen vanwege problemen met geheugenbeschadiging

In een ingewikkeld C ++ -programma kan het implementeren van geheugenbeheer met behulp van new en delete tijdrovend zijn. Geheugenbeheer is inderdaad een veel voorkomende bron van bugs.

De Java-aanpak - afvalinzameling

Java heeft een andere aanpak. In plaats van een expliciete delete , biedt Java een automatisch mechanisme dat bekend staat als garbage collection om het geheugen terug te winnen dat wordt gebruikt door objecten die niet langer nodig zijn. Het Java-runtime-systeem neemt de verantwoordelijkheid voor het vinden van de te verwijderen objecten. Deze taak wordt uitgevoerd door een component die een vuilnisman wordt genoemd , of kortweg GC.

Op elk moment tijdens de uitvoering van een Java-programma kunnen we de set van alle bestaande objecten in twee verschillende subsets 1 verdelen:

  • Bereikbare objecten worden als volgt gedefinieerd door de JLS:

    Een bereikbaar object is elk object dat kan worden benaderd in elke potentiële doorlopende berekening vanuit elke live thread.

    In de praktijk betekent dit dat er een reeks verwijzingen is die beginnen met een lokale variabele binnen het bereik of een static variabele waarmee een code mogelijk het object kan bereiken.

  • Onbereikbare objecten zijn objecten die onmogelijk kunnen worden bereikt zoals hierboven.

Alle onbereikbare objecten komen in aanmerking voor afvalinzameling. Dit betekent niet dat ze afval worden verzameld. Eigenlijk:

  • Een onbereikbaar object wordt niet direct verzameld als het onbereikbaar wordt 1 .
  • Een onbereikbaar object mag nooit afval worden verzameld.

De Java-taalspecificatie geeft een JVM-implementatie veel speelruimte om te beslissen wanneer onbereikbare objecten moeten worden verzameld. Het geeft ook (in de praktijk) toestemming voor een JVM-implementatie om conservatief te zijn in hoe het onbereikbare objecten detecteert.

Het enige dat de JLS garandeert, is dat er nooit bereikbare objecten zullen worden verzameld.

Wat gebeurt er als een object onbereikbaar wordt

Allereerst, niets specifieks gebeurt wanneer een object onbereikbaar wordt. Dingen gebeuren alleen wanneer de vuilnisman loopt en het detecteert dat het object onbereikbaar is. Verder is het gebruikelijk dat een GC-run niet alle onbereikbare objecten detecteert.

Wanneer de GC een onbereikbaar object detecteert, kunnen de volgende gebeurtenissen optreden.

  1. Als er Reference zijn die naar het object verwijzen, worden deze verwijzingen gewist voordat het object wordt verwijderd.

  2. Als het object finalizable, dan zal het worden afgerond. Dit gebeurt voordat het object wordt verwijderd.

  3. Het object kan worden verwijderd en het geheugen dat het in beslag neemt kan worden teruggevorderd.

Merk op dat er een duidelijke volgorde is waarin de bovenstaande gebeurtenissen kunnen voorkomen, maar niets vereist dat de afvalverzamelaar de definitieve verwijdering van een specifiek object in een specifiek tijdsbestek uitvoert.

Voorbeelden van bereikbare en onbereikbare objecten

Overweeg de volgende voorbeeldklassen:

// A node in simple "open" linked-list.
public class Node {
    private static int counter = 0;

    public int nodeNumber = ++counter;
    public Node next;
}

public class ListTest {
    public static void main(String[] args) {
        test();                    // M1
        System.out.prinln("Done"); // M2
    }
    
    private static void test() {
        Node n1 = new Node();      // T1
        Node n2 = new Node();      // T2
        Node n3 = new Node();      // T3
        n1.next = n2;              // T4
        n2 = null;                 // T5
        n3 = null;                 // T6
    }
}

Laten we onderzoeken wat er gebeurt als test() wordt aangeroepen. Statements T1, T2 en T3 creëren Node objecten, en de objecten zijn allemaal bereikbaar via respectievelijk de variabelen n1 , n2 en n3 . Statement T4 wijst de verwijzing naar het 2nd Node object toe aan het next veld van de eerste. Als dat is gebeurd, is de 2e Node via twee paden bereikbaar:

 n2 -> Node2
 n1 -> Node1, Node1.next -> Node2

In instructie T5 wijzen we null aan n2 . Dit breekt de eerste van de bereikbaarheidsketens voor Node2 , maar de tweede blijft ongebroken, zodat Node2 nog steeds bereikbaar is.

In instructie T6 wijzen we null aan n3 . Dit doorbreekt de enige bereikbaarheidsketen voor Node3 , waardoor Node3 onbereikbaar is. Node1 en Node2 zijn echter beide nog steeds bereikbaar via de variabele n1 .

Wanneer de methode test() slotte terugkeert, vallen de lokale variabelen n1 , n2 en n3 buiten het bereik en kunnen daarom door niets worden benaderd. Hiermee worden de resterende bereikbaarheidsketens voor Node1 en Node2 en zijn alle Node objecten niet onbereikbaar en komen ze niet in aanmerking voor afvalinzameling.


1 - Dit is een vereenvoudiging die finalisatie en Reference negeert. 2 - Hypothetisch zou een Java-implementatie dit kunnen doen, maar de prestatiekosten om dit te doen maken het onpraktisch.

De formaten Heap, PermGen en Stack instellen

Wanneer een virtuele Java-machine start, moet deze weten hoe groot de Heap moet zijn en wat de standaardgrootte is voor thread-stacks. Deze kunnen worden gespecificeerd met behulp van opdrachtregelopties op het java commando. Voor versies van Java die ouder zijn dan Java 8, kunt u ook de grootte van de PermGen-regio van de Heap opgeven.

Merk op dat PermGen is verwijderd in Java 8, en als u probeert de PermGen-grootte in te stellen, wordt de optie genegeerd (met een waarschuwingsbericht).

Als u de formaten Heap en Stack niet expliciet opgeeft, gebruikt de JVM standaardwaarden die op een versie- en platformspecifieke manier worden berekend. Dit kan ertoe leiden dat uw toepassing te weinig of te veel geheugen gebruikt. Dit is meestal OK voor thread-stacks, maar het kan problematisch zijn voor een programma dat veel geheugen gebruikt.

Heap, PermGen en standaard stapelgroottes instellen:

De volgende JVM-opties stellen de heapgrootte in:

  • -Xms<size> - stelt de initiële heapgrootte in
  • -Xmx<size> - stelt de maximale heapgrootte in
  • -XX:PermSize<size> - stelt de initiële PermGen-grootte in
  • -XX:MaxPermSize<size> - stelt de maximale PermGen-grootte in
  • -Xss<size> - stelt de standaard -Xss<size>

De parameter <size> kan een aantal bytes zijn of kan een achtervoegsel hebben van k , m of g . De laatste specificeert de grootte in kilobytes, megabytes en gigabytes respectievelijk.

Voorbeelden:

$ java -Xms512m -Xmx1024m JavaApp
$ java -XX:PermSize=64m -XX:MaxPermSize=128m JavaApp
$ java -Xss512k JavaApp

De standaardformaten vinden:

De optie -XX:+printFlagsFinal kan worden gebruikt om de waarden van alle vlaggen af te drukken voordat de JVM wordt gestart. Dit kan als volgt worden gebruikt om de standaardwaarden voor de heap- en stapelgrootte-instellingen af te drukken:

  • Voor Linux, Unix, Solaris en Mac OSX

    $ java -XX: + PrintFlagsFinal -version | grep -iE 'HeapSize | PermSize | ThreadStackSize'

  • Voor ramen:

    java -XX: + PrintFlagsFinal -version | findstr / i "HeapSize PermSize ThreadStackSize"

De uitvoer van de bovenstaande opdrachten lijkt op het volgende:

uintx InitialHeapSize                          := 20655360        {product}
uintx MaxHeapSize                              := 331350016       {product}
uintx PermSize                                  = 21757952        {pd product}
uintx MaxPermSize                               = 85983232        {pd product}
 intx ThreadStackSize                           = 1024            {pd product}

De maten worden gegeven in bytes.

Geheugenlekken in Java

In het voorbeeld van Garbage collection impliceerden we dat Java het probleem van geheugenlekken oplost. Dit is eigenlijk niet waar. Een Java-programma kan geheugen lekken, hoewel de oorzaken van de lekken nogal verschillend zijn.

Bereikbare objecten kunnen lekken

Overweeg de volgende naïeve stapelimplementatie.

public class NaiveStack {
    private Object[] stack = new Object[100];
    private int top = 0;

    public void push(Object obj) {
        if (top >= stack.length) {
            throw new StackException("stack overflow");
        }
        stack[top++] = obj;
    }

    public Object pop() {
        if (top <= 0) {
            throw new StackException("stack underflow");
        }
        return stack[--top];
    }

    public boolean isEmpty() {
        return top == 0;
    }
}

Wanneer u een object push en het vervolgens onmiddellijk laat pop , is er nog steeds een verwijzing naar het object in de stack .

De logica van de stapelimplementatie betekent dat die verwijzing niet kan worden geretourneerd naar een client van de API. Als een object is gepopt, kunnen we bewijzen dat het "niet toegankelijk is in een potentiële doorlopende berekening vanuit een live thread" . Het probleem is dat een huidige generatie JVM dit niet kan bewijzen. De huidige generatie JVM's houden geen rekening met de logica van het programma om te bepalen of referenties bereikbaar zijn. (Om te beginnen is het niet praktisch.)

Maar afgezien van de vraag wat bereikbaarheid echt betekent, hebben we hier duidelijk een situatie waarin de NaiveStack implementatie " NaiveStack " aan objecten die moeten worden teruggewonnen. Dat is een geheugenlek.

In dit geval is de oplossing eenvoudig:

    public Object pop() {
        if (top <= 0) {
            throw new StackException("stack underflow");
        }
        Object popped = stack[--top];
        stack[top] = null;              // Overwrite popped reference with null.
        return popped;
    }

Caches kunnen geheugenlekken zijn

Een veelgebruikte strategie voor het verbeteren van de serviceprestaties is het opslaan van resultaten. Het idee is dat u veelvoorkomende verzoeken en hun resultaten registreert in een gegevensstructuur in het geheugen die bekend staat als een cache. Vervolgens, telkens wanneer een verzoek wordt gedaan, zoekt u het verzoek op in de cache. Als het opzoeken lukt, retourneert u de bijbehorende opgeslagen resultaten.

Deze strategie kan zeer effectief zijn als deze correct wordt geïmplementeerd. Als het echter onjuist wordt geïmplementeerd, kan een cache een geheugenlek zijn. Overweeg het volgende voorbeeld:

public class RequestHandler {
    private Map<Task, Result> cache = new HashMap<>();

    public Result doRequest(Task task) {
        Result result = cache.get(task);
        if (result == null) {
            result == doRequestProcessing(task);
            cache.put(task, result);
        }
        return result;
    }
}

Het probleem met deze code is dat hoewel elke aanroep van doRequest een nieuw item aan de cache kan toevoegen, er niets is om ze te verwijderen. Als de service voortdurend verschillende taken krijgt, verbruikt de cache uiteindelijk alle beschikbare geheugen. Dit is een vorm van geheugenlek.

Een manier om dit op te lossen is om een cache met een maximale grootte te gebruiken en oude items weg te gooien wanneer de cache het maximum overschrijdt. (Het weggooien van de minst recent gebruikte invoer is een goede strategie.) Een andere benadering is om de cache te bouwen met behulp van WeakHashMap zodat de JVM cache-invoer kan verwijderen als de heap te vol raakt.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow