Kako debagovati JVM podatke u toku rada? HSDB (Hotspot Debugger) od osnova do prakse
HSDB (Hotspot Debugger) je GUI alat za debagovanje ugrađen u SA, koji se može koristiti za debagovanje JVM podataka u toku rada radi rešavanja problema.
Pokretanje HSDB-a
Za različite verzije JDK potrebno je koristiti različitu verziju HSDB-a, inače se lako mogu pojaviti neobjašnjivi problemi poput toga da objekti ne mogu da se skeniraju.
- Mac: i JDK7 i JDK8 mogu koristiti sledeći način
$ sudo java -cp ,:/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDBU stvari, testovi su pokazali da čak i kada se koristi
sa-jdi.jarkoji dolazi uz JDK8 za skeniranje objekata (scanoops), može se desiti da se objekti ne pronađu, ali se to može zaobići drugim sredstvima.
Način pokretanja za JDK11 se donekle razlikuje:
$ /Library/Java/JavaVirtualMachines/jdk-11.0.1.jdk/Contents/Home/bin/jhsdb hsdbU stvari, testovi su pokazali da verzija pokrenuta ovim
HSDB-om podržava manje komandi (na primermem,whatis), zato se trenutno ne preporučuje korišćenje ove verzije.
- Windows:
$ java -classpath "%JAVA_HOME%/lib/sa-jdi.jar" sun.jvm.hotspot.HSDBVerziju koja se pokreće možete dobiti pomoću /usr/libexec/java_home -V.
Ako naiđete na grešku "Unable to locate an executable at /Users/xx/.jenv/versions/1.7/bin/jhsdb (-1)", možete je rešiti prebacivanjem na trenutnu JDK verziju pomoću
Jenv-a.
Podešavanje JVM parametara
HSDB dobro podržava Serial GC, pa se prilikom debagovanja dodaje parametar -XX:+UseSerialGC. Kao alat za debagovanje može se koristiti IDE ili JDB.
Dobijanje ID-a procesa aplikacije
jps pronalazi samo Java procese trenutnog korisnika, a ne sve procese na trenutnom sistemu.
$ jps- Podrazumevano prikazuje pid i naziv klase koja odgovara main metodi.
- -v: ispisuje parametre prosleđene JVM-u.
- -l: ispisuje pun naziv paketa klase koja odgovara main metodi.
Često korišćene CLHSDB komande
universe: prikaz informacija o heap prostoru.scanoops start end [type]: skenira instance tipa type i njegovih podklasa u zadatom prostoru.
scanoopsuHSDBverziji za JDK8 ne može da pronađe objekte, ali putem GUI interfejsaTools -> Object Histogrammožete uneti željeni objekat za pretragu, a zatim dvoklikom dobiti adresu objekta; unutar njega možete i dalje kliknuti nainspectda vidite informacije o objektu.
inspect: prikaz informacija o objektu (OOP) [koristitetools->inspect, unosom adrese objekta dobijate detaljnije informacije].revptrs: obrnuti pokazivači, pronalazi pokazivače koji referenciraju taj objekat.
HSDB GUI interfejs
Vizualizacija steka niti
Kroz Tools → Java Threads prikazuje se spisak svih niti; izborom niti i klikom na Stack Memory otvara se memorijski prikaz njenog steka — svaki frejm metode sa svojim lokalnim promenljivama i njihovim adresama.
Histogram objekata
Tools → Object Histogram — pomoću histograma objekata možemo brzo locirati adresu objekata određenog tipa radi dalje analize:
Count Class description
2345 byte[]
892 java.lang.String
512 java.util.HashMap$Node
...Dvoklik na tip otvara spisak svih instanci tog tipa sa njihovim adresama (npr. 0x76ae0d0a0 java.lang.String), a desno se nalazi i dugme Inspect za detaljnije informacije o objektu.
OOP informacije
Na osnovu adrese objekta možemo u Tools → Inspector dobiti instancu instanceOopDesc objekta na nivou JVM-a, koja obuhvata zaglavlje objekta _mark i _metadata kao i informacije o instanci:
instanceOop @ 0x76ab8f8e0 {
_mark : 0x1
_metadata._compressed_klass : 0x00073008
VMShow.objInt : java.lang.Integer @ 0x76ab8f918
VMShow.basicString : java.lang.String @ 0x76ab4f7a0
...
}Informacije o heap-u
Pomoću Tools → Heap Parameters možemo dobiti informacije o heap-u, koje se mogu kombinovati sa adresom objekta da se utvrdi pozicija objekta (kojoj oblasti — eden, from/to, old — objekat pripada):
Heap Parameters:
Gen 0: [ 0x000000010b600000, 0x000000010d570000 )
eden [ 0x000000010b600000, 0x000000010d1a0000 )
from [ 0x000000010d1a0000, 0x000000010d390000 )
to [ 0x000000010d390000, 0x000000010d570000 )
Gen 1: [ 0x0000000108000000, 0x000000010b600000 )
old [ 0x0000000108000000, 0x000000010b600000 )Lista učitanih klasa
Pomoću Tools → Class Browser možemo dobiti listu svih učitanih klasa — pretragom po imenu dobija se adresa InstanceKlass-a, sadržaj bazena konstanti te klase i dugmad Create .class za generisanje class fajla.
Oblast metapodataka
U HotSpot VM postoji skup objekata namenjenih čuvanju metapodataka, koji obuhvataju:
Objekte iz
Klassfamilije, koji opisuju opšte informacije o tipu [putemOOPinformacija (inspect) može se videtiinstanceKlassobjekat].ConstantPool/ConstantPoolCacheobjekte: svakiInstanceKlassje povezan sa jednimConstantPool-om, koji služi kao bazen konstanti u toku rada za taj tip. Struktura ovog bazena konstanti u osnovi odgovara bazenu konstanti u Class fajlu.


Methodobjekte, koji opisuju opšte informacije o Java metodi, kao što su adresa ulaza u metodu, brojači poziva/petlji itd.ConstMethodobjekti, koji beleže nepromenljive opisne informacije o Java metodi, uključujući naziv metode, modifikatore pristupa metode, bytecode, tabelu brojeva linija, tabelu lokalnih promenljivih itd. Imajte na umu da se bytecode instrukcije nalaze na kraju memorijske oblasticonstMethodOopobjekta.MethodDataobjekti, koji beleže profile informacije o izvršavanju Java metode, na primer da li je neki bytecode unutar metode nikada nije naišao na null, da li neki uslovni skok uvek ide istom granom itd. Ove informacije prikuplja interpretator (a u režimu slojevitog kompajliranja i kod generisan u nižim slojevima kompajliranja), a zatim ih koristi HotSpot Server Compiler za agresivne optimizacije.
Symbolobjekte, koji odgovaraju konstantama tipaJVM_CONSTANT_Utf8u bazenu konstanti Class fajla. Postoji globalni VMSymbolTablekoji upravlja svimSymbolobjektima.Symbolobjekte dele sve Java klase.
Generisanje class fajla
Kada kliknete na create .class ispod odgovarajuće klase, u direktorijumu iz kojeg je pokrenut HSDB možete videti generisani class fajl — pogodno za pregled bytecode-a generisanog dinamičkim proksijem.
Praksa
Analiza oblasti skladištenja objekata
Gde se u sledećem kodu respektivno skladište statičke promenljive i promenljive instance?
public class Main {
private static VMShow StaticVmShow = new VMShow();
private VMShow objVmShow = new VMShow();
public static void main(String[] args) {
fn();
}
private static VMShow fn(){
return new VMShow();
}
}
class VMShow {
private int basicInt = 1;
private Integer objInt = 2;
private static Integer staticInt = 3;
private String basicString = "basicString";
private static String staticString = new String("staticString");
}Prvo, preko histograma objekata možemo pronaći tri VMShow objekta:
Count Class description
3 VMShowKako onda utvrditi kojim promenljivama pripadaju te tri adrese? Prvo tražimo statičke promenljive — u JDK8 one se nalaze u Class objektu, pa možemo potražiti njihove obrnute pokazivače (revptrs); ako je u pitanju java.lang.Class, onda je to statička promenljiva:
hsdb> revptrs 0x76ab8f8e0
Oop for java.lang.Class @ 0x76ab6df28Iz instanceKlass-a klase možemo preko ogledala (java_mirror) pronaći njen java.lang.Class objekat i tako proveriti da li je to class tog objekta.
Kako onda razlikovati promenljive instance od lokalnih promenljivih? Promenljive instance referencira instanca klase (obrnuti pokazivač pokazuje na VMShow @ ...), dok se adrese lokalnih promenljivih nalaze u oblasti steka.
Pošto su obrnuti pokazivači lokalnih promenljivih svi null, kako utvrditi da ih referencira oblast steka? Možemo pogledati vizualizaciju steka niti.

Analiza oblasti skladištenja string literala
public class StringTest {
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = s1 + s2;
String s4 = new String("ab");
System.out.println(s4);
}
}Gde se skladište svi string literali i instance obuhvaćeni gore?
Prvo postavite prekidnu tačku na s2 i pokrenite
HSDBza nadgledanje tog procesa.Otvorite histogram objekata i videćete da postoji samo jedan string objekat
a(ostali literali se još nisu alocirali).Pronađite adresu objekta
au StringTable:
jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUtilities.stringOopToString(s).matches('^(a)')) {print(s + ': ');s.printValueOn(java.lang.System.out); println('')}})"Po potrebi možete promeniti vrednost unutar matches da biste prilagodili poklapanje. Izlaz prikazuje adresu objekta i njegovu vrednost:
0x76ae0d0a0: aMože se videti da je adresa ovog objekta ista kao adresa referencirana u StringTable.
Zatim postavite prekidnu tačku na sout i ponovo pokrenite nadgledanje procesa.
Ponovo pregledajte String vrednosti pomoću histograma objekata.
Ovde ima 5 vrednosti, a ab se pojavljuje 3 puta:
- literal
ab - pošto je s3 ekvivalentno
new StringBuild().append("a").append("b").toString(), kreira se instancaab - s4 kreira instancu
ab
- Ponovo odštampajmo odgovarajuće vrednosti StringTable radi provere:
jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUtilities.stringOopToString(s).matches('^(a|b).?')) {print(s + ': ');s.printValueOn(java.lang.System.out); println('')}})"0x76ae0d0a0: a
0x76ae0d0ce8: b
0x76ae0d4560: abŠta se onda nalazi u bazenu konstanti u toku rada? U stvari su to, kao i u StringTable, reference na te objekte; samo što je StringTable globalno deljen, dok bazen konstanti u toku rada sadrži samo literale te klase. Možemo pogledati preko liste učitanih klasa (Class Browser): pretragom klase StringTest dobija se njen bazen konstanti, u kome se, pored ostalih simbola, vide i reference na string literale te klase.
Analiza String.intern
public class StringInternTest {
public static void main(String[] args) {
String s1 = new String("he") + new String("llo"); // 1
s1.intern(); // 2
String s2="hello"; // 3
System.out.println(s1==s2); // true
String s3 = new String("1") + new String("2"); // 4
String s4 = "12"; // 5
s3.intern(); // 6
System.out.println(s3 == s4); // false
}
}Literali određeni u kompajleru gore su he, llo, hello, 1, 2, 12, ali pre stvarnog izvršavanja naredbe, simbolička referenca ne mora biti razrešena u direktnu referencu, odnosno objekat koji odgovara literalu kreira se tek kada se izvrši naredba (ldc instrukcija).
Prvo pregledajmo bytecode instrukcije preko liste učitanih klasa:
| line | bci | bytecode |
|---|---|---|
| 7 | 0 | new #2 [Class java.lang.StringBuilder] |
| 7 | 3 | dup |
| 7 | 4 | invokespecial #3 [Method void ()] |
| 7 | 7 | new #4 [Class java.lang.String] |
| 7 | 10 | dup |
| 7 | 11 | ldc #5(0) [fast_aldc] |
| 7 | 13 | invokespecial #6 [Method void (java.lang.String)] |
| 7 | 16 | invokevirtual #7 [Method java.lang.StringBuilder append(java.lang.String)] |
| 7 | 19 | new #4 [Class java.lang.String] |
| 7 | 22 | dup |
| 7 | 23 | ldc #8(1) [fast_aldc] |
| 7 | 25 | invokespecial #6 [Method void (java.lang.String)] |
| 7 | 28 | invokevirtual #7 [Method java.lang.StringBuilder append(java.lang.String)] |
| 7 | 31 | invokevirtual #9 [Method java.lang.String toString()] |
| 7 | 34 | astore_1 |
| 8 | 35 | aload_1 |
| 8 | 36 | invokevirtual #10 [Method java.lang.String intern()] |
| 8 | 39 | pop |
| 9 | 40 | ldc #11(2) [fast_aldc] |
| 9 | 42 | astore_2 |
| 10 | 43 | getstatic #12 [Field java.io.PrintStream out] |
| 10 | 46 | aload_1 |
| 10 | 47 | aload_2 |
| 10 | 48 | if_acmpne 55 |
| 10 | 51 | iconst_1 |
| 10 | 52 | goto 56 |
| 10 | 55 | iconst_0 |
| 10 | 56 | invokevirtual #13 [Method void println(boolean)] |
| 12 | 59 | new #2 [Class java.lang.StringBuilder] |
| 12 | 62 | dup |
| 12 | 63 | invokevirtual #13 [Method void println(boolean)] |
| 12 | 66 | new #4 [Class java.lang.String] |
| 12 | 69 | dup |
| 12 | 70 | ldc #14(3) [fast_aldc] |
| 12 | 72 | invokespecial #6 [Method void (java.lang.String)] |
| 12 | 75 | invokevirtual #7 [Method java.lang.StringBuilder append(java.lang.String)] |
| 12 | 78 | new #4 [Class java.lang.String] |
| 12 | 81 | dup |
| 12 | 82 | ldc #15(4) [fast_aldc] |
| 12 | 84 | invokespecial #6 [Method void (java.lang.String)] |
| 12 | 87 | invokevirtual #7 [Method java.lang.StringBuilder append(java.lang.String)] |
| 12 | 90 | invokevirtual #9 [Method java.lang.String toString()] |
| 12 | 93 | astore_3 |
| 13 | 94 | ldc #16(5) [fast_aldc] |
| 13 | 96 | astore #4 |
| 14 | 98 | aload_3 |
| 14 | 99 | invokevirtual #10 [Method java.lang.String intern()] |
| 14 | 102 | pop |
| 15 | 103 | getstatic #12 [Field java.io.PrintStream out] |
| 15 | 106 | aload_3 |
| 15 | 107 | aload #4 |
| 15 | 109 | if_acmpne 116 |
| 15 | 112 | iconst_1 |
| 15 | 113 | goto 117 |
| 15 | 116 | iconst_0 |
| 15 | 117 | invokevirtual #13 [Method void println(boolean)] |
| 16 | 120 | return |
Može se videti da zaista postoji 6 ldc-a, ali ako postavimo prekidnu tačku na prvu liniju, uvidećemo da se nijedan od njih ne nalazi u StringTable (mada je he ovde prisutan, verovatno ga koristi neka druga klasa), a zatim, nakon izvršavanja prve linije, uvidećemo da su he i llo sada tu, ali hello nije.
jseval "st = sa.vm.stringTable;st.stringsDo(function (s) { if (sapkg.oops.OopUtilities.stringOopToString(s).matches('^(he|llo|hello|1|2|12)')) {print(s + ': ');s.printValueOn(java.lang.System.out); println('')}})"0x76ae0c260: he
0x76ae0c3d0: lloIpak, hello objekat i dalje postoji (kreiran sa new), samo još nije u StringTable — histogram ga pokazuje na svojoj adresi:
0x76ae0c680: helloZatim, izvršavanje s1.intern će staviti adresu hello objekta u StringTable:
0x76ae0c680: helloKada se zatim izvrši String s2="hello";, uvidećemo da i dalje postoji samo jedan hello objekat i da oba pokazuju na isti.
Ako zatim postavimo prekidnu tačku na 6, odnosno nakon izvršavanja String s4 = "12";, pošto 12 nije u bazenu string konstanti, kreiraće se nova instanca 12 i bazen string konstanti će je referencirati, pa ćemo videti da sada postoje dva 12:
0x76ae0d8f0: 12
0x76ae0da10: 12Referenca na izvor: https://juejin.cn/post/7072344870374866951
