Obsah
1. Základní funkce správce paměti – shrnutí
2. Propustnost versus čas pozastavení běhu aplikací
3. Co lze napravit při výkonnostních problémech?
5. Správce paměti jako úzké hrdlo víceprocesorového systému?
8. Výsledky běhu testovacího příkladu
1. Základní funkce správce paměti – shrnutí
V předchozích částech seriálu o vlastnostech programovacího jazyka Java i jeho virtuálního stroje (JVM) jsme se seznámili s tím, jakým způsobem je implementován správce paměti (GC – Garbage Collector). Připomeňme si, že správce paměti slouží k vyhledávání těch objektů umístěných na haldě, které již nejsou dosažitelné ze žádného vlákna běžící aplikace. Tyto objekty jsou následně odstraněny a ostatní objekty na haldě jsou v případě potřeby přeuspořádány takovým způsobem, aby halda nebyla příliš fragmentovaná, což by znemožnilo alokaci paměti pro větší objekty. Vzhledem k tomu, že vyhledávání potenciálně neživých objektů je z časového i výkonnostního hlediska poměrně náročná činnost, jsou při implementaci správců paměti použity některé heuristické metody. Základní myšlenkou, na které jsou tyto metody založeny, je zjištění, že objekty, které jsou již živé po delší časový úsek, budou s velkou pravděpodobností živé i po provedení dalšího běhu správce paměti. Většinou platí i opak – nově vytvořené objekty budou mít s velkou pravděpodobností jen krátkou dobu života.
Obrázek 1: Dvě hlavní oblasti haldy (young generation a tenured generation) a podrobnější pohled na podoblast young generation.
Při použití výše zmíněné heuristiky je celá halda (heap) rozdělena na dvě oblasti – young generation a tenured generation s tím, že nově vytvářené objekty jsou nejprve umístěny do oblasti young generation. Virtuální stroj jazyka Java se snaží jemu dostupnou (přidělenou) operační paměť pro haldu rozdělit na oblasti young generation a tenured generation na základě vzorců odvozených opět od chování běžných aplikací (navíc se pro haldu může rezervovat větší paměťový úsek, než je skutečně v daný okamžik alokován). V některých případech, především u netypických aplikací či u aplikací, kde záleží na co nejvyšším výkonu či rychlé odezvě aplikace, je však možné při spuštění virtuálního stroje definovat jak alokovanou i maximální velikost haldy, tak i způsob rozdělení haldy na dvě výše zmíněné oblasti. K tomuto účelu slouží několik přepínačů, především volby -Xmx, -Xms, -XX:NewRatio=, -XX:NewSize= a -XX:MaxNewSize=, jejichž význam byl vysvětlen v předchozí části tohoto seriálu.
Obrázek 2: Dvě hlavní oblasti haldy (young generation a tenured generation) a podrobnější pohled na podoblast tenured generation.
2. Propustnost versus čas pozastavení běhu aplikací
V předchozí části seriálu jsme si taktéž popsali základní metody (nabízené přímo běhovým prostředím Javy – JRE) pro monitorování činnosti správce paměti. Pro tento účel je možné při spouštění virtuálního stroje Javy použít volby -verbose:gc, -Xloggc:jméno_souboru a -XX:PrintGCDetails, které se často kombinují s volbou >-XX:PrintGCTimeStamps (přidání informací o čase, v níž byl správce paměti spuštěn). Pomocí těchto voleb lze zjistit, jak často se správce paměti spouští, o kterého správce paměti se jedná (young generation či tenured generation) a taktéž obsazení jednotlivých oblastí na haldě. Pokud dochází k výkonnostním problémům, lze výše uvedené volby kombinovat taktéž s profilerem (-Xprof) s jehož pomocí je možné zjistit, kterou část kódu je vhodné upravit. Ovšem v některých případech nám údaje poskytované s využitím těchto voleb nemusí plně dostačovat.
Obrázek 3: Celková doba běhu testovací aplikace ConcatTest1 (vyjádřená v sekundách) v závislosti na maximální velikosti haldy (vyjádřené v megabajtech).
Jedná se například o situace, kdy dochází k takovým problémům se správou paměti, že aplikace musí být pozastavovaná na dlouhou dobu, což sice nemusí při číselném vyjádření průměrné doby činnosti správce paměti být alarmující, ovšem dlouhé intervaly jsou velmi nepříjemné, a to jak u desktopových aplikací s grafickým uživatelským rozhraním, tak i například u aplikací serverových (vůbec již nemluvě o aplikacích běžících v reálném čase). V praxi se tedy rozlišuje mezi propustností systému (zhruba řečeno jde o zprůměrovaný výkon snižovaný mj. i činností správce paměti) a maximální dobou pozastavení aplikace z důvodu činnosti správce paměti.
Obrázek 4: Počet spuštění správce paměti v závislosti na maximální velikosti haldy (vyjádřené v megabajtech).
Pro detekci takových kritických okamžiků v životě aplikace je možné použít volby -XX:+PrintGCApplicationConcurrentTime a -XX:+PrintGCApplicationStoppedTime. Každou z těchto voleb je samozřejmě možné využít samostatně, společně, nebo je lze kombinovat s ostatními volbami – vše záleží na našich schopnostech při zpracování standardního výstupu generovaného JRE :-). První z těchto voleb slouží pro zapnutí výpisu časových intervalů, v nichž aplikace běží bez přerušení, přesněji řečeno bez přerušení správcem paměti (aplikace samozřejmě může pasivně čekat na jiné události). Pokud tuto volbu použijeme, budou se na standardní výstup vypisovat následující údaje (formát je platný pro poslední oficiální verzi JDK 6 i pro OpenJDK 6):
Application time: 0.0194960 seconds Application time: 0.0035360 seconds Application time: 0.0030870 seconds Application time: 0.0026110 seconds ...
Obrázek 5: Graficky zpracované hodnoty vypisované pomocí volby -XX:PrintGCApplicationConcurrentTime (intervaly kdy aplikace běží) v případě, že je testovací aplikace ConcatTest1 spuštěna s maximální povolenou velikostí haldy na 20MB.
Význam těchto údajů je zřejmý: nejprve aplikace pracovala po dobu 19,4 ms, poté byla na určitou dobu přerušena kvůli správě paměti, následně opět mohla běžet bez přerušení cca 3,5 ms atd. Informaci o tom, na jak dlouho byla aplikace přerušena činností správce paměti, lze získat po zapnutí volby -XX:+PrintGCApplicationStoppedTime:
Total time for which application threads were stopped: 0.0035280 seconds Total time for which application threads were stopped: 0.0017170 seconds Total time for which application threads were stopped: 0.0009440 seconds
Obrázek 6: Graficky zpracované hodnoty vypisované pomocí volby -XX:PrintGCApplicationStoppedTime (intervaly, kdy je aplikace přerušena kvůli GC) v případě, že je testovací aplikace ConcatTest1 spuštěna s maximální povolenou velikostí haldy na 20MB.
Při kombinaci obou výše zmíněných voleb získáme výpis, v němž se oba údaje střídají:
Application time: 0.0196750 seconds Total time for which application threads were stopped: 0.0034630 seconds Application time: 0.0035820 seconds Total time for which application threads were stopped: 0.0017060 seconds Application time: 0.0030790 seconds Total time for which application threads were stopped: 0.0008940 seconds Application time: 0.0026010 seconds Total time for which application threads were stopped: 0.0005410 seconds
Obrázek 7: Graficky zpracované hodnoty vypisované pomocí volby -XX:PrintGCApplicationConcurrentTime, ovšem tentokrát setříděné sestupně.
3. Co lze napravit při výkonnostních problémech?
Pokud se při měření výkonnosti aplikace stane, že jsou zjištěny velké prodlevy způsobené činností správce paměti, nebo že správce paměti výrazným způsobem snižuje celkovou propustnost systému, lze provést (nebo alespoň vyzkoušet) některou z následujících úprav:
- Upravit algoritmus aplikace a/nebo datové struktury, které aplikace používá (to se ovšem lehko řekne, že…).
- Na 64bitových platformách je možné použít již popsané komprimované ukazatele na objekty, které v některých případech mohou poměrně výrazně snížit obsazení haldy. Do jaké míry je tato volba v konkrétní aplikaci užitečná, záleží především na tom, s jakými datovými strukturami se pracuje (resp. v jaké míře se používají reference – typické u grafových struktur, speciálně binárních stromů či vázaných seznamů).
- Velký vliv na výkonnost správce paměti a tím pádem i celé aplikace, má výchozí i maximální velikost haldy nastavovaná pomocí výše uvedených voleb -Xmx a -Xms (v mnoha případech je výhodné, aby obě volby obsahovaly shodné hodnoty). Ovšem maximální velikost haldy by neměla přesáhnout volnou kapacitu fyzické operační paměti, protože by jinak docházelo k odkládání stránek na disk, což naopak výkonnost citelně snižuje (to je již výhodnější obětovat několik procent strojového času na běh správce paměti).
- Kromě specifikace celkové velikosti haldy má velký vliv na výkonnost i způsob rozdělení haldy na young generation a tenured generation pomocí voleb popsaných minule. Na rozdíl od předchozích voleb -Xmx a -Xms, jejichž hodnoty se nastavují celkem snadno, je v tomto případě již většinou nutné provádět profilování a monitorování aplikací běžících ve stejném prostředí, kde mají být aplikace nasazeny a taktéž se shodnými (nebo velmi podobnými) daty (měření výkonnosti aplikace nad testovací databází s 10000 záznamy nám prakticky nic neřekne o tom, jak se aplikace bude chovat nad reálnými daty s 10 miliony záznamy a několika desítkami souběžně zpracovávaných požadavků).
- V případě, že správce paměti zvolený virtuálním strojem jazyka Java z nějakého důvodu potřebám aplikace nevyhovuje (což se stává poměrně často!), lze zvolit jiný typ správce paměti. Jde se o dosti důležitou problematiku popsanou v navazujících kapitolách.
- V neposlední řadě je taktéž možné správci paměti poněkud pomoci využitím velkých stránek (huge pages, large pages). Tuto možnost sice není možné využít na všech systémech, ale na mnohých serverech je možné velké stránky při startu systému povolit a následně je i využívat v JRE při mapování paměti pro haldu. Tato problematika, která se kvůli neustále se zvyšujícím kapacitám operačních pamětí dostává stále častěji do povědomí programátorů i administrátorů, bude popsána v navazující části tohoto seriálu.
Obrázek 8: Při snížení maximální kapacity haldy na pouhé 2MB se zmenšují i intervaly, v nichž testovací aplikace běží bez toho, aby byl její běh přerušován správcem paměti.
4. Sériový správce paměti
Na některých platformách a zejména při spuštění běhového prostředí Javy v režimu client, se používá takzvaný sériový správce paměti. Jak již název napovídá, pracuje tento správce paměti v jednom vláknu, přičemž jeho činnost odpovídá činnosti popsané v minulé a předminulé části tohoto seriálu (rozdělení haldy na dvě části, dva různé typy algoritmů v závislosti na tom, nad kterou částí haldy správce paměti pracuje…). Na počítačích vybavených jedním procesorem s jedním nebo dvěma jádry (popř. s jedním jádrem podporujícím hyperthreading) se ve velké většině případů jedná o nejvhodnější typ správce paměti, který se navíc chová velmi deterministicky (předvídatelně). Současně je sériový správce paměti tím nejjednodušším správcem paměti, který je v současných verzích JDK a OpenJDK implementován; navíc jde i o historicky nejstarší implementaci stále podporovanou i v moderních JDK (tyto dvě vlastnosti přispívají k jeho velké stabilitě). V některých případech se však může ukázat, že tento správce paměti nemusí být pro daný typ aplikace tou nejvhodnější volbou.
Obrázek 9: Hodnoty vypisované pomocí volby -XX:PrintGCApplicationStoppedTime (intervaly, kdy je aplikace přerušena kvůli GC) v případě, že je testovací aplikace ConcatTest1 spuštěna s maximální povolenou velikostí haldy sníženou na pouhé 2MB (lze porovnat s obrázkem číslo 6). Povšimněte si, jak se postupně mění poměr krátkých a dlouhých intervalů.
Sériový správce paměti může způsobovat problémy u těch aplikací, u nichž jsou vyžadovány co nejkratší (popř. navíc predikovatelné) intervaly přerušení běhu aplikace kvůli správě paměti. V tomto případě je namísto sériového správce paměti vhodnější použít buď některý z ostatních standardních správců paměti, nebo použít prozatím sice ještě experimentální, ale velmi dobrý a po teoretické stránce promyšlený správce paměti G1, který se v současných verzích JDK a OpenJDK zapíná dvojicí voleb -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC. Ovšem sériový správce paměti může být nevhodný taktéž i ve chvíli, kdy je aplikace provozována na výkonných serverech s větším množstvím procesorových jader – v současnosti se ostatně stroje s osmi či dokonce šestnácti jádry stávají již vcelku běžnou výbavou serveroven. Na těchto výkonných počítačích se totiž sériový správce paměti může stát úzkým hrdlem celé aplikace, což si budeme ilustrovat v následující kapitole.
Obrázek 10: Hodnoty vypisované pomocí volby -XX:PrintGCApplicationConcurrentTime, ovšem tentokrát setříděné sestupně. Tento graf je platný pro testovací aplikaci ConcatTest1 spuštěnou s maximální povolenou velikostí haldy 2 MB (lze porovnat s obrázek 7).
5. Správce paměti jako úzké hrdlo víceprocesorového systému?
V předchozí kapitole jsme si řekli, že sériový správce paměti (serial garbage collector) nemusí být vždy vhodný při provozování aplikace na počítačích, které jsou osazeny větším množstvím procesorových jader. Důvod je jednoduchý: sériový správce paměti je spuštěn v jediném, i když samostatném vlákně, takže vlastně představuje tu část aplikace, která není dobře škálovatelná se zvyšujícím se počtem procesorových jader – zejména v těch případech, kdy jsou použity čtyři procesorová jádra nebo i větší množství jader (pro jednoduchost nyní uvažujme o procesorech podporujících hyperthreading jako o vícejádrových procesorech, což sice není zcela přesné, ale pro první přiblížení dostačující).
Obrázek 11: Jednovláknová aplikace (ConcatTest1) spuštěná na počítači s osmi jádry nedokáže dobře využít všechny možnosti nabízené tímto relativně výkonným strojem.
Představme si, že aplikace provozovaná na virtuálním stroji jazyka Java je optimalizovaná takovým způsobem, aby se dosáhlo co největší propustnosti (throughput) na počítači, v němž je nainstalováno větší množství procesorových jader, například dva procesory s podporou hyperthreadingu. To znamená, že je v takové aplikaci vytvořeno větší množství souběžně pracujících vláken. Ve zcela ideálním případě (každý ideál je ovšem nedosažitelný ;-)) pracují všechna vlákna skutečně samostatně, tj. není zapotřebí je synchronizovat kvůli přístupu k nějakému sdílenému prostředku. Poměrně velké množství algoritmizovatelných problémů je skutečně možné paralelizovat, takže by se mohlo zdát, že zvýšení propustnosti celého systému lze mj. dosáhnout i prostým spuštěním aplikace na počítači s větším množstvím procesorů a že výkon aplikace poroste zhruba lineárně s počtem procesorů (nebo jejich jader) – viz též následující obrázek.
Obrázek 12: Dokonale škálovatelná aplikace, kde sériový správce paměti nevyžaduje žádný výkon CPU. Na horizontální osu je vynesen počet procesorových jader, na vertikální osu míra urychlení (propustnost).
Ovšem pokud je aplikace provozována na virtuálním stroji jazyka Java (JVM) s běžícím sériovým správcem paměti, stává se tento správce úzkým hrdlem celého systému, protože při jeho běhu, přesněji ve druhé části algoritmu Mark and Sweep, musí správce paměti na určitý okamžik pozastavit všechna ostatní vlákna aplikace. I v případě, že celý cyklus správy paměti vyžaduje pouze 5% nebo 10% výkonu počítače, má i tato relativně malá část aplikace velký dopad na její škálovatelnost – v této situaci se tedy uplatňuje Amdahlův zákon (i když je pravda, že jsme si celou problematiku poněkud zjednodušili).
Obrázek 13: Již ve chvíli, kdy sériový správce paměti vyžaduje pouhých 5% výkonu, klesá dosti výrazně propustnost celé aplikace. Například pro počítač s 20 jádry není urychlení dvacetinásobné, ale pouze desetinásobné.
6. Paralelní správci paměti
Naštěstí však nejsme v současných verzích JDK a OpenJDK omezeni pouze na použití sériového správce paměti. Namísto něho můžeme v případě potřeby použít další tři typy správců – paralelního správce paměti (ParallelGC), takzvaného souběžně pracujícího správce typu Mark and Sweep (Concurrent Mark and Sweep – CMS) a taktéž správce paměti G1, který se v současnosti nachází v testovací fázi vývoje. U paralelního správce paměti lze zvolit, zda má jeho paralelní část běžet pouze nad oblastí young generation nebo i nad oblastí tenured generation a u všech správců se navíc může zvolit (a je to i v mnoha případech vhodné), v kolika vláknech tito správci poběží. Podrobnějším popisem algoritmů těchto správců paměti se budeme zabývat příště, dnes si však již řekneme, jaké volby lze pro jejich spuštění použít. Pro úplnost začněme sériovým správcem paměti. Ten se vybírá následujícím způsobem:
java -XX:+UseSerialGC ConcatTest1
Paralelní správce paměti běžící nad oblastí young generation se zapíná takto:
java -XX:+UseParallelGC ConcatTest1
Pokud má paralelní část správce paměti pracovat i nad oblastí tenured generation, lze využít volbu:
java -XX:+UseParallelOldGC ConcatTest1
Obrázek 14: Stav, kdy sériový správce paměti vyžaduje již 10% výkonu, by byl již pro větší počet procesorových jader neakceptovatelný.
Správce paměti CMS není možné použít současně s předchozími typy správců paměti, starší verze JDK však na tuto chybu uživatele neupozorňovaly. V každém případě, pokud je vhodné použít CMS (například při požadavcích na rychlé odezvy aplikace), používá se tato volba:
java -XX:+UseConcMarkSweepGC ConcatTest1
Jak jsme si již řekli v předchozím textu, někdy je vhodné přímo určit počet vláken, které jsou jednotlivými paralelními správci paměti využity:
java -XX:+UseParallelGC -XX:ParallelGCThreads=4 ConcatTest1 java -XX:+UseParallelOldGC -XX:ParallelGCThreads=4 ConcatTest1 java -XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=4 ConcatTest1
Obrázek 15: Vliv sériového správce paměti si můžeme zobrazit i jinak. Na tomto grafu jsou na horizontální osu vyneseny požadavky správce paměti na výpočetní výkon (od 0% do 100%) a na vertikální ose skutečné urychlení jinak dokonale škálovatelné aplikace běžící na počítači se šestnácti procesory.
7. Testovací příklad
Pro otestování funkce správců paměti na počítači s větším množstvím procesorů jsem vytvořil velmi jednoduchou modifikaci našeho původního příkladu ConcatTest1. Modifikovaná verze nazvaná ConcurrentConcatenationTest po svém spuštění vytvoří n vláken (n lze zadat jako parametr, jinak je zvolena hodnota 8), kde každé vlákno samostatně provádí paměťově náročnou konkatenaci řetězce. Při startu vlákna, při dokončení výpočtu (konkatenace řetězce) i před ukončením vlákna se na standardní výstup vypíše číslo (ID) daného vlákna a informace o tom, v jakém stavu se vlákno nachází:
class ConcatenationThread extends Thread { private static final int DEFAULT_LOOP_COUNT = 10000; private int threadId; public ConcatenationThread(int threadId) { this.threadId = threadId; } public String createString() { String str = ""; for (int i = 0; i < DEFAULT_LOOP_COUNT; i++) { str += i + " "; } return str; } @Override public void run() { System.out.format("Thread #%d started\n", this.threadId); System.out.format("Thread #%d: string length %d\n", this.threadId, createString().length()); System.out.format("Thread #%d finished\n", this.threadId); } } public class ConcurrentConcatenationTest { private static final int DEFAULT_THREAD_COUNT = 8; public static void main(String[] args) { int threadCount = args.length == 0 ? DEFAULT_THREAD_COUNT : Integer.parseInt(args[0]); for (int i = 0; i < threadCount; i++) { new ConcatenationThread(i).start(); } System.out.println("Main thread finished"); } }
Obrázek 16: Testovací příklad ConcurrentConcatenationTest spuštěný na počítači s osmi procesorovými jádry již využívá možnosti počítače lépe než program ConcatTest1.
8. Výsledky běhu testovacího příkladu
Základní vlastnost paralelních správců paměti – tj. propustnost aplikace – si můžeme jednoduše otestovat tak, že budeme spouštět výše uvedený demonstrační příklad s různými správci paměti a s různým počtem vláken vyhrazených pro tyto správce. Skript, který zajistí vytvoření souborů se statistickými daty, je poměrně jednoduchý (povšimněte si, že lze specifikovat i maximální velikost paměti vyhrazené pro haldu):
#!/bin/bash # kolik vlaken ma byt vyhrazeno GC MIN_GC_THREADS=1 MAX_GC_THREADS=16 # velikost haldy v megabajtech HEAP_SIZE=5 function run_java () { # nechceme volat interni prikaz time z BASHe! echo "JVM parameters: $1" /usr/bin/time -o $2 -a -f "%U" java -Xmx${HEAP_SIZE}M $1 ConcurrentConcatenationTest } # seriovy spravce pameti run_java "-XX:+UseSerialGC" _time_serial.txt # paralelni spravce pameti bezici nad young generation for par in `seq $MIN_GC_THREADS $MAX_GC_THREADS`; do run_java "-XX:+UseParallelGC -XX:ParallelGCThreads=${par}" _time_parallel_young.txt done # paralelni spravce pameti bezici nad young i tenured generation for par in `seq $MIN_GC_THREADS $MAX_GC_THREADS`; do run_java "-XX:+UseParallelOldGC -XX:ParallelGCThreads=${par}" _time_parallel_old_gc.txt done # spravce pameti typu Concurrent Mark and Sweep for par in `seq $MIN_GC_THREADS $MAX_GC_THREADS`; do run_java "-XX:+UseConcMarkSweepGC -XX:ParallelGCThreads=${par}" _time_concurrent.txt done
Výsledky bude pravděpodobně nejlepší si zobrazit graficky. V prvním grafu jsou ukázány časy běhu testovací aplikace v případě, že je maximální velikost haldy nastavena na 20MB a počet vláken pro správce paměti se pohybuje od 1 do 16 (vše na osmijádrovém systému!). Povšimněte si, že s rostoucím počtem vláken vytvořených pro správce paměti (nikoli pro výkonnou část aplikace) ve skutečnosti od určité poměrně nízké hranice klesá propustnost, tj. prodlužuje se celkový čas běhu aplikace:
Při pohledu na předchozí graf by se mohlo zdát, že vždy jasně vyhrává paralelní správce paměti. Ve skutečnosti tomu však tak nemusí být vždycky. Podívejme se, co se stane, když se velikost haldy sníží na pouhých 5MB. Zde se dostáváme do situace (zde uměle vytvořené, ovšem v praxi taktéž nastávající), že se zmenšují oblasti, nad kterými správci paměti pracují a jednotlivá vlákna si tak spíše překážejí (toto je ostatně jeden z případů, kdy starý dobrý sériový správce paměti může být výkonnější než mnohem složitější správci paralelní):
To, jakým způsobem lze tyto (a mnohé další) problémy řešit, se budeme zabývat příště.
9. Odkazy na Internetu
- První graf z osmé kapitoly v plném rozlišení
java-11-graf1 - Druhý graf z osmé kapitoly v plném rozlišení
java-11-graf2 - Všechny zdrojové soubory a skripty použité v dnešním článku
java11-src - Výsledky používané při tvorbě grafů v dnešním článku
java11-results - Java HotSpot VM Options
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html - Amdahl's law
http://en.wikipedia.org/wiki/Amdahl_law - Garbage collection (computer science)
http://en.wikipedia.org/wiki/Garbage_collection_(computer_science) - Dr. Dobb's | G1: Java's Garbage First Garbage Collector
http://www.drdobbs.com/article/printableArticle.jhtml?articleId=219401061&dept_url=/java/ - Java's garbage-collected heap
http://www.javaworld.com/javaworld/jw-08–1996/jw-08-gc.html - Compressed oops in the Hotspot JVM
http://wikis.sun.com/display/HotSpotInternals/CompressedOops - 32-bit or 64-bit JVM? How about a Hybrid?
http://blog.juma.me.uk/2008/10/14/32-bit-or-64-bit-jvm-how-about-a-hybrid/ - Compressed object pointers in Hotspot VM
http://blogs.sun.com/nike/entry/compressed_object_pointers_in_hotspot - Java HotSpot™ Virtual Machine Performance Enhancements
http://download.oracle.com/javase/7/docs/technotes/guides/vm/performance-enhancements-7.html - Using jconsole
http://download.oracle.com/javase/1.5.0/docs/guide/management/jconsole.html - jconsole – Java Monitoring and Management Console
http://download.oracle.com/javase/1.5.0/docs/tooldocs/share/jconsole.html - Great Computer Language Shootout
http://c2.com/cgi/wiki?GreatComputerLanguageShootout - x86–64
http://en.wikipedia.org/wiki/X86–64 - Physical Address Extension
http://en.wikipedia.org/wiki/Physical_Address_Extension - Java performance
http://en.wikipedia.org/wiki/Java_performance - 1.6.0_14 (6u14)
http://www.oracle.com/technetwork/java/javase/6u14–137039.html?ssSourceSiteId=otncn - Update Release Notes
http://www.oracle.com/technetwork/java/javase/releasenotes-136954.html - Java virtual machine: 4.10 Limitations of the Java Virtual Machine
http://java.sun.com/docs/books/jvms/second_edition/html/ClassFile.doc.html#88659 - Java™ Platform, Standard Edition 7 Binary Snapshot Releases
http://dlc.sun.com.edgesuite.net/jdk7/binaries/index.html - Trying the prototype
http://mail.openjdk.java.net/pipermail/lambda-dev/2010-August/002179.html - Better closures (for Java)
http://blogs.sun.com/jrose/entry/better_closures - Lambdas in Java: An In-Depth Analysis
http://www.infoq.com/articles/lambdas-java-analysis - Class ReflectiveOperationException
http://download.java.net/jdk7/docs/api/java/lang/ReflectiveOperationException.html - Proposal: Indexing access syntax for Lists and Maps
http://mail.openjdk.java.net/pipermail/coin-dev/2009-March/001108.html - Proposal: Elvis and Other Null-Safe Operators
http://mail.openjdk.java.net/pipermail/coin-dev/2009-March/000047.html - Java 7 : Oracle pushes a first version of closures
http://www.baptiste-wicht.com/2010/05/oracle-pushes-a-first-version-of-closures/ - Groovy: An agile dynamic language for the Java Platform
http://groovy.codehaus.org/Operators - Better Strategies for Null Handling in Java
http://www.slideshare.net/Stephan.Schmidt/better-strategies-for-null-handling-in-java - Control Flow in the Java Virtual Machine
http://www.artima.com/underthehood/flowP.html - Java Virtual Machine
http://en.wikipedia.org/wiki/Java_virtual_machine - ==, .equals(), compareTo(), and compare()
http://leepoint.net/notes-java/data/expressions/22compareobjects.html - New JDK7 features
http://openjdk.java.net/projects/jdk7/features/ - Project Coin: Bringing it to a Close(able)
http://blogs.sun.com/darcy/entry/project_coin_bring_close - ClosableFinder source code
http://blogs.sun.com/darcy/resource/ProjectCoin/CloseableFinder.java - Joe Darcy blog about JDK
http://blogs.sun.com/darcy - Java 7 – more dynamics
http://www.baptiste-wicht.com/2010/04/java-7-more-dynamics/ - ArrayList (JDK 1.4)
http://download.oracle.com/javase/1.4.2/docs/api/java/util/ArrayList.html