Să explorăm împreună conceptul de „descărcare de fire” și modul în care o putem analiza eficient.
Vom detalia cum această analiză ne ajută să identificăm problemele și vom prezenta câteva instrumente specifice pe care le putem utiliza în acest scop.
Ce reprezintă un Fir de Execuție?
Un proces este un program activ în memoria calculatorului, gata să fie executat. Acesta poate fi procesat de un singur procesor sau de un ansamblu de procesoare. În memorie, un proces este descris prin informații esențiale, cum ar fi alocările de variabile, gestiunea fișierelor, poziția curentă de execuție, starea registrelor și a semnalelor etc.
Un proces poate fi structurat în mai multe subprocese, numite fire de execuție (threads). Acest mod de lucru permite obținerea paralelismului, unde un proces este divizat în multiple fire, îmbunătățind astfel performanța. Toate firele dintr-un proces împart aceeași zonă de memorie și sunt interdependente.
Importanța Descărcărilor de Fire
În timp ce un proces este activ, putem analiza starea firelor sale de execuție prin intermediul descărcărilor de fire. O descărcare de fire este o „fotografie” a tuturor firelor active la un moment dat în timpul execuției unui program. Aceasta furnizează detalii esențiale despre fiecare fir și starea sa curentă.
O aplicație modernă utilizează frecvent un număr mare de fire. Fiecare fir necesită resurse specifice și realizează sarcini distincte legate de proces. Această abordare poate crește eficiența unei aplicații, deoarece firele pot folosi nucleele CPU disponibile.
Cu toate acestea, există și dezavantaje. Uneori, firele multiple pot avea dificultăți în a se coordona, ducând la blocaje. În astfel de situații, descărcările de fire devin indispensabile pentru a investiga starea firelor noastre.
Descărcarea de Fire în Java
O descărcare de fire JVM oferă o imagine a stării tuturor firelor dintr-un proces la un moment specific. Conține informații despre stiva firului, prezentate sub formă de urmărire a stivei. Aceste date sunt salvate în format text, facilitând revizuirea ulterioară. Analiza descărcărilor de fire ajută la:
- Optimizarea performanței JVM
- Îmbunătățirea performanței aplicațiilor
- Diagnosticarea problemelor, precum blocaje sau conflicte între fire
Cum Generăm Descărcări de Fire
Există multiple metode de a genera descărcări de fire. Mai jos, explorăm câteva instrumente bazate pe JVM, accesibile prin linia de comandă (CLI) sau din directorul /bin al instalației Java (instrumente GUI).
Să le analizăm pe rând.
#1. jStack
Cea mai simplă metodă de a genera o descărcare de fire este cu jStack, un instrument inclus în JVM și accesibil prin linia de comandă. Este necesar PID-ul procesului pentru care se dorește generarea descărcării. Pentru a afla PID-ul, putem utiliza comanda jps, după cum urmează.
jps -l
Comanda jps listează ID-urile tuturor proceselor Java active.
Pe Windows
C:Program FilesJavajdk1.8.0_171bin>jps -l 47172 portal 6120 sun.tools.jps.Jps C:Program FilesJavajdk1.8.0_171bin>
Pe Linux
[[email protected] ~]# jps -l 1088 /opt/keycloak/jboss-modules.jar 26680 /var/lib/jenkins/workspace/kyc/kyc/target/kyc-1.0.jar 7193 jdk.jcmd/sun.tools.jps.Jps 2058 /usr/share/jenkins/jenkins.war 11933 /var/lib/jenkins/workspace/admin-portal/target/portal-1.0.jar [[email protected] ~]#
Aici vedem o listă cu toate procesele Java în desfășurare, incluzând ID-ul VM și numele aplicației. Pentru a genera descărcarea de fire, utilizăm jStack cu flagul –l, care oferă o ieșire detaliată. Putem redirecționa rezultatul către un fișier text.
jstack -l 26680
[[email protected] ~]# jstack -l 26680 2020-06-27 06:04:53 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.221-b11 mixed mode): "Attach Listener" #16287 daemon prio=9 os_prio=0 tid=0x00007f0814001800 nid=0x4ff2 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE Locked ownable synchronizers: - None "logback-8" #2316 daemon prio=5 os_prio=0 tid=0x00007f07e0033000 nid=0x4792 waiting on condition [0x00007f07baff8000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000006ca9a1fc0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None "logback-7" #2315 daemon prio=5 os_prio=0 tid=0x00007f07e0251800 nid=0x4791 waiting on condition [0x00007f07bb0f9000] java.lang.Thread.State: WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x00000006ca9a1fc0> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1081) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:809) at java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1074) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1134) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Locked ownable synchronizers: - None
#2. jvisualvm
jvisualvm este un instrument GUI care ne ajută să monitorizăm și să profilăm aplicațiile Java. Acesta este inclus în JVM și poate fi accesat din directorul /bin al instalației Java. Este intuitiv și ușor de folosit, oferind printre altele și posibilitatea de a obține descărcarea de fire pentru un anumit proces.
Pentru a vizualiza descărcarea de fire pentru un proces specific, dăm click dreapta pe proces și selectăm opțiunea din meniul contextual.
#3. jcmd
jcmd este un utilitar de linie de comandă inclus în JDK, folosit pentru a trimite cereri de diagnosticare către JVM. Funcționează doar pe mașina locală unde rulează aplicația Java. Acesta poate fi folosit pentru a controla înregistrările Java Flight Recorder, diagnosticarea și depanarea aplicațiilor JVM. Putem utiliza comanda Thread.print a jcmd pentru a obține descărcarea de fire a unui anumit proces, specificat prin PID.
Mai jos este un exemplu de utilizare a jcmd:
jcmd 28036 Thread.print
C:Program FilesJavajdk1.8.0_171bin>jcmd 28036 Thread.print 28036: 2020-06-27 21:20:02 Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.171-b11 mixed mode): "Bundle File Closer" #14 daemon prio=5 os_prio=0 tid=0x0000000021d1c000 nid=0x1d4c in Object.wait() [0x00000000244ef000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Unknown Source) at org.eclipse.osgi.framework.eventmgr.EventManager$EventThread.getNextEvent(EventManager.java:403) - locked <0x000000076f380a88> (a org.eclipse.osgi.framework.eventmgr.EventManager$EventThread) at org.eclipse.osgi.framework.eventmgr.EventManager$EventThread.run(EventManager.java:339) "Active Thread: Equinox Container: 0b6cc851-96cd-46de-a92b-253c7f7671b9" #12 prio=5 os_prio=0 tid=0x0000000022e61800 nid=0xbff4 waiting on condition [0x00000000243ee000] java.lang.Thread.State: TIMED_WAITING (parking) at sun.misc.Unsafe.park(Native Method) - parking to wait for <0x000000076f388188> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject) at java.util.concurrent.locks.LockSupport.parkNanos(Unknown Source) at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(Unknown Source) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(Unknown Source) at java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(Unknown Source) at java.util.concurrent.ThreadPoolExecutor.getTask(Unknown Source) at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) at java.lang.Thread.run(Unknown Source) "Service Thread" #10 daemon prio=9 os_prio=0 tid=0x0000000021a7b000 nid=0x2184 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C1 CompilerThread3" #9 daemon prio=9 os_prio=2 tid=0x00000000219f5000 nid=0x1300 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread2" #8 daemon prio=9 os_prio=2 tid=0x00000000219e0000 nid=0x48f4 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread1" #7 daemon prio=9 os_prio=2 tid=0x00000000219df000 nid=0xb314 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "C2 CompilerThread0" #6 daemon prio=9 os_prio=2 tid=0x00000000219db800 nid=0x2260 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Attach Listener" #5 daemon prio=5 os_prio=2 tid=0x00000000219d9000 nid=0x125c waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Signal Dispatcher" #4 daemon prio=9 os_prio=2 tid=0x00000000219d8000 nid=0x834 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Finalizer" #3 daemon prio=8 os_prio=1 tid=0x000000001faf3000 nid=0x36c0 in Object.wait() [0x0000000021eae000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x000000076f390180> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(Unknown Source) - locked <0x000000076f390180> (a java.lang.ref.ReferenceQueue$Lock) at java.lang.ref.ReferenceQueue.remove(Unknown Source) at java.lang.ref.Finalizer$FinalizerThread.run(Unknown Source) "Reference Handler" #2 daemon prio=10 os_prio=2 tid=0x0000000005806000 nid=0x13c0 in Object.wait() [0x00000000219af000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) - waiting on <0x000000076f398178> (a java.lang.ref.Reference$Lock) at java.lang.Object.wait(Unknown Source) at java.lang.ref.Reference.tryHandlePending(Unknown Source) - locked <0x000000076f398178> (a java.lang.ref.Reference$Lock) at java.lang.ref.Reference$ReferenceHandler.run(Unknown Source) "main" #1 prio=5 os_prio=0 tid=0x000000000570e800 nid=0xbf8 runnable [0x0000000000fec000] java.lang.Thread.State: RUNNABLE at java.util.zip.ZipFile.open(Native Method) at java.util.zip.ZipFile.<init>(Unknown Source) at java.util.zip.ZipFile.<init>(Unknown Source) at java.util.zip.ZipFile.<init>(Unknown Source) at org.eclipse.osgi.framework.util.SecureAction.getZipFile(SecureAction.java:307) at org.eclipse.osgi.storage.bundlefile.ZipBundleFile.getZipFile(ZipBundleFile.java:136) at org.eclipse.osgi.storage.bundlefile.ZipBundleFile.lockOpen(ZipBundleFile.java:83) at org.eclipse.osgi.storage.bundlefile.ZipBundleFile.getEntry(ZipBundleFile.java:290) at org.eclipse.equinox.weaving.hooks.WeavingBundleFile.getEntry(WeavingBundleFile.java:65) at org.eclipse.osgi.storage.bundlefile.BundleFileWrapper.getEntry(BundleFileWrapper.java:55) at org.eclipse.osgi.storage.BundleInfo$Generation.getRawHeaders(BundleInfo.java:130) - locked <0x000000076f85e348> (a java.lang.Object) at org.eclipse.osgi.storage.BundleInfo$CachedManifest.get(BundleInfo.java:599) at org.eclipse.osgi.storage.BundleInfo$CachedManifest.get(BundleInfo.java:1) at org.eclipse.equinox.weaving.hooks.SupplementerRegistry.addSupplementer(SupplementerRegistry.java:172) at org.eclipse.equinox.weaving.hooks.WeavingHook.initialize(WeavingHook.java:138) at org.eclipse.equinox.weaving.hooks.WeavingHook.start(WeavingHook.java:208) at org.eclipse.osgi.storage.FrameworkExtensionInstaller.startActivator(FrameworkExtensionInstaller.java:261) at org.eclipse.osgi.storage.FrameworkExtensionInstaller.startExtensionActivators(FrameworkExtensionInstaller.java:198) at org.eclipse.osgi.internal.framework.SystemBundleActivator.start(SystemBundleActivator.java:112) at org.eclipse.osgi.internal.framework.BundleContextImpl$3.run(BundleContextImpl.java:815) at org.eclipse.osgi.internal.framework.BundleContextImpl$3.run(BundleContextImpl.java:1) at java.security.AccessController.doPrivileged(Native Method) at org.eclipse.osgi.internal.framework.BundleContextImpl.startActivator(BundleContextImpl.java:808) at org.eclipse.osgi.internal.framework.BundleContextImpl.start(BundleContextImpl.java:765) at org.eclipse.osgi.internal.framework.EquinoxBundle.startWorker0(EquinoxBundle.java:1005) at org.eclipse.osgi.internal.framework.EquinoxBundle$SystemBundle$EquinoxSystemModule.initWorker(EquinoxBundle.java:190) at org.eclipse.osgi.container.SystemModule.init(SystemModule.java:99) at org.eclipse.osgi.internal.framework.EquinoxBundle$SystemBundle.init(EquinoxBundle.java:272) at org.eclipse.osgi.internal.framework.EquinoxBundle$SystemBundle.init(EquinoxBundle.java:257) at org.eclipse.osgi.launch.Equinox.init(Equinox.java:171) at org.eclipse.core.runtime.adaptor.EclipseStarter.startup(EclipseStarter.java:316) at org.eclipse.core.runtime.adaptor.EclipseStarter.run(EclipseStarter.java:251) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source) at java.lang.reflect.Method.invoke(Unknown Source) at org.eclipse.equinox.launcher.Main.invokeFramework(Main.java:661) at org.eclipse.equinox.launcher.Main.basicRun(Main.java:597) at org.eclipse.equinox.launcher.Main.run(Main.java:1476) "VM Thread" os_prio=2 tid=0x000000001fae8800 nid=0x32cc runnable "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000000005727800 nid=0x3264 runnable "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000000005729000 nid=0xbdf4 runnable "GC task thread#2 (ParallelGC)" os_prio=0 tid=0x000000000572a800 nid=0xae6c runnable "GC task thread#3 (ParallelGC)" os_prio=0 tid=0x000000000572d000 nid=0x588 runnable "GC task thread#4 (ParallelGC)" os_prio=0 tid=0x000000000572f000 nid=0xac0 runnable "GC task thread#5 (ParallelGC)" os_prio=0 tid=0x0000000005730800 nid=0x380 runnable "GC task thread#6 (ParallelGC)" os_prio=0 tid=0x0000000005733800 nid=0x216c runnable "GC task thread#7 (ParallelGC)" os_prio=0 tid=0x0000000005734800 nid=0xb930 runnable "VM Periodic Task Thread" os_prio=2 tid=0x0000000021a8d000 nid=0x2dcc waiting on condition JNI global references: 14 C:Program FilesJavajdk1.8.0_171bin>
#4. JMC
JMC, prescurtarea pentru Java Mission Control, este un instrument GUI open-source, inclus în JDK, utilizat pentru a colecta și analiza datele aplicațiilor Java. Acesta poate fi accesat din directorul /bin al instalației Java. Administratorii și dezvoltatorii îl folosesc pentru a obține informații detaliate despre comportamentul JVM și al aplicațiilor. Permite analiza eficientă a datelor colectate de Java Flight Recorder.
La lansarea JMC, vedem o listă a proceselor Java care rulează pe mașina locală. Se pot realiza și conexiuni la distanță. Pentru un proces specific, putem da click dreapta și selecta „Start Flight Recording”, apoi putem vizualiza descărcările de fire din fila „Threads”.
#5. jconsole
jconsole este un instrument Java Management Extension utilizat pentru gestionarea și monitorizarea aplicațiilor. Acesta oferă operații predefinite pe agentul JMX. Permite detectarea și analizarea urmelor stivei unui program live și poate fi lansat din folderul /bin al instalației Java.
Utilizând jconsole, putem analiza urma stivei fiecărui fir la conectarea la un proces Java în desfășurare. În fila „Threads”, vedem toate firele active. Pentru a detecta un blocaj, selectăm „Detect Deadlock” din partea de jos a ferestrei. Dacă un blocaj este prezent, acesta va fi afișat într-o filă nouă, altfel va apărea mesajul „No Deadlock Detected”.
#6. ThreadMxBean
ThreadMXBean este o interfață de gestionare a sistemului de fire al mașinii virtuale Java, parte a pachetului java.lang.Management. Este folosită în principal pentru a detecta firele blocate și a obține detalii despre acestea.
Interfața ThreadMxBean poate fi utilizată pentru a captura programatic descărcarea de fire. Metoda getThreadMXBean() din ManagementFactory returnează o instanță a interfeței ThreadMXBean, incluzând numărul de fire demon și non-demon active. ManagementFactory este o clasă utilitară pentru obținerea bean-urilor gestionate pentru platforma Java.
private static String getThreadDump (boolean lockMonitors, boolean lockSynchronizers) { StringBuffer threadDump = new StringBuffer (System.lineSeparator ()); ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean (); for (ThreadInfo threadInfo : threadMXBean.dumpAllThreads (lockMonitors, lockSynchronizers)) { threadDump.append (threadInfo.toString ()); } return threadDump.toString (); }
Analiza Manuală a Descărcărilor de Fire
Analiza descărcărilor de fire este esențială pentru a identifica problemele în procesele cu mai multe fire. Probleme precum blocajele, conflictele de blocare și utilizarea excesivă a CPU pot fi rezolvate analizând stările fiecărui fir în descărcare.
Performanța maximă a aplicației poate fi atinsă prin remedierea stării fiecărui fir, după analizarea descărcării de fire.
De exemplu, dacă un proces consumă mult CPU, putem identifica firul care utilizează cel mai mult din acesta. Dacă identificăm un astfel de fir, convertim numărul său LWP într-un număr hexazecimal. Apoi, din descărcarea firului, căutăm firul cu nid egal cu numărul hexazecimal obținut. Urma stivei firului ne va ajuta să localizăm problema. Putem afla ID-ul procesului și al firului folosind comanda de mai jos.
ps -mo pid,lwp,stime,time,cpu -C java
[[email protected] ~]# ps -mo pid,lwp,stime,time,cpu -C java PID LWP STIME TIME %CPU 26680 - Dec07 00:02:02 99.5 - 10039 Dec07 00:00:00 0.1 - 10040 Dec07 00:00:00 95.5
Să analizăm o parte a descărcării de fire. Pentru a obține descărcarea de fire pentru procesul 26680, folosim comanda jstack -l 26680.
[[email protected] ~]# jstack -l 26680 2020-06-27 09:01:29 <strong>Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.221-b11 mixed mode):</strong> "Attach Listener" #16287 daemon prio=9 os_prio=0 tid=0x00007f0814001800 nid=0x4ff2 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE Locked ownable synchronizers: - None . . . . . . . "<strong>Reference Handler</strong>" #2 daemon prio=10 os_prio=0 tid=0x00007f085814a000 nid=0x6840 in Object.wait() [0x00007f083b2f1000] java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:502) at java.lang.ref.Reference.tryHandlePending(Reference.java:191) - locked <0x00000006c790fbd0> (a java.lang.ref.Reference$Lock) at java.lang.ref.Reference$ReferenceHandler.run(Reference.java:153) Locked ownable synchronizers: - None "VM Thread" os_prio=0 tid=0x00007f0858140800 nid=0x683f runnable "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f0858021000 nid=0x683b runnable "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f0858022800 nid=0x683c runnable "GC task thread#2 (ParallelGC)" os_prio=0 tid=0x00007f0858024800 nid=0x683d runnable "GC task thread#3 (ParallelGC)" os_prio=0 tid=0x00007f0858026000 nid=0x683e runnable "VM Periodic Task Thread" os_prio=0 tid=0x00007f08581a0000 nid=0x6847 waiting on condition JNI global references: 1553
Să analizăm componentele unui dump de fir. La o primă vedere, volumul de informație poate părea intimidant. Însă, analizând pas cu pas, înțelegerea devine mai facilă. Prima linie prezintă:
27-06-2020 09:01:29
Descărcare completă a firelor virtuale Java HotSpot™ pe 64 de biți (mod mixt 25.221-b11):
Aceasta indică momentul generării descărcării și informații despre JVM utilizat. Mai jos, găsim lista de fire, primul fiind firul ReferenceHandler.
Analiza Firelor Blocate
Analizând jurnalele de descărcare de fire, se poate observa că pot fi detectate fire cu starea „BLOCKED”, ceea ce încetinește drastic performanța unei aplicații. Identificând firele blocate, putem încerca să extragem informații despre blocările pe care aceste fire încearcă să le obțină. Analiza urmei stivei firului care deține blocarea poate rezolva problema.
[[email protected] ~]# jstack -l 26680 . . . . " DB-Processor-13" daemon prio=5 tid=0x003edf98 nid=0xca waiting for monitor entry [0x000000000825f000] java.lang.Thread.State: <strong>BLOCKED</strong> (on object monitor) at beans.ConnectionPool.getConnection(ConnectionPool.java:102) - waiting to lock <0xe0375410> (a beans.ConnectionPool) at beans.cus.ServiceCnt.