Java Language
Java Memory Management
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.
Om det finns några
Reference
som refererar till objektet kommer dessa referenser att raderas innan objektet tas bort.Om objektet kan slutföras kommer det att slutföras. Detta händer innan objektet raderas.
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.