¿Qué es un seguimiento de pila producido por la plataforma TM? Es una imagen amigable para el usuario de los threads y monitores en la máquina virtual Java. Dependiendo de lo compleja que sea nuestra aplicación o applet, un seguimiento de pila puede tener un rango desde las cincuenta líneas hasta los cientos de líneas de diagnóstico.
Sin importar el tamaño del seguimiento de pila, hay unas pocas cosas importantes que nos pueden ayudar a diagnosticar la mayoría de los problemas de software sin importar si somos expertos o nuevos en la plataforma Java.
Hay tres formas populares para generar un seguimiento de pila: enviar una señal a la Máquina Virtual Java (JVM); la máquina virtual java genera un seguimiento de pila por nosotros; o usar herramientas de depuración o llamadas al API.
Sistemas Unix:
Por ejemplo, en la plataforma SolarisTM, podemos usar el comando kill -QUIT process_id, donde process_id es el número de proceso de nuestro programa.
De forma alternativa podemos introducir la secuencia clave <ctrl>\ en la ventana donde se arrancó el programa.
El envío de esta señal instruye a un manejador de señal de la JVM a que imprima recursivamente toda la información de los threads y monitores que hay dentro de la JVM.
Windows 95/NT:
Para generar un seguimiento de pila en plataformas Windows 95 o Windows NT, introducimos esta secuencia <ctrl><break> en la ventana donde se está ejecutando el programa.
También podemos obtener información similar introduciendo el comando where dentro del depurador Java.
Si tenemos éxito al generar un seguimiento de pila, podremos ver algo similar a esto seguimiento de pila.
En la versiones Java 2, los threads que llaman a métodos que resultan en una llamada a código nativo son indicados en el seguimiento de pila.strings core | grep JAVA_HOME
Sin embargo si no hay string de versión, podemos obtener una idea sobre de qué versión proviene este seguimiento de pila. Obviamente, si nosotros mismos hemos generado este seguimiento de pila no debe ser un problema, pero podríamos estar viendo un seguimiento de pila posteado en un grupo de noticias o en un artículo por e-mail.
Primero identificaremos donde está la sección "Registered Monitor Dump" en el seguimiento de pila:
Verificando la existencia de un Alarm monitor en la salida del seguimiento de pila podemos identificar que este seguimiento de pila viene de un thread verde la JVM.
| Clave | Significado |
|---|---|
| R | Thread runnable o ejecutándose |
| S | Thread suspendido |
| CW | Thread esperando en un condición variable |
| MW | Thread esperando un bloqueo de monitor |
| MS | Thread suspendido esperando un bloqueo de monitor |
Normalmente, sólo los threadas en estados R, S, CW o MW deberían aparecer en el seguimiento de pila.
Los monitores se usan para controlar el acceso a código que sólo debería ser ejecutado por un sólo thread a la vez. Monitores se cubren en más detalles en la siguiente sección. Los otros dos estados de threads comunes que podríamos ver son R, threads ejecutables y CW, threads en una condición de estado de espera. Los threadas ejecutables son por definición threads que podrían ser ejecutados o estar ejecutándose en ese momento. En una máquina multi-procesador ejecutándo un sistema operativo realmente multi-procesador es posible que todos los threads ejecutables se estén ejecutando en el mismo momento. Sin embargo es más probable que otros threads ejecutables estén esperando un programador de threads para tener su turno de ejecución.
Podríamos pensar en los threads en una condición de estado de espera como esperando a que ocurra un evento. Frecuentemente un thread aparecerá en el estado CW si está en un Thread.sleep o en una espera sincronizada. En nuestro anterior seguimiento de pila el método main estaba esperando a que un thread se completara y se notificara su finalización. En el seguimiento de pila esto aparecerá como:
El código que creó este seguimiento de fila es este:"main" (TID:0xebc981e0, sys_thread_t:0x26bb0, state:CW) prio=5 at java.lang.Object.wait(Native Method) at java.lang.Object.wait(Object.java:424) at HangingProgram.main(HangingProgram.java:33)
synchronized(t1) {
try {
t1.wait(); //line 33
}catch (InterruptedException e){}
}
En la versión Java 2 las operaciones de monitores, incluyendo nuestra espera aquí, son manejadas por la máquina virtual Java a través de una llamada JNI a sysMonitor. La condición de espera de un thread se mantiene en una cola de espera de monitor especial del objeto que está esperando. Esto explica porqué aunque seamos los únicos esperando por un objeto el código todavía necesita estar sincronizado con el objeto como si estuviera utilizano de hecho el monitor de ese objeto.
Podría ser sencillo imaginar un monitor como un lavadero de coches. En muchos lavaderos de coches, sólo se puede lavar un coche a la vez. En nuestro código Java sólo un thread a la vez puede tener el bloqueo sobre una pieza sincronizada de código. Todos los demás threads esperan en la cola para entrar al código sincronizado como lo hacen los coches para entrar en el lavadero de coches.
Se puede pensar en un monitor como un bloqueo para un objeto, y cada objeto tiene un monitor. Cuando generamos un seguimiento de pila, los monitores se listan como registrados o no registrados. En la mayoría de los casos estos monitores registrados, o monitores del sistema, no deberían ser la causa de nuestro problema de software, pero nos ayudarán a entenderlos y reconocerlos. La siguiente tabla describe los monitores registrados mas comunes:
| Monitor | Descripción |
|---|---|
| utf8 hash table | Bloquea el hashtable de Strings i18N definidos que fueron cargados desde la clase constant pool. |
| JNI pinning lock | Protege las copias de bloques de array a código de métodos nativos. |
| JNI global reference lock | ¡Bloquea la tabla de referencias globales que contiene los valores que necesitan ser liberado explícitamete, y sobrevivirá al tiempo de vida de la llamada del método nativo. |
| BinClass lock | Bloquea el acceso a la lista de clases cargadas y resueltas. La tabla global de lista de clases. |
| Class linking lock | Protege datos de clases cuando se cargan librerías nativas para resolver referencias simbólicas |
| System class loader lock | Asegura que sólo un thread se carga en una clase del sistema a la vez. |
| Code rewrite lock | Protege el código cuando se intenta una optimización. |
| Heap lock | Protege la pila Java durante el manejo de memoria de la pila. |
| Monitor cache lock | Sólo un thread puede tener acceso al monitor cache a la vez este bloqueo asegura la integridad del monitor cache. |
| Dynamic loading lock | Protege los threads verdes de la JVM Unix de la carga de librería compartida stub libdl.so más de uno a la vez. |
| Monitor IO lock | Protege I/O física por ejemplo, abrir y leer. |
| User signal monitor | Controla el acceso al controlador de señal si hay una señal de usuario en un thread verde de la JVM. |
| Child death monitor | Controla accesos al proceso de información de espera cuando usamos llamadas al sistema de ejecución para ejecutar comandos locales en un thread verde de la JVM. |
| I/O Monitor | Controla accesos al fichero descriptor de threadas para eventos poll/select. |
| Alarm Monitor | Controla accesos a un controlador de reloj usado en threads verdes de la JVM para manejar timeouts |
| Thread queue lock | Protege la cola de threads activos. |
| Monitor registry | Sólo un thread puede tener acceso al registro de monitores al mismo tiempo que este bloqueo asegura la integridad de este registro. |
| Has finalization queue lock * | Protege la lista de objetos bloqueados que han sido recolectadas para la basura, y considera la finalización necesaria. Son copiados a la cola Finalize. |
| Finalize me queue lock * | Protege una lista de objetos que pueden ser finalizados por desocupados. |
| Name and type hash table lock * | Protege las tablas de constantes de las JVMs y sus tipos. |
| String intern lock * | Bloquea la hashtable de Strings definidos que fueron cargadas desde la clase constant pool |
| Class loading lock * | Asegura que sólo un thread carga una clase a la vez. |
| Java stack lock * | Protege la lista de segmentos libres de la pila |
Nota: * bloqueo aparecidos sólo en los seguimientos de pre-Java 2.
El propio registro de monitores está protegido por un monitor. Esto significa que el thread al que pertenece un bloqueo es el último thread en usar un monitor. Es como decir que este thread es el thread actual. Como sólo un thread pueden entrar en un bloque sincronizado a la vez, otros threads se ponen a la cola para entrar en el código sincronizado y aparecen con el estado MW. En el volcado del caché de monitores, se denotan como threads "esperando para entrar". En el código de usuario un monitor es llamado a acción siempre que se usa un bloque o método sincronizado.
Cualquier código que espere un objeto o un evento (método que espera) también tiene que estar dentro de un bloque sincronizado. Sin emabrgo, una vez que se llama a este método, se entrega el bloqueo sobre el objeto sincronizado.
Cuando el thread en estado de espera es notificado de un evento hacia el objeto, teine la competencia del acceso exclusivo a ese objeto, y tiene que obtener el monitor. Incluso cuando un thread a enviado un "notify event" a los threads que están esperando, ninguno de estos threads puede obtener realmente le control del monitor bloqueado hasta que el thread notificado haya abandonado el bloque de código sincronizado
Consideremos un problema de la vida real como por ejemplo el Bug ID 4098756. Podemos encontrar más detalles sobre este bus en el JDC Bug Parade. Este bug documenta un problema que ocurre cuando usamos un componente Choice sobre Windows 95.
Cuando el usuario selecciona una de las opciones desde el componente Choice usando el ratón, todo va bien. Sin embargo, cuando el usuario intenta usar una tecla de fleca paramover la lista de opciones, la aplicación Java se congela.
Afortunadamente, este problema es reproducible y había un seguimiento de pila Java para ayudar a corregir el problem. El seguimiento de pila completo está en la página del bug, pero sólo necesitamos enfocarnos en estos dos threads claves:
"AWT-Windows" (TID:0xf54b70,
sys_thread_t:0x875a80,Win32ID:0x67,
state:MW) prio=5
java.awt.Choice.select(Choice.java:293)
sun.awt.windows.WChoicePeer.handleAction(
WChoicePeer.java:86)
"AWT-EventQueue-0" (TID:0xf54a98,sys_thread_t:0x875c20,
Win32ID:0x8f, state:R) prio=5
java.awt.Choice.remove(Choice.java:228)
java.awt.Choice.removeAll(Choice.java:246)
El thread AWT-EventQueue-0 está en estado ejecutable dentro del método remove. Remove está sincronizado, lo que explíca por qué el thread AWT-Windows no puede entrar al método select. El thread AWT-Windows está en estado MW (monitor wait); sin embargo, sin embargo si seguimos el seguimiento de pila, esta situación no cambia aunque el interface gráfico de usuario (GUI) parezca estár congelado.
Esto indica que la llamada a remove nunca retornó. Siguiendo el camino del código hacia la clase ChoicePeer, podemos ver que se está haciendo a un llamada al MFC nativo que no retorna, Es aquí donde está el problema real y es un bug de las clases corazón Java. El código del usuario esta bien.
Ejemplo 2
En este segundo ejemplo investigaremos un bug que al principio parece ser un fallo de Swing pero descubriremos que es debido al hecho que Swing no es seguro ante los threads.
El informa de bug también está disponible en la site JDCm el número del bug es 4098525.
Aquí tenemos un ejemplo del código usado para reproducir este problem. El díalogo modal se crea desde dentro del método JPanel paint.
import java.awt.event.*;
import java.awt.*;
import java.util.*;
import javax.swing.*;
class MyDialog extends Dialog
implements ActionListener {
MyDialog(Frame parent) {
super(parent, "My Dialog", true);
Button okButton = new Button("OK");
okButton.addActionListener(this);
add(okButton);
pack();
}
public void actionPerformed(ActionEvent event) {
dispose();
}
}
public class Tester extends JPanel {
MyDialog myDialog;
boolean firstTime = true;
public Tester (JFrame frame) throws Exception {
super();
myDialog = new MyDialog(frame);
}
void showDialogs() {
myDialog.show();
}
public void paint(Graphics g) {
super.paint(g);
if (firstTime) {
firstTime = false;
showDialogs();
}
}
public static void main(String args[])
throws Exception {
JFrame frame = new JFrame ("Test");
Tester gui = new Tester(frame);
frame.getContentPane().add(gui);
frame.setSize(800, 600);
frame.pack();
frame.setVisible(true);
}
}
Cuando ejecutamos este programa encontramos que se bloquea al principio. Haciendo un seguimiento de pila podremos ver estos threads claves.
El seguimiento de pista que tenemos aquí es ligeramente diferente al que aparece en el informe del bug, pero tienen el mismo efecto. También usamos la versión Java 2 para generar el seguimiento y suministrar la opción -Djava.compiler=NONE cuando ejecutamos el programa para que podams ver los números de línea del fuent. El thread a buscar es el que tiene el estado MW, monitor de espaera que en este caso es el threadAWT-EventQueue-1
"AWT-EventQueue-1" (
TID:0xebca8c20, sys_thread_t:0x376660,
state:MW) prio=6
at java.awt.Component.invalidate(Component.java:1664)
at java.awt.Container.invalidate(Container.java:507)
t java.awt.Window.dispatchEventImpl(Window.java:696)
at java.awt.Component.dispatchEvent(
Component.java:2289)
at java.awt.EventQueue.dispatchEvent(
EventQueue.java:258)
at java.awt.EventDispatchThread.run(
EventDispatchThread.java:68)
Si buscamos está línea en el fichero java/awt/Component.java que está contenido en el archivo src.jar, veremos esto:
public void invalidate() {
synchronized (getTreeLock()) { //line 1664
Es aquío donde nuestra aplicaciónse bloquea, está esperando a que el monitor getTreeLock se libere. La siguiente tarea es encontrar elthread que tiene bloqueado este monitos.
Para ver quién está bloqueando este monitor buscamos en el volcado del cache de Monitores y en este ejemplo podemos ver lo siguiente:
Monitor Cache Dump:
java.awt.Component$AWTTreeLock@EBC9C228/EBCF2408:
owner "AWT-EventQueue-0" ( 0x263850) 3 entries
Waiting to enter:
"AWT-EventQueue-1" (0x376660)
El monitor getTreeLock está actualmente bloqueado en un objeto de una clase interna creada especialmente AWTTreeLock. Este es el código para crear ese bloqueo en el fichero Component.java.
static final Object LOCK = new AWTTreeLock();
static class AWTTreeLock {}
El propietario actual es AWT-EventQueue-0. El thread llamó a nuestro método paint para crear nuesto Dialog modal mediante una llamada a paintComponent. El propio paintComponent fue llamado desde una llamada a update del JFrame.
¿Pero dónde se originó el bloqueo? Bien, ni hay una forma sencilla de encontrar qué parte del marco tiene el bloqueo pero una simple búsqueda de javax.swing.JComponent podremos ver que getTreeLock es llamado dentro del método paintChildren que dejamos en la línea 388.
at Tester.paint(Tester.java:39)
at javax.swing.JComponent.paintChildren(
JComponent.java:388)
El resto del puzzle se coloca junto analizando el método MDialogPeer show. El código del diálogo crea un nuevo ModalThread que es por lo que hemos visto un thread AWT-Modal en la salida del seguimiento de pila, este thread es usado para postear el diálogo. Es cuando el evento de despacha usando AWT-EventQueue-1 que es usado para ser el proxy de despacho de eventos de AWT y es necesario un acceso al monitor getTreeLock y es aquí donde tenemos el bloqueo.
Desafortunadamente el código Swing no está diseñado para ser seguro con los threads por eso la solución en este ejemplo es no crear diálogos modales desde dentro de método paint de Swing. Ya que Swing tiene que hacer cantidad de bloqueos y cálculos; que las partes de un componente ligero que necesitan ser dibujadas deben estar fuertemente advertidas de que no incluyan código sincronizado o código que puede resultar en una llamada sincronizadac como en un diálogo modal, dentro del método paint.
Esto completa la teoria del seguimiento de pila Java, y ahora deberíamos saber qué busar la siguiente vez que veamos un seguimiento de pila. Para ahorrar tiempo, deberíamos hacer uso de la búsqueda de Bugs del JDC para ver si nuestro problema ha sido reportado por alguien más.