Sök…


Anmärkningar

I Java allokeras objekt i högen och heapminnet återvinns av automatisk skräppost. Ett applikationsprogram kan inte uttryckligen ta bort ett Java-objekt.

De grundläggande principerna för Java-skräpsamling beskrivs i exemplet Garbage collection . Andra exempel beskriver färdigställandet, hur man utlöser skräpfångaren för hand och problemet med lagringsläckor.

slutförande

Ett Java-objekt kan förklara en finalize . Denna metod kallas precis innan Java släpper minnet för objektet. Det ser normalt ut så här:

public class MyClass {
  
    //Methods for the class

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

Men det finns några viktiga varningar om beteendet med Java-slutförande.

  • Java garanterar inte när en finalize() kommer att ringas.
  • Java garanterar inte ens att en finalize() -metod kommer att kallas någon gång under den körande applikationens livstid.
  • Det enda som garanteras är att metoden kommer att ringas innan objektet raderas ... om objektet raderas.

Observera ovan betyder att det är en dålig idé att lita på finalize för att utföra sanering (eller andra) åtgärder som måste utföras i tid. Över beroende av slutförande kan leda till lagringsläckor, minnesläckor och andra problem.

Kort sagt, det finns väldigt få situationer där slutförandet faktiskt är en bra lösning.

Finaliserare körs bara en gång

Normalt raderas ett objekt efter att det har slutförts. Men detta händer inte hela tiden. Tänk på följande exempel 1 :

public class CaptainJack {
    public static CaptainJack notDeadYet = null;

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

När en instans av CaptainJack blir otillgänglig och skräpkollektorn försöker återkrava den kommer metoden finalize() att tilldela en referens till instansen till variabeln notDeadYet . Det gör att instansen kan nås ännu en gång, och skräpsamlaren tar inte bort den.

Fråga: Är kapten Jack odödlig?

Svar: Nej

Fångsten är att JVM kommer bara att köra en finaliserare på ett objekt en gång under sin livstid. Om du tilldelar null till notDeadYet orsakar att en återupptagen instans inte kan nås ännu en gång, kommer inte skräppsamlaren att ringa finalize() på objektet.

1 - Se https://sv.wikipedia.org/wiki/Jack_Harkness .

Manuell utlösning av GC

Du kan aktivera Garbage Collector manuellt genom att ringa

System.gc();

Java garanterar dock inte att Garbage Collector har körts när samtalet återkommer. Denna metod "föreslår" helt enkelt för JVM (Java Virtual Machine) att du vill att den ska köra sopor, men inte tvingar den att göra det.

Det anses i allmänhet som en dålig praxis att manuellt utlösa sopor. JVM kan köras med -XX:+DisableExplicitGC att inaktivera samtal till System.gc() . Att utlösa soporinsamling genom att ringa System.gc() kan störa normal sophantering / objektpromotionsaktiviteter för den specifika implementeringen av sopor som används av JVM.

Skräp samling

C ++ -metoden - ny och radera

På ett språk som C ++ är applikationsprogrammet ansvarigt för att hantera minnet som används av dynamiskt allokerat minne. När ett objekt skapas i C ++ -högen med den new operatören måste det finnas en motsvarande användning av delete att bortskaffa objektet:

  • Om programmet glömmer att delete ett objekt och bara "glömmer" om det, förloras det tillhörande minnet till applikationen. Termen för denna situation är en minnesläcka , och det för mycket minne läcker en applikation kan använda mer och mer minne och krascha så småningom.

  • Å andra sidan, om en applikation försöker ta delete samma objekt två gånger, eller använda ett objekt efter att det har raderats, kan applikationen krascha på grund av problem med minneskorruption

I ett komplicerat C ++ -program kan implementering av minneshantering med new och delete vara tidskrävande. Faktum är att minnehantering är en vanlig källa till buggar.

Java-metoden - skräpkollektion

Java tar en annan inställning. I stället för en uttrycklig delete tillhandahåller Java en automatisk mekanism som kallas skräpsamling för att återställa minnet som används av objekt som inte längre behövs. Java-runtime-systemet tar ansvar för att hitta de objekt som ska kasseras. Denna uppgift utförs av en komponent som kallas en sopor , eller GC för kort.

När som helst under körningen av ett Java-program kan vi dela uppsättningen med alla befintliga objekt i två distinkta delmängder 1 :

  • Nåbara objekt definieras av JLS enligt följande:

    Ett tillgängligt objekt är alla objekt som kan nås i alla potentiella fortsatta beräkningar från alla livetrådar.

    I praktiken innebär detta att det finns en kedja av referenser som startar från en lokal variabel som omfattas eller en static variabel med vilken någon kod kan nå objektet.

  • Oåtkomliga objekt är objekt som omöjligt kan nås som ovan.

Alla objekt som inte kan nås är berättigade till sopor. Detta betyder inte att de kommer att samlas in. Faktiskt:

  • Ett oreagerbart objekt samlas inte in omedelbart när det blir ouppnåeligt 1 .
  • Ett oreagerbart föremål kanske aldrig samlas in.

Java-språkspecifikationen ger mycket latitud till en JVM-implementering för att bestämma när man ska samla in nåbara objekt. Den ger också (i praktiken) tillåtelse för att en JVM-implementering ska vara konservativ när den upptäcker obearbetbara objekt.

Det enda som JLS garanterar att inga nås föremål någonsin kommer att skräp samlas.

Vad händer när ett objekt blir oåtkomligt

Först och främst händer ingenting specifikt när ett objekt blir oåtkomligt. Saker händer bara när skräpfångaren körs och den upptäcker att föremålet inte kan nås. Dessutom är det vanligt att en GC-körning inte upptäcker alla obearbetbara objekt.

När GC upptäcker ett ouppnåeligt objekt kan följande händelser inträffa.

  1. Om det finns några Reference som refererar till objektet kommer dessa referenser att raderas innan objektet tas bort.

  2. Om objektet kan slutföras kommer det att slutföras. Detta händer innan objektet raderas.

  3. Objektet kan raderas och minnet som det upptar kan återvinnas.

Observera att det finns en tydlig sekvens där ovanstående händelser kan inträffa, men ingenting kräver att skräpkollektorn utför den slutliga raderingen av något specifikt objekt i någon specifik tidsram.

Exempel på nåbara och oåtkomliga objekt

Tänk på följande exempelklasser:

// 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
    }
}

Låt oss undersöka vad som händer när test() kallas. Påståenden T1, T2 och T3 skapar Node , och alla objekt kan nås via n1 , n2 och n3 variablerna. Uttalande T4 tilldelar referensen till 2: a Node till next fält i det första. När detta är gjort kan den andra Node nås via två vägar:

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

I uttalandet T5, vi tilldela null till n2 . Detta bryter den första av nåbarhetskedjorna för Node2 , men den andra förblir obruten, så Node2 fortfarande nås.

I uttalande T6 tilldelar vi null till n3 . Detta bryter den enda tillgänglighetskedjan för Node3 , vilket gör Node3 . Emellertid är båda Node1 och Node2 fortfarande tillgängliga via n1 variabeln.

Slutligen, när test() återgår, går dess lokala variabler n1 , n2 och n3 utanför räckvidden och kan därför inte nås av någonting. Detta bryter de återstående tillgänglighetskedjorna för Node1 och Node2 , och alla Node objekt är inte heller nåbara och berättigade till skräpuppsamling.


1 - Detta är en förenkling som ignorerar slutbehandling och Reference . 2 - Hypotetiskt kan en Java-implementering göra detta, men prestandakostnaderna för att göra detta gör det opraktiskt.

Ställa in storlekarna på högen, PermGen och Stack

När en virtuell Java-maskin startar måste den veta hur stor att skapa Heap och standardstorlek för trådstackar. Dessa kan specificeras med hjälp av kommandoradsalternativ på java kommandot. För versioner av Java före Java 8 kan du också ange storleken på PermGen-regionen i högen.

Observera att PermGen togs bort i Java 8, och om du försöker ställa in PermGen-storlek ignoreras alternativet (med ett varningsmeddelande).

Om du inte specificerar Heap- och Stack-storlekar uttryckligen kommer JVM att använda standardvärden som beräknas på en version och plattformsspecifikt sätt. Det kan leda till att din applikation använder för lite eller för mycket minne. Detta är vanligtvis OK för trådstackar, men det kan vara problematiskt för ett program som använder mycket minne.

Ställa in högen, PermGen och standardstorlekar:

Följande JVM-alternativ ställer in högstorleken:

  • -Xms<size> - ställer in den ursprungliga höghöjden
  • -Xmx<size> - ställer in högsta högstorlek
  • -XX:PermSize<size> - ställer in den ursprungliga PermGen-storleken
  • -XX:MaxPermSize<size> - ställer in den maximala PermGen-storleken
  • -Xss<size> - ställer in standardstorleken för trådstapeln

Parametern <size> kan vara ett antal byte eller kan ha ett suffix av k , m eller g . Den senare anger storleken i kilobyte, megabyte respektive gigabyte.

Exempel:

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

Hitta standardstorlekar:

-XX:+printFlagsFinal kan användas för att skriva ut värdena på alla flaggor innan JVM startas. Detta kan användas för att skriva ut standardvärdena för inställningarna för hög och stackstorlek enligt följande:

  • För Linux, Unix, Solaris och Mac OSX

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

  • För Windows:

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

Utgången från kommandona ovan kommer att likna följande:

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

Storlekarna anges i byte.

Minnet läcker i Java

I exemplet Garbage-insamling antydde vi att Java löser problemet med minnesläckor. Det är inte riktigt. Ett Java-program kan läcka minne, men orsakerna till läckorna är ganska olika.

Nåbara föremål kan läcka

Tänk på följande naiva stackimplementering.

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;
    }
}

När du push ett objekt och sedan pop det omedelbart kommer det fortfarande att finnas en hänvisning till objektet i stack .

Logiken för stackimplementeringen innebär att referensen inte kan returneras till en klient till API: n. Om ett objekt har poppats kan vi bevisa att det inte kan "nås i någon potentiell fortlöpande beräkning från någon live tråd" . Problemet är att en nuvarande generation av JVM inte kan bevisa detta. Nuvarande generations JVM: er tar inte hänsyn till programmets logik för att avgöra om referenser kan nås. (Till en början är det inte praktiskt.)

Men bortsett från frågan om vad tillgänglighet egentligen betyder, har vi helt klart en situation här där implementeringen av NaiveStack "hänger på" objekt som borde återkrävas. Det är ett minnesläckage.

I detta fall är lösningen enkel:

    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;
    }

Cachar kan vara minnesläckor

En vanlig strategi för att förbättra servicens prestanda är att cache-resultat. Tanken är att du registrerar vanliga förfrågningar och deras resultat i en datastruktur i minnet känd som en cache. Sedan letar du upp begäran i cachen varje gång en begäran görs. Om uppslaget lyckas returnerar du motsvarande sparade resultat.

Denna strategi kan vara mycket effektiv om den implementeras korrekt. Men om det implementeras felaktigt kan en cache vara ett minnesläcka. Tänk på följande exempel:

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;
    }
}

Problemet med den här koden är att medan alla samtal till doRequest kan lägga till en ny post i cachen, finns det inget att ta bort dem. Om tjänsten kontinuerligt får olika uppgifter, kommer cachen så småningom att konsumera allt tillgängligt minne. Detta är en form av minnesläcka.

En metod för att lösa detta är att använda en cache med en maximal storlek och kasta ut gamla poster när cachen överskrider det maximala. (Att kasta bort den minst nyligen använda posten är en bra strategi.) En annan metod är att bygga cachen med WeakHashMap så att JVM kan släppa cacheposter om högen börjar bli för full.



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow