Java Language
Java-fallgropar - Prestationsproblem
Sök…
Introduktion
Detta ämne beskriver ett antal "fallgropar" (dvs. misstag som nybörjare av Java-programmerare gör) som relaterar till Java-applikationsprestanda.
Anmärkningar
Det här ämnet beskriver vissa "mikro" Java-kodningspraxis som är ineffektiva. I de flesta fall är ineffektiviteten relativt små, men det är fortfarande värt att undvika dem är möjligt.
Fallgrop - Kostnaderna för att skapa loggmeddelanden
TRACE
och DEBUG
finns för att kunna förmedla hög detalj om driften av den givna koden vid körning. Det rekommenderas vanligtvis att ställa in loggnivån ovanför dessa, men vissa försiktighetsåtgärder måste vidtas för att dessa påståenden inte påverkar prestandan, även om det till synes "stängs av".
Överväg detta logguttalande:
// Processing a request of some kind, logging the parameters
LOG.debug("Request coming from " + myInetAddress.toString()
+ " parameters: " + Arrays.toString(veryLongParamArray));
Även när loggnivån är inställd på INFO
kommer argument som skickas till debug()
att utvärderas vid varje exekvering av raden. Detta gör det onödigt förbrukande på flera punkter:
-
String
sammanlänkning: fleraString
instanser skapas -
InetAddress
kan till och med göra en DNS-sökning. - the
veryLongParamArray
kan vara väldigt lång - att skapa en sträng ur det kräver minne, tar tid
Lösning
De flesta loggningsramar ger medel för att skapa loggmeddelanden med hjälp av fixsträngar och objektreferenser. Loggmeddelandet utvärderas endast om meddelandet faktiskt är inloggat. Exempel:
// No toString() evaluation, no string concatenation if debug is disabled
LOG.debug("Request coming from {} parameters: {}", myInetAddress, parameters));
Detta fungerar mycket bra så länge alla parametrar kan konverteras till strängar med String.valueOf (Object) . Om beräkningen av loggmeddelandet är mer komplex kan loggnivån kontrolleras innan du loggar:
if (LOG.isDebugEnabled()) {
// Argument expression evaluated only when DEBUG is enabled
LOG.debug("Request coming from {}, parameters: {}", myInetAddress,
Arrays.toString(veryLongParamArray);
}
Här LOG.debug()
med den dyra Arrays.toString(Obect[])
beräkningen endast när DEBUG
faktiskt är aktiverat.
Fallgrop - Strängkoncentration i en slinga skalas inte
Betrakta följande kod som en illustration:
public String joinWords(List<String> words) {
String message = "";
for (String word : words) {
message = message + " " + word;
}
return message;
}
Tyvärr är den här koden ineffektiv om words
är lång. Roten till problemet är detta uttalande:
message = message + " " + word;
För varje loop-iteration skapar detta uttalande en ny message
innehåller en kopia av alla tecken i den ursprungliga message
där extra tecken bifogas. Detta genererar många tillfälliga strängar och gör mycket kopiering.
När vi analyserar joinWords
, antar vi att det finns N-ord med en genomsnittlig längd på M, finner vi att O (N) tillfälliga strängar skapas och O (MN 2 ) -tecken kopieras i processen. N 2- komponenten är särskilt oroande.
Det rekommenderade tillvägagångssättet för den här typen av problem 1 är att använda en StringBuilder
istället för strängkoppling enligt följande:
public String joinWords2(List<String> words) {
StringBuilder message = new StringBuilder();
for (String word : words) {
message.append(" ").append(word);
}
return message.toString();
}
Analysen av joinWords2
måste ta hänsyn till omkostnaderna för att "växa" StringBuilder
som innehåller byggarens karaktärer. Det visar sig dock att antalet nya objekt som skapats är O (logN) och att antalet kopierade tecken är O (MN) -tecken. Det senare inkluderar tecken som kopieras i det sista toString()
.
(Det kan vara möjligt att ställa in detta ytterligare genom att skapa StringBuilder
med rätt kapacitet till att börja med. Den totala komplexiteten förblir dock densamma.)
När joinWords
till den ursprungliga joinWords
metoden visar det sig att det kritiska uttalandet optimeras av en typisk Java-kompilator till något liknande:
StringBuilder tmp = new StringBuilder();
tmp.append(message).append(" ").append(word);
message = tmp.toString();
Java-kompilatorn "lyfter" dock inte StringBuilder
ur slingan, som vi gjorde för hand i koden för joinWords2
.
Referens:
1 - I Java 8 och senare kan klassen Joiner
användas för att lösa detta problem. Det är emellertid inte vad detta exempel verkligen ska handla om .
Fallgrop - Att använda "nytt" för att skapa primitiva omslagstillstånd är ineffektivt
Java-språket låter dig använda new
att skapa instanser Integer
, Boolean
och så vidare, men det är i allmänhet en dålig idé. Det är bättre att antingen använda autoboxing (Java 5 och senare) eller metoden valueOf
.
Integer i1 = new Integer(1); // BAD
Integer i2 = 2; // BEST (autoboxing)
Integer i3 = Integer.valueOf(3); // OK
Anledningen till att användning av new Integer(int)
uttryckligen är en dålig idé är att det skapar ett nytt objekt (såvida inte optimerat av JIT-kompilatorn). Däremot, när autoboxing eller ett uttryckligt valueOf
samtal används, kommer Java-runtime att försöka återanvända ett Integer
objekt från en cache av tidigare existerande objekt. Varje gång runtime har en "cache" -cache, undviker det att skapa ett objekt. Detta sparar också högminne och reducerar GC-omkostnader orsakade av föremålskämning.
Anmärkningar:
- I nya Java-implementationer implementeras autoboxning genom att ringa
valueOf
, och det finns cachar förBoolean
,Byte
,Short
,Integer
,Long
ochCharacter
. - Cachebeteendet för integraltyperna krävs av Java Language Specification.
Fallgrop - Att kalla "ny sträng (sträng)" är ineffektivt
Att använda new String(String)
att kopiera en sträng är ineffektiv och nästan alltid onödig.
- Strängobjekt är oföränderliga, så det finns inget behov att kopiera dem för att skydda mot förändringar.
- I vissa äldre versioner av Java kan
String
objekt dela stödmatriser med andraString
objekt. I dessa versioner är det möjligt att läcka minne genom att skapa en (liten) understräng av en (stor) sträng och behålla den. Men från Java 7 och framåt,String
uppbackning matriser delas inte.
I avsaknad av några konkreta fördelar är det helt enkelt slöseri med att ringa new String(String)
:
- Att göra kopian tar CPU-tid.
- Kopian använder mer minne som ökar programmets memorandefotavtryck och / eller ökar GC-omkostnader.
- Funktioner som
equals(Object)
ochhashCode()
kan vara långsammare om String-objekt kopieras.
Pitfall - Calling System.gc () är ineffektivt
Det är (nästan alltid) en dålig idé att ringa System.gc()
.
Metoden gc()
för gc()
anger följande:
"Att ringa
gc
metoden tyder på att Java Virtual Machine lägger ned ansträngningar på att återvinna obrukade objekt för att göra det minne de för närvarande upptar tillgängligt för snabb återanvändning. När kontrollen återgår från metodsamtalet har Java Virtual Machine gjort ett bästa försök att återkräva utrymme från alla kasserade objekt. "
Det finns ett par viktiga punkter som kan dras av detta:
Användningen av ordet "föreslår" snarare än (säger) "berättar" betyder att JVM är fritt att ignorera förslaget. Standard JVM-beteende (senaste utgåvor) är att följa förslaget, men detta kan åsidosättas genom att ställa
-XX:+DisableExplicitGC
när du startar JVM.Uttrycket "en bästa ansträngning för att återta utrymme från alla kasserade objekt" innebär att det att ringa
gc
kommer att utlösa en "full" skrämsamling.
Så varför kallar System.gc()
en dålig idé?
Först är det dyrt att köra en fullständig skräpkollektion. En fullständig GC innebär att man besöker och "markerar" varje objekt som fortfarande kan nås; dvs varje föremål som inte är skräp. Om du utlöser detta när det inte finns mycket skräp att samla in, gör GC mycket arbete för relativt liten nytta.
För det andra kan en fullständig soporuppsamling störa "lokalitets" -egenskaperna för de föremål som inte samlas in. Objekt som tilldelas av samma tråd ungefär samtidigt tenderar att tilldelas nära varandra i minnet. Det här är bra. Objekt som tilldelas samtidigt kommer sannolikt att vara relaterade; dvs. hänvisa till varandra. Om din applikation använder dessa referenser, är chansen stor att minnesåtkomst blir snabbare på grund av olika minnes- och sidcacheringseffekter. Tyvärr tenderar en fullständig sopsamling att flytta föremål runt så att objekt som en gång var nära var nu längre isär.
För det tredje kan det hända att du kör en fullständig skrämsamling att göra din ansökan paus tills samlingen är klar. Medan detta händer kommer din ansökan inte att svara.
Faktum är att den bästa strategin är att låta JVM bestämma när GC ska köras, och vilken typ av samling som ska köras. Om du inte stör, väljer JVM en tid och insamlingstyp som optimerar genomströmning eller minimerar GC-pausetider.
I början sa vi "... (nästan alltid) en dålig idé ...". Det finns faktiskt ett par scenarier där det kan vara en bra idé:
Om du implementerar ett enhetstest för någon kod som är känslig för skräpsamling (t.ex. något som involverar slutbehandlare eller svaga / mjuka /
System.gc()
kan det behövas att ringaSystem.gc()
.I vissa interaktiva applikationer kan det finnas speciella tidpunkter där användaren inte bryr sig om det är en skräpuppsamlingspaus. Ett exempel är ett spel där det finns naturliga pauser i "spelet"; t.ex. när du laddar en ny nivå.
Fallgrop - Överanvändning av primitiva omslagstyper är ineffektiva
Tänk på dessa två kodstycken:
int a = 1000;
int b = a + 1;
och
Integer a = 1000;
Integer b = a + 1;
Fråga: Vilken version är effektivare?
Svar: De två versionerna ser nästan identiska ut, men den första versionen är mycket effektivare än den andra.
Den andra versionen använder en representation för siffrorna som använder mer utrymme och förlitar sig på automatisk boxning och auto-unboxing bakom kulisserna. I själva verket är den andra versionen direkt likvärdig med följande kod:
Integer a = Integer.valueOf(1000); // box 1000
Integer b = Integer.valueOf(a.intValue() + 1); // unbox 1000, add 1, box 1001
Jämför detta med den andra versionen som använder int
, det finns uppenbarligen tre extra metodsamtal när Integer
används. När det gäller valueOf
kommer samtalen att skapa och initiera ett nytt Integer
objekt. Allt detta extra boxnings- och unboxningarbete kommer sannolikt att göra den andra versionen till en storleksordning långsammare än den första.
Utöver det fördelar den andra versionen objekt på högen i varje valueOf
samtal. Medan rymdutnyttjandet är plattformspecifikt ligger det troligtvis i området 16 byte för varje Integer
objekt. Däremot behöver int
versionen noll extra högutrymme, förutsatt att a
och b
är lokala variabler.
En annan stor anledning till att primitiven är snabbare än deras boxade ekvivalent är hur deras respektive arraytyper läggs ut i minnet.
Om du tar int[]
och Integer[]
som ett exempel, för int[]
läggs int
värdena sammanhängande i minnet. Men i fallet med ett Integer[]
det inte de värden som läggs ut, utan referenser (pekare) till Integer
, som i sin tur innehåller de faktiska int
värdena.
Förutom att det är en extra indirektnivå, kan detta vara en stor tank när det gäller cache-lokalitet när det upprepas över värdena. Vid en int[]
kunde CPU hämta alla värden i matrisen i sin cache på en gång, eftersom de är sammanhängande i minnet. Men för ett Integer[]
måste CPU potentiellt göra en extra minnehämtning för varje element, eftersom matrisen endast innehåller referenser till de faktiska värdena.
Kort sagt, att använda primitiva omslagstyper är relativt dyra i både CPU- och minnesresurser. Att använda dem onödigt är effektivt.
Fallgrop - Iterating a Map-nycklar kan vara ineffektiva
Följande exempelkod är långsammare än den behöver vara:
Map<String, String> map = new HashMap<>();
for (String key : map.keySet()) {
String value = map.get(key);
// Do something with key and value
}
Det beror på att det krävs en kartuppslagning ( get()
-metoden) för varje nyckel på kartan. Det kan hända att denna uppslagning inte är effektiv (i en HashMap innebär det att man ringer hashCode
på nyckeln, sedan letar upp rätt hink i interna datastrukturer, och ibland till och med att samtal är equals
). På en stor karta kanske detta inte är en trivial overhead.
Det korrekta sättet att undvika detta är att upprepa på kartans poster, som beskrivs i samlingens ämne
Fallgrop - Att använda storlek () för att testa om en samling är tom är ineffektiv.
Java Collections Framework innehåller två relaterade metoder för alla Collection
:
-
size()
returnerar antalet poster i enCollection
och -
isEmpty()
returnerar sant om (och bara om)Collection
är tom.
Båda metoderna kan användas för att testa för uppsamling tomhet. Till exempel:
Collection<String> strings = new ArrayList<>();
boolean isEmpty_wrong = strings.size() == 0; // Avoid this
boolean isEmpty = strings.isEmpty(); // Best
Även om dessa tillvägagångssätt ser lika ut, lagras inte några samlingsimplementeringar storleken. För en sådan samling måste implementeringen av size()
beräkna storleken varje gång den kallas. Till exempel:
- En enkel länkad
java.util.LinkedList
(men intejava.util.LinkedList
) kan behöva korsa listan för att räkna elementen. - Klassen
ConcurrentHashMap
måste summera uppgifterna i alla kartans "segment". - En lat implementering av en samling kan behöva realisera hela samlingen i minnet för att räkna elementen.
Däremot isEmpty()
en isEmpty()
-metod bara testa om det finns minst ett element i samlingen. Detta innebär inte att elementen räknas.
Medan size() == 0
inte alltid är mindre effektiv som är isEmpty()
, är det tänkbart att en korrekt implementerad isEmpty()
är mindre effektiv än size() == 0
. Därför isEmpty()
.
Fallgrop - Effektivitetsproblem med regelbundna uttryck
Regelbunden uttrycksmatchning är ett kraftfullt verktyg (i Java och i andra sammanhang) men det har vissa nackdelar. Ett av dessa som vanliga uttryck tenderar att vara ganska dyrt.
Mönster- och Matcher-instanser bör återanvändas
Tänk på följande exempel:
/**
* Test if all strings in a list consist of English letters and numbers.
* @param strings the list to be checked
* @return 'true' if an only if all strings satisfy the criteria
* @throws NullPointerException if 'strings' is 'null' or a 'null' element.
*/
public boolean allAlphanumeric(List<String> strings) {
for (String s : strings) {
if (!s.matches("[A-Za-z0-9]*")) {
return false;
}
}
return true;
}
Den här koden är korrekt, men den är ineffektiv. Problemet är i matches(...)
. Under huven s.matches("[A-Za-z0-9]*")
detta:
Pattern.matches(s, "[A-Za-z0-9]*")
vilket i sin tur motsvarar
Pattern.compile("[A-Za-z0-9]*").matcher(s).matches()
Pattern.compile("[A-Za-z0-9]*")
samtal analyserar det reguljära uttrycket, analyserar det och konstruerar ett Pattern
som innehåller datastrukturen som kommer att användas av regex-motorn. Detta är en icke-trivial beräkning. Sedan en Matcher
objekt skapas för att linda s
argument. Slutligen kallar vi match()
att göra den faktiska mönstermatchningen.
Problemet är att allt detta arbete upprepas för varje loop-iteration. Lösningen är att omstrukturera koden enligt följande:
private static Pattern ALPHA_NUMERIC = Pattern.compile("[A-Za-z0-9]*");
public boolean allAlphanumeric(List<String> strings) {
Matcher matcher = ALPHA_NUMERIC.matcher("");
for (String s : strings) {
matcher.reset(s);
if (!matcher.matches()) {
return false;
}
}
return true;
}
Observera att javadoc för Pattern
anger:
Instanser av denna klass är oföränderliga och är säkra för användning av flera samtidiga trådar. Instanser av
Matcher
klassen är inte säkra för sådan användning.
Använd inte matchning () när du ska hitta find ()
Anta att du vill testa om en sträng s
innehåller tre eller fler siffror i rad. Du kan uttrycka detta på olika sätt inklusive:
if (s.matches(".*[0-9]{3}.*")) {
System.out.println("matches");
}
eller
if (Pattern.compile("[0-9]{3}").matcher(s).find()) {
System.out.println("matches");
}
Den första är mer kortfattad, men den är också trolig mindre effektiv. Mot bakgrund av det kommer den första versionen att försöka matcha hela strängen mot mönstret. Eftersom ". *" Är ett "girigt" mönster kommer mönstermatcharen troligen att gå "ivrigt" försök till slutet av strängen och backspåra tills den hittar en matchning.
Däremot kommer den andra versionen att söka från vänster till höger och slutar söka så snart den hittar de 3 siffrorna i rad.
Använd effektivare alternativ till vanliga uttryck
Regelbundna uttryck är ett kraftfullt verktyg, men de bör inte vara ditt enda verktyg. Många uppgifter kan göras mer effektivt på andra sätt. Till exempel:
Pattern.compile("ABC").matcher(s).find()
gör samma sak som:
s.contains("ABC")
förutom att det senare är mycket effektivare. (Även om du kan amortera kostnaden för att sammanställa det reguljära uttrycket.)
Ofta är den icke-regexformen mer komplicerad. Exempelvis kan testet som utförs av matches()
kalla den tidigare allAlplanumeric
metoden skrivas om som:
public boolean matches(String s) {
for (char c : s) {
if ((c >= 'A' && c <= 'Z') ||
(c >= 'a' && c <= 'z') ||
(c >= '0' && c <= '9')) {
return false;
}
}
return true;
}
Nu är det mer kod än att använda en Matcher
, men det kommer också att bli betydligt snabbare.
Katastrofal backtracking
(Detta är potentiellt ett problem med alla implementeringar av reguljära uttryck, men vi kommer att nämna det här eftersom det är en fallgrop för Pattern
.)
Tänk på detta (förfalskade) exempel:
Pattern pat = Pattern.compile("(A+)+B");
System.out.println(pat.matcher("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB").matches());
System.out.println(pat.matcher("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC").matches());
Det första println
samtalet kommer snabbt att skriva ut true
. Den andra kommer att skriva ut false
. Så småningom. Faktum är att om du experimenterar med koden ovan ser du att varje gång du lägger till ett A
före C
kommer tiden att fördubblas.
Detta är beteende är ett exempel på katastrofala backspårningar . Mönstermatchningsmotorn som implementerar regex-matchningen försöker fruktlöst alla möjliga sätt som mönstret kan matcha.
Låt oss titta på vad (A+)+B
egentligen betyder. Ytligt verkar det säga "en eller flera A
tecken följt av ett B
värde", men i verkligheten säger det en eller flera grupper, som var och en består av en eller flera A
tecken. Så till exempel:
- 'AB' matchar endast ett sätt: '(A) B'
- 'AAB' matchar två sätt: '(AA) B' eller '(A) (A) B'
- 'AAAB' matchar fyra sätt: '(AAA) B' eller '(AA) (A) B
or '(A)(AA)B
eller '(A) (A) (A) B' - och så vidare
Med andra ord är antalet möjliga matchningar 2 N där N är antalet A
tecken.
Ovanstående exempel är tydligt motstridigt, men mönster som uppvisar denna typ av prestandaegenskaper (dvs. O(2^N)
eller O(N^K)
för ett stort K
) uppstår ofta när dåligt betraktade reguljära uttryck används. Det finns några standardåtgärder:
- Undvik att häcka upprepade mönster inom andra upprepade mönster.
- Undvik att använda för många upprepade mönster.
- Använd repetitioner som inte är backtracking efter behov.
- Använd inte regexer för komplicerade analysuppgifter. (Skriv en rätt tolkare istället.)
Slutligen, se upp för situationer där en användare eller en API-klient kan förse en regex-sträng med patologiska egenskaper. Det kan leda till oavsiktlig eller avsiktlig "förnekande av tjänst".
referenser:
- Regular Expressions- taggen, särskilt http://www.riptutorial.com/regex/topic/259/getting-started-with-regular-expressions/977/backtracking#t=201610010339131361163 och http://www.riptutorial.com/ regex / ämne / 259 / komma-igång-med-Regular-uttryck / 4527 / när-du-ska-inte-use-ordinarie uttryck # t = 201610010339593564913
- "Regex Performance" av Jeff Atwood.
- "Hur man dödar Java med ett regelbundet uttryck" av Andreas Haufler.
Fallgrop - Interningsträngar så att du kan använda == är en dålig idé
När vissa programmerare ser detta råd:
"Testa strängar med
==
är felaktigt (om inte strängarna är internerade)"
deras första reaktion är att internsträngar så att de kan använda ==
. (När allt ==
är snabbare än att ringa String.equals(...)
, är det inte.)
Detta är fel inställning ur ett antal perspektiv:
Bräcklighet
Först och främst kan du bara använda ==
om du vet att alla String
du testar har internerats. JLS garanterar att strängbokstäver i din källkod har internerats. Ingen av de vanliga Java SE API: erna garanterar dock att returnera internerade strängar, förutom String.intern(String)
. Om du bara saknar en källa till String
som inte har internerats kommer din ansökan att vara opålitlig. Denna opålitlighet kommer att manifestera sig som falska negativa snarare än undantag som kan göra det svårare att upptäcka.
Kostnader för att använda 'praktikant ()'
Under huven fungerar interning genom att upprätthålla en hashtabell som innehåller tidigare internerade String
. Någon slags svag referensmekanism används så att interning hash-tabellen inte blir en lagringsläcka. Medan hashtabellen implementeras i inbyggd kod (till skillnad från HashMap
, HashTable
och så vidare) är intern
fortfarande relativt kostsamma vad gäller CPU och minne som används.
Denna kostnad måste jämföras med besparingen vi kommer att få genom att använda ==
istället för equals
. Faktum är att vi inte kommer att bryta jämnt om varje internerad sträng jämförs med andra strängar "några" gånger.
(Bortsett från: de få situationerna där interning är värt, tenderar att handla om att minska minnesfotavtrycket för en applikation där samma strängar upprepas många gånger, och de strängarna har en lång livslängd.)
Påverkan på sopor
Förutom de direkta CPU- och minneskostnaderna som beskrivs ovan påverkar internerade strängar på soporuppsamlarens prestanda.
För versioner av Java före Java 7 hålls internerade strängar i "PermGen" -utrymmet som samlas sällan. Om PermGen måste samlas in, utlöser detta (vanligtvis) en fullständig skräpsamling. Om PermGen-utrymmet fylls helt, kraschar JVM, även om det fanns ledigt utrymme i de vanliga högutrymmena.
I Java 7 flyttades strängpoolen från "PermGen" till den normala högen. Men hashtabellen kommer fortfarande att vara en långlivad datastruktur, vilket kommer att göra att alla internerade strängar är långlivade. (Även om de internerade strängobjekten tilldelades i Eden-rymden skulle de sannolikt marknadsföras innan de samlades in.)
I alla fall kommer interning en sträng således att förlänga dess livslängd relativt en vanlig sträng. Det kommer att öka kostnaderna för avfallssamlingen under JVM: s livstid.
Den andra frågan är att hashtabellen måste använda en svag referensmekanism av något slag för att förhindra att strängen bryter ut läckande minne. Men en sådan mekanism är mer arbete för sopor.
Dessa kostnader för avfallssamling är svåra att kvantifiera, men det råder ingen tvekan om att de existerar. Om du använder intern
utsträckning kan de vara betydelsefulla.
Strängpoolen hashtable storlek
Enligt denna källa , från Java 6 och framåt, implementeras strängpoolen som faststorget hashbord med kedjor för att hantera strängar som hash till samma hink. I tidiga utgåvor av Java 6 hade hashtabellen en (hårddisk) konstant storlek. En inställningsparameter ( -XX:StringTableSize
) lades till som en mid-life-uppdatering till Java 6. I en mid-life-uppdatering till Java 7 ändrades poolens standardstorlek från 1009
till 60013
.
Sammanfattningen är att om du tänker använda intern
intensivt i din kod, är det tillrådligt att välja en version av Java där hashtable-storleken är inställbar och se till att du ställer in storleken på rätt sätt. Annars kan intern
prestanda försämras när poolen blir större.
Interning som en potentiell förnekande av tjänstevektor
Haskodalgoritmen för strängar är välkänd. Om du internerar strängar som tillhandahålls av skadliga användare eller applikationer, kan det här användas som en del av en attack om denial of service (DoS). Om den skadliga agenten ordnar att alla strängar som den tillhandahåller har samma haschkod, kan detta leda till ett obalanserat hashbord och O(N)
-prestanda för intern
... där N
är antalet kolliderade strängar.
(Det finns enklare / effektivare sätt att starta en DoS-attack mot en tjänst. Denna vektor kan dock användas om målet med DoS-attacken är att bryta säkerheten eller att undvika första linje DoS-försvar.)
Fallgrop - Små läser / skriver på obuffrade strömmar är ineffektiva
Tänk på följande kod för att kopiera en fil till en annan:
import java.io.*;
public class FileCopy {
public static void main(String[] args) throws Exception {
try (InputStream is = new FileInputStream(args[0]);
OutputStream os = new FileOutputStream(args[1])) {
int octet;
while ((octet = is.read()) != -1) {
os.write(octet);
}
}
}
}
(Vi har lade utelämnat normal argument kontroll, felrapportering och så vidare, eftersom de inte är relevanta för punkt i detta exempel.)
Om du sammanställer ovanstående kod och använder den för att kopiera en enorm fil, kommer du att märka att den går mycket långsamt. I själva verket kommer det att vara åtminstone ett par storleksordrar långsammare än de vanliga OS-filkopieringsverktygen.
( Lägg till faktiska prestandamätningar här! )
Det huvudsakliga skälet till att exemplet ovan är långsamt (i det stora filfallet) är att det utför en byte-avläsning och en-byte skriver på obuffrade byte-strömmar. Det enkla sättet att förbättra prestanda är att linda in strömmarna med buffrade strömmar. Till exempel:
import java.io.*;
public class FileCopy {
public static void main(String[] args) throws Exception {
try (InputStream is = new BufferedInputStream(
new FileInputStream(args[0]));
OutputStream os = new BufferedOutputStream(
new FileOutputStream(args[1]))) {
int octet;
while ((octet = is.read()) != -1) {
os.write(octet);
}
}
}
}
Dessa små förändringar kommer att förbättra datakopieringshastigheten med minst ett par storleksordrar, beroende på olika plattformsrelaterade faktorer. De buffrade strömförpackningarna får data att läsas och skrivas i större bitar. Båda fallen har buffertar implementerade som byte-matriser.
Med
is
läses data från filen in i bufferten några kilobyte åt gången. Närread()
kallas, kommer implementeringen vanligtvis att returnera en byte från bufferten. Den kommer bara att läsas från den underliggande inmatningsströmmen om bufferten har tömts.Uppträdandet för
os
är analogt. Samtal tillos.write(int)
skriva enstaka byte i bufferten. Data skrivs endast till utgångsströmmen när bufferten är full, eller näros
spolas eller stängs.
Vad sägs om karaktärsbaserade strömmar?
Som ni borde vara med, tillhandahåller Java I / O olika API: er för att läsa och skriva binär- och textdata.
-
InputStream
ochOutputStream
är bas-API: er för strömbaserad binär I / O -
Reader
ochWriter
är bas-API: er för strömbaserad text I / O.
För text I / O är BufferedReader
och BufferedWriter
ekvivalenterna för BufferedInputStream
och BufferedOutputStream
.
Varför gör buffrade strömmar så mycket skillnad?
Det verkliga skälet till att buffrade strömmar hjälper prestanda är att göra med det sätt som en applikation pratar med operativsystemet:
Java-metod i en Java-applikation eller inbyggda procedursamtal i JVM: s ursprungliga runtime-bibliotek är snabba. De tar vanligtvis ett par maskininstruktioner och har minimal prestanda.
Däremot är JVM-runtime-samtal till operativsystemet inte snabba. De involverar något som kallas en "syscall". Det typiska mönstret för en syscall är följande:
- Placera syscall-argumenten i register.
- Utför en SYSENTER-fällinstruktion.
- Fällhanteraren bytte till privilegierat tillstånd och ändrar de virtuella minneskartläggningarna. Sedan skickas den till koden för att hantera det specifika syscallet.
- Systemhanteraren kontrollerar argumenten och tar hand om att det inte får höra att få åtkomst till minne som användarprocessen inte ska se.
- Det syscallspecifika arbetet utförs. Vid
read
syscall kan detta innebära:- kontrollera att det finns data som kan läsas i filbeskrivarens aktuella position
- uppmanar filsystemhanteraren att hämta nödvändig data från disken (eller var den än är lagrad) i buffertcachen,
- kopiering av data från buffertcachen till den JVM-levererade adressen
- justera thstream pointerse filbeskrivarens position
- Återgå från syscall. Detta innebär att ändra VM-mappningar igen och stänga av det privilegierade tillståndet.
Som du kan föreställa dig, genom att utföra en enda syscall kan tusentals maskininstruktioner. Konservativt minst två storleksordrar längre än ett vanligt metodsamtal. (Förmodligen tre eller fler.)
Med tanke på detta är anledningen till att buffrade strömmar gör en stor skillnad att de drastiskt minskar antalet syscalls. Istället för att göra ett syscall för varje read()
-samtal read()
den buffrade inmatningsströmmen en stor mängd data i en buffert efter behov. De flesta read()
samtal på den buffrade strömmen gör några enkla gränskontroller och returnerar en byte
som har lästs tidigare. Liknande resonemang gäller i utgångsströmfallet och även i teckenströmfall.
(Vissa människor tror att buffrad I / O-prestanda kommer från missanpassningen mellan läsförfrågningsstorleken och storleken på ett skivblock, diskens rotations latens och liknande saker. I själva verket använder ett modernt operativsystem ett antal strategier för att säkerställa att ansökan behöver vanligtvis inte vänta på disken. Detta är inte den verkliga förklaringen.)
Är buffrade strömmar alltid en vinst?
Inte alltid. Buffrade strömmar är definitivt en vinst om din ansökan kommer att göra massor av "små" läsningar eller skrivningar. Men om din applikation bara behöver utföra stora läsningar eller skriva till / från en stor byte[]
eller char[]
, kommer buffrade strömmar inte att ge dig några verkliga fördelar. Det kan faktiskt till och med finnas en (liten) prestationsstraff.
Är detta det snabbaste sättet att kopiera en fil i Java?
Nej det är det inte. När du använder Javas strömbaserade API: er för att kopiera en fil, kostar du minst en extra minne-till-minne-kopia av data. Det är möjligt att undvika detta om du använder NIO ByteBuffer
och Channel
API: er. ( Lägg till en länk till ett separat exempel här. )