Maison  >  Article  >  Java  >  Comment le chargeur de classes personnalisé JVM charge-t-il toutes les classes et tous les fichiers JAR sous le classPath spécifié ?

Comment le chargeur de classes personnalisé JVM charge-t-il toutes les classes et tous les fichiers JAR sous le classPath spécifié ?

不言
不言avant
2018-10-08 15:45:282643parcourir

Le contenu de cet article explique comment le chargeur de classe personnalisé JVM charge toutes les classes et tous les fichiers jar sous le classPath spécifié. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer. .

1. Types de chargeurs de classes dans JVM

Du point de vue de la machine virtuelle Java, il n'y a que deux chargeurs de classes différents : le chargeur de classes de démarrage et les autres chargeurs de classes.

1. Boot Class Loader (Boostrap ClassLoader) : ceci est implémenté par C++ et est principalement responsable de l'API principale dans le répertoire JAVA_HOME/lib ou du package jar spécifié par l'option -Xbootclasspath.

2. D'autres chargeurs de classes : implémentés par Java, et leurs objets Class peuvent être trouvés dans la zone des méthodes. Celui-ci est ensuite subdivisé en plusieurs chargeurs

a). Extension ClassLoader : responsable du chargement du répertoire JAVA_HOME/lib/ext, ou par la variable système -Djava.ext.dirs Spécifiez toutes les bibliothèques de classes (jars) dans le chemin spécifié, et les développeurs peuvent utiliser directement le chargeur de classe d'extension. Le chemin spécifié par la variable système java.ext.dirs peut être visualisé via System.getProperty("java.ext.dirs").

b). Application ClassLoader : responsable du chargement des classes et des packages jar dans le répertoire pointé par java -classpath ou -Djava.class.path. Les développeurs peuvent utiliser directement ce chargeur de classe. Il s'agit du chargeur par défaut du programme lorsqu'aucun chargeur de classe personnalisé n'est spécifié.

c). Chargeur de classe personnalisé (User ClassLoader) : pendant l'exécution du programme, les fichiers de classe sont chargés dynamiquement via des sous-classes de java.lang.ClassLoader, reflétant les caractéristiques de chargement de classe dynamique en temps réel de Java.

La relation hiérarchique de ces quatre chargeurs de classes est comme indiqué dans la figure ci-dessous.

  

2. Pourquoi avez-vous besoin de personnaliser le chargeur de classe ? Les serveurs d'applications ont de nombreuses applications indépendantes déployées sur eux, et ils ont de nombreuses classes avec le ? même nom mais versions différentes. Pour distinguer les différentes versions de classes, chaque application doit bien entendu disposer de son propre chargeur de classes indépendant, sinon il sera impossible de distinguer lequel est utilisé.

  1. Partage de bibliothèque de classes : chaque application Web peut utiliser sa propre version de jar dans Tomcat. Mais il existe des éléments tels que Servlet-api.jar, des packages natifs Java et des bibliothèques de classes Java personnalisées qui peuvent être partagées entre elles.

  2. Améliorer la classe : le chargeur de classe peut réécrire et écraser la classe lors du chargement de la classe, au cours de laquelle la fonctionnalité de la classe peut être améliorée. Par exemple, utilisez javassist pour ajouter et modifier des fonctions aux classes, ou ajoutez des proxys dynamiques utilisés dans la programmation orientée aspect, ainsi que dans le débogage et d'autres principes.

  3. Remplacement à chaud : mettez à niveau le logiciel pendant que l'application est en cours d'exécution sans redémarrer l'application. Par exemple, mise à jour et remplacement de JSP dans le serveur toccat

  4. 3. Chargeur de classe personnalisé

3.1 Description des méthodes liées à l'implémentation ClassLoader de la classe personnalisée loader

Pour implémenter un chargeur de classe personnalisé, vous devez d'abord hériter de ClassLoader. La classe ClassLoader est une classe abstraite responsable du chargement des objets des classes. Vous devez connaître au moins trois des méthodes du ClassLoader personnalisé : loadClass, findClass, definitionClass.


public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
loadClass
protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
}
 : Lorsque la JVM charge une classe, elle charge la classe via la méthode loadClass() de ClassLoader utilise le parent. Modèle de délégation. Si vous souhaitez modifier le mode de délégation parent, vous pouvez modifier loadClass pour changer la façon dont la classe est chargée. Le modèle de délégation parentale ne sera pas décrit en détail ici.
protected final Class<?> defineClass(String name, byte[] b, int off, int len)throws ClassFormatError
{return defineClass(name, b, off, len, null);
}

findClass : ClassLoader charge les classes via la méthode findClass(). Le chargeur de classes personnalisé implémente cette méthode pour charger les classes requises, telles que les fichiers sous le chemin spécifié, les flux d'octets, etc.
définiClass : définiClass est utilisé dans findClass En appelant le tableau d'octets passé dans un fichier Class, un objet Class peut être généré dans la zone méthode, ce qui signifie que findClass implémente la fonction de chargement de classe.
Publiez un morceau de code source de loadClass dans ClassLoader pour voir le vrai visage...

Description du code source...

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

Traduction C'est probablement

 : Utilisez le nom binaire spécifié pour charger la classe. L'implémentation par défaut de cette méthode trouve la classe dans l'ordre suivant : Appelez la méthode findLoadedClass(String) pour vérifier si cette classe a été chargée. Utilisez le chargeur parent pour appeler la méthode loadClass(String). Si le chargeur parent est Null, le chargeur de classe charge le chargeur intégré de la machine virtuelle et appelle la méthode findClass(String) pour charger la classe. trouvé avec succès selon les étapes ci-dessus et que la valeur du paramètre de résolution reçu par cette méthode est vraie, puis appelez la méthode solveClass(Class) pour traiter la classe. Il est préférable que les sous-classes de ClassLoader remplacent findClass(String) au lieu de cette méthode. Sauf substitution, cette méthode est synchrone (thread-safe) tout au long du processus de chargement par défaut.
/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/

resolveClass:Class载入必须链接(link),链接指的是把单一的Class加入到有继承关系的类树中。这个方法给Classloader用来链接一个类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。否则,这个类将被按照 Java™规范中的Execution描述进行链接。

3.2 自定义类加载器实现

按照3.1的说明,继承ClassLoader后重写了findClass方法加载指定路径上的class。先贴上自定义类加载器。

package com.chenerzhu.learning.classloader;

import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * @author chenerzhu
 * @create 2018-10-04 10:47
 **/
public class MyClassLoader extends ClassLoader {
    private String path;

    public MyClassLoader(String path) {
        this.path = path;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] result = getClass(name);
            if (result == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getClass(String name) {
        try {
            return Files.readAllBytes(Paths.get(path));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

以上就是自定义的类加载器了,实现的功能是加载指定路径的class。再看看如何使用。 

package com.chenerzhu.learning.classloader;
import org.junit.Test;
/**
 * Created by chenerzhu on 2018/10/4.
 */
public class MyClassLoaderTest {
    @Test
    public void testClassLoader() throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("src/test/resources/bean/Hello.class");
        Class clazz = myClassLoader.loadClass("com.chenerzhu.learning.classloader.bean.Hello");
        Object obj = clazz.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}

首先通过构造方法创建MyClassLoader对象myClassLoader,指定加载src/test/resources/bean/Hello.class路径的Hello.class(当然这里只是个例子,直接指定一个class的路径了)。然后通过myClassLoader方法loadClass加载Hello的Class对象,最后实例化对象。以下是输出结果,看得出来实例化成功了,并且类加载器使用的是MyClassLoader。

com.chenerzhu.learning.classloader.bean.Hello@2b2948e2
com.chenerzhu.learning.classloader.MyClassLoader@335eadca

四、类Class卸载

JVM中class和Meta信息存放在PermGen space区域(JDK1.8之后存放在MateSpace中)。如果加载的class文件很多,那么可能导致元数据空间溢出。引起java.lang.OutOfMemory异常。对于有些Class我们可能只需要使用一次,就不再需要了,也可能我们修改了class文件,我们需要重新加载 newclass,那么oldclass就不再需要了。所以需要在JVM中卸载(unload)类Class。
  JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):

  1. 该类所有的实例都已经被GC。

  2. 该类的java.lang.Class对象没有在任何地方被引用。

  3. 加载该类的ClassLoader实例已经被GC。

很容易理解,就是要被卸载的类的ClassLoader实例已经被GC并且本身不存在任何相关的引用就可以被卸载了,也就是JVM清除了类在方法区内的二进制数据。

JVM自带的类加载器所加载的类,在虚拟机的生命周期中,会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象。因此这些Class对象始终是可触及的,不会被卸载。而用户自定义的类加载器加载的类是可以被卸载的。虽然满足以上三个条件Class可以被卸载,但是GC的时机我们是不可控的,那么同样的我们对于Class的卸载也是不可控的。

五、JVM自定义类加载器加载指定classPath下的所有class及jar

经过以上几个点的说明,现在可以实现JVM自定义类加载器加载指定classPath下的所有class及jar了。这里没有限制class和jar的位置,只要是classPath路径下的都会被加载进JVM,而一些web应用服务器加载是有限定的,比如tomcat加载的是每个应用classPath+“/classes”加载class,classPath+“/lib”加载jar。以下就是代码啦...

package com.chenerzhu.learning.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author chenerzhu
 * @create 2018-10-04 12:24
 **/
public class ClassPathClassLoader extends ClassLoader{

    private static Map<String, byte[]> classMap = new ConcurrentHashMap<>();
    private String classPath;

    public ClassPathClassLoader() {
    }

    public ClassPathClassLoader(String classPath) {
        if (classPath.endsWith(File.separator)) {
            this.classPath = classPath;
        } else {
            this.classPath = classPath + File.separator;
        }
        preReadClassFile();
        preReadJarFile();
    }

    public static boolean addClass(String className, byte[] byteCode) {
        if (!classMap.containsKey(className)) {
            classMap.put(className, byteCode);
            return true;
        }
        return false;
    }

    /**
     * 这里仅仅卸载了myclassLoader的classMap中的class,虚拟机中的
     * Class的卸载是不可控的
     * 自定义类的卸载需要MyClassLoader不存在引用等条件
     * @param className
     * @return
     */
    public static boolean unloadClass(String className) {
        if (classMap.containsKey(className)) {
            classMap.remove(className);
            return true;
        }
        return false;
    }

    /**
     * 遵守双亲委托规则
     */
    @Override
    protected Class<?> findClass(String name) {
        try {
            byte[] result = getClass(name);
            if (result == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getClass(String className) {
        if (classMap.containsKey(className)) {
            return classMap.get(className);
        } else {
            return null;
        }
    }

    private void preReadClassFile() {
        File[] files = new File(classPath).listFiles();
        if (files != null) {
            for (File file : files) {
                scanClassFile(file);
            }
        }
    }

    private void scanClassFile(File file) {
        if (file.exists()) {
            if (file.isFile() && file.getName().endsWith(".class")) {
                try {
                    byte[] byteCode = Files.readAllBytes(Paths.get(file.getAbsolutePath()));
                    String className = file.getAbsolutePath().replace(classPath, "")
                            .replace(File.separator, ".")
                            .replace(".class", "");
                    addClass(className, byteCode);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    scanClassFile(f);
                }
            }
        }
    }
    private void preReadJarFile() {
        File[] files = new File(classPath).listFiles();
        if (files != null) {
            for (File file : files) {
                scanJarFile(file);
            }
        }
    }

    private void readJAR(JarFile jar) throws IOException {
        Enumeration<JarEntry> en = jar.entries();
        while (en.hasMoreElements()) {
            JarEntry je = en.nextElement();
            je.getName();
            String name = je.getName();
            if (name.endsWith(".class")) {
                //String className = name.replace(File.separator, ".").replace(".class", "");
                String className = name.replace("\\", ".")
                        .replace("/", ".")
                        .replace(".class", "");
                InputStream input = null;
                ByteArrayOutputStream baos = null;
                try {
                    input = jar.getInputStream(je);
                    baos = new ByteArrayOutputStream();
                    int bufferSize = 1024;
                    byte[] buffer = new byte[bufferSize];
                    int bytesNumRead = 0;
                    while ((bytesNumRead = input.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesNumRead);
                    }
                    addClass(className, baos.toByteArray());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (baos != null) {
                        baos.close();
                    }
                    if (input != null) {
                        input.close();
                    }
                }
            }
        }
    }

    private void scanJarFile(File file) {
        if (file.exists()) {
            if (file.isFile() && file.getName().endsWith(".jar")) {
                try {
                    readJAR(new JarFile(file));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    scanJarFile(f);
                }
            }
        }
    }


    public void addJar(String jarPath) throws IOException {
        File file = new File(jarPath);
        if (file.exists()) {
            JarFile jar = new JarFile(file);
            readJAR(jar);
        }
    }
}

如何使用的代码就不贴了,和3.2节自定义类加载器的使用方式一样。只是构造方法的参数变成classPath了,篇末有代码。当创建MyClassLoader对象时,会自动添加指定classPath下面的所有class和jar里面的class到classMap中,classMap维护className和classCode字节码的关系,只是个缓冲作用,避免每次都从文件中读取。自定义类加载器每次loadClass都会首先在JVM中找是否已经加载className的类,如果不存在就会到classMap中取,如果取不到就是加载错误了。

六、最后

至此,JVM自定义类加载器加载指定classPath下的所有class及jar已经完成了。这篇博文花了两天才写完,在写的过程中有意识地去了解了许多代码的细节,收获也很多。本来最近仅仅是想实现Quartz控制台页面任务添加支持动态class,结果不知不觉跑到类加载器的坑了,在此也趁这个机会总结一遍。当然以上内容并不能保证正确,所以希望大家看到错误能够指出,帮助我更正已有的认知,共同进步。。。

本文的代码已经上传github:https://github.com/chenerzhu/learning/tree/master/classloader

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer