Java – Archivos JAR bloqueados por URLClassLoader en Windows

Un archivo JAR es un contenedor comprimido de clases y recursos asociados, útil para la distribución de aplicaciones, librerías o módulos.  En arquitecturas de tipo modular es común utilizar diferentes cargadores de clases para poder cargar, re-cargar y descargar módulos en forma dinámica y en tiempo de ejecución sin tener que reiniciar la JVM.

Esta forma de mantener una aplicación es conocida popularmente en inglés como “hot-deployment”, y es normalmente utilizada en aplicaciones de servidor. Todo esto implicaría que, de ser necesario, un modulo puede ser reemplazado por una versión actualizada del mismo sin afectar el funcionamiento general de la aplicación.

El java.net.URLClassLoader es la implementación de un cargador de clases incluida en la API desde Java 1.2. Permite cargar buscando desde una dirección URL la cual puede ser un archivo en el sistema(file:), un servidor HTTP(http:), o un archivo JAR(jar:). Para muchos casos con usar o extender la funcionalidad de URLClassLoader ya es suficiente y no requiere un trabajo considerable.

Lamentablemente al utilizar URLClassLoader en la plataforma Windows existe un pequeño y gran inconveniente reportado como bug (ID: 5041014). Cuando se crea un cargador de clases para cargar el contenido de un JAR este último se mantiene abierto durante la vida de la JVM, esto es seguramente por eficiencia ya que luego no se necesitará reabrir el JAR nuevamente si fuera necesario. El sistema operativo mantiene un JAR abierto a través de un descriptor de archivo, naturalmente en Windows un archivo en este estado no puede ser manipulado por otra aplicación, por lo tanto no se puede eliminar ni renombrar (aunque este no es el caso en sistemas basados en UNIX). Como resultado un archivo JAR utilizado queda “bloqueado” hasta que la JVM finalice, incluso si el cargador de clases asociado y las clases cargadas fueran eliminados por el colector de basura. Esto perjudica a todos aquellos proyectos que pretenden hacer uso del mecanismo modular mencionado antes.

Hasta antes de Java 7 no había ningún método en la API de URLClassLoader que permitiera liberar los recursos utilizados, sin embargo si había algunas posibles soluciones, aunque algo incomodas, para superar el problema. Al día de hoy hay varias soluciones, a continuación se mencionan las principales.

Realizando copias auxiliares

Un archivo JAR puede ser copiado a una ubicación temporal y entonces se le asigna a un cargador de clases en esa nueva ubicación. Si se necesita actualizar se copia la nueva versión del JAR a otra ubicación temporal y se elimina\reemplaza el cargador de clases anterior por uno nuevo asociado al nuevo JAR. Esto puede terminar siendo más un problema que una solución, el hecho de tener que realizar copias de cada JAR perjudica la eficiencia de la aplicación, y es un desperdicio de espacio y memoria ya que las antiguas versiones del archivo JAR quedaran abiertas y no se pueden eliminar. Inclusive en caso de que este mecanismo se utilice mucho el sistema podría alcanzar el limite máximo de descriptores de archivo abiertos.

El popular contenedor de servlets Apache Tomcat parece utilizar esta solución y la implementa en una opción llamada “antiJARLocking“. La documentación sobre esta opción advierte sobre como puede afectar el rendimiento durante la carga, sin embargo suele ser necesario activarla en Windows.

Utilizar otro cargador de clases

Existen otras implementaciones de cargadores de clases que no sufren del mismo problema.  Como desventaja muchas de estas no tienen toda la funcionalidad de URLClasLoader, y para algunas personas tener que recurrir a componentes terciarios es una opción poco preferible.

Por ejemplo:  org.eclipse.jdt.apt.core.internal.JarClassLoader.

Implementar un cargador de clases propio

Esto puede hacerse extendiendo la clase ClassLoader. De esta forma tenemos todo el control y podemos encargarnos de cerrar los JAR abiertos. Sin embargo no es una tarea trivial ni tampoco el trabajo que cualquiera quisiera tomarse.

Extender URLClassLoader y tratar de cerrar los JAR abiertos

Esto fue sugerido en el reporte del bug. Mientras no se disponga de un método publico en la API se puede extender URLClassLoader y añadir un método close() donde se utiliza un poco de reflexión para poder acceder a la lista de archivos JAR abiertos y así poder cerrarlos. La reflexión es necesaria porque se necesita el acceso a datos que no son de acceso publico, este es el camino a seguir:

URLClassLoader -> URLClassPath ucp -> ArrayList<Loader> loaders -> JarLoader -> JarFile jar -> jar.close()

Esto puede considerarse un “hack” valido al menos para la JVM de Sun (ahora Oracle). Aquí el código del método close():

...
/**
 * Cerrar todos los JAR abiertos
 */
public void close() {
    try {
        Class clazz = java.net.URLClassLoader.class;
        Field ucp = clazz.getDeclaredField("ucp");    //acceso a URLClassPath
        ucp.setAccessible(true);
        Object sunMiscURLClassPath = ucp.get(this);
        Field loaders = sunMiscURLClassPath.getClass().getDeclaredField("loaders");    //acceso a ArrayList
        loaders.setAccessible(true);
        Object collection = loaders.get(sunMiscURLClassPath);    //colección de JarLoader's
        for (Object sunMiscURLClassPathJarLoader : ((Collection) collection).toArray()) {
            try {
                Field loader = sunMiscURLClassPathJarLoader.getClass().getDeclaredField("jar");    //acceso a JarLoader
                loader.setAccessible(true);
                Object jarFile = loader.get(sunMiscURLClassPathJarLoader);    //acceso a JarFile
                ((JarFile) jarFile).close();    //close()
            } catch (Throwable t) {
                // if we got this far, this is probably not a JAR loader so skip it
            }
        }
    } catch (Throwable t) {
        // probably not a SUN VM
    }
    return;
}
...

Y en caso de tratar con librerías nativas se puede aplicar otro poco de reflexión en el método close(). El camino a seguir:

ClassLoader -> Vector<NativeLibrary> nativeLibraries ->  NativeLibrary ->  finalize()

El trozo de código:

...
clazz = ClassLoader.class;
Field nativeLibraries = clazz.getDeclaredField("nativeLibraries");    //acceso a Vector
nativeLibraries.setAccessible(true);
Vector java_lang_ClassLoader_NativeLibrary = (Vector) nativeLibraries.get(this);    //colección de  NativeLibrary's
for (Object lib : java_lang_ClassLoader_NativeLibrary) {
    Method finalize = lib.getClass().getDeclaredMethod("finalize", new Class[0]);    //acceso a finalize()
    finalize.setAccessible(true);
    finalize.invoke(lib, new Object[0]);    //finalize()
}
...

Sin embargo según algunos reportes no hay garantías de que esto funcione en todo caso.

En este enlace se ve una clase que pertenece a las herramientas de Hinbernate. Es también una extensión de URLClassLoader que realiza la misma tarea mencionada anteriormente pero en una forma más elaborada.

Método close() en la API desde JDK 7

Posiblemente la solución definitiva y tan esperada. Desde el build 48 del JDK7  la clase URLClassLoader posee el método close()  implementando la interface java.lang.Closeable. Con este método se cierran todos los recursos previamente abiertos.

La sintaxis de una URL siguiendo el protocolo JAR es:

  jar:<url>!/[<entry>]

En el siguiente ejemplo se crea un URLClassLoader desde la URL jar:file:/c:/users/user/test/TestJAR.jar!/. Se carga la clase SomeClass.class y se invoca su método main(String[] args) el cual imprime “Hola Mundo” en consola. Luego se cierra el URLClassLoader con el método close() y se intenta eliminar el archivo TestJAR.jar. Fue probado en Windows XP y 7.

import java.net.*;
import java.io.*;

class TestJARClose {

    public static void main(String[] args) {
        try {
            //ubicación del archivo JAR
            String path = "c:\\users\\user\\test\\TestJAR.jar";

            //crear URL siguiendo el protocolo JAR (también funcionaría con el protocolo FILE)
            String jarName = ((new File(path)).toURI()).toString();
            URL url = new URL("jar", "", jarName + "!/");
            System.out.println("* JAR URL: " + url);

            //crear URLClassLoader desde la URL
            URLClassLoader loader = new URLClassLoader(new URL[] {url});
            Class c = loader.loadClass("SomeClass");              //cargar clase
            Method main = c.getMethod("main", String[].class);    //obtener método main(String[] args)
            main.invoke(null, (Object) new String[0]);            //invocar

            //utilizar close() para liberar el JAR
            loader.close();

            //eliminar el JAR
            if (new File(path).delete()) {
                System.out.println("* JAR eliminado.");
            } else {
                System.out.println("* No se puede eliminar JAR.");
            }
        } catch (Exception e) {    //en caso de excepción
            e.printStackTrace();
        }
    }
}

La salida es:

* JAR URL: jar:file:/c:/users/user/test/TestJAR.jar!/
Hola Mundo
* JAR eliminado.

También vale decir que el método close() no tiene porque ser invocado unicamente para finalizar definitivamente el ciclo de vida de un URLClassLoader. Este método también se puede invocar inmediatamente luego de haber cargado los recursos necesarios, pero esto teniendo en cuenta que luego no se podrán cargar más recursos desde ese mismo URLClassLoader, de lo contrario se generaría una ClassNotFoundException.


Más información:
Bug ID: 5041014
* Core Java Technologies Tech Tips – Closing a URLClassLoader

Documentación:
* Java™ Platform, Standard Edition 6 API Specification – URLClassLoader


, , , , , , , ,

  1. #1 por Black Dragon el noviembre 28, 2011 - 5:42 pm

    Hola, justo estoy aprendiendo a programar Java y doy con tu blog, esta increible espero sigas haciendo tus publicaciones. Saludos…

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: