ホームページ  >  記事  >  Java  >  JVM カスタム クラス ローダーは、指定された classPath の下にあるすべてのクラスと jar をどのようにロードしますか?

JVM カスタム クラス ローダーは、指定された classPath の下にあるすべてのクラスと jar をどのようにロードしますか?

不言
不言転載
2018-10-08 15:45:282660ブラウズ

この記事の内容は、JVM カスタム クラス ローダーが指定された classPath の下にあるすべてのクラスと jar をロードする方法についてです。必要な方は参考にしていただければ幸いです。 。

1. JVM のクラス ローダーの種類

Java 仮想マシンの観点から見ると、異なるクラス ローダーは 2 つだけあります。起動クラス ローダーとその他のクラス ローダーです。

1. ブート クラス ローダー (Boostrap ClassLoader): これは c によって実装され、主に JAVA_HOME/lib ディレクトリ内のコア API または -Xbootclasspath オプションで指定された jar パッケージを担当します。

2. その他のクラス ローダー: Java によって実装され、その Class オブジェクトはメソッド領域にあります。これはさらにいくつかのローダーに分割されます

a) 拡張クラスローダー: JAVA_HOME/lib/ext ディレクトリのロードを担当するか、-Djava.ext.dirs システム変数によってすべてのクラス ライブラリ (jar) を指定します。指定されたパスを使用すると、開発者は拡張クラス ローダーを直接使用できます。 java.ext.dirs システム変数で指定されたパスは、System.getProperty("java.ext.dirs") を通じて表示できます。

b). アプリケーション クラスローダー: java -classpath または -Djava.class.path が指すディレクトリにクラスと jar パッケージをロードします。開発者はこのクラスローダーを直接使用できます。これは、カスタム クラス ローダーが指定されていない場合のプログラムのデフォルト ローダーです。

c) カスタム クラス ローダー (ユーザー クラスローダー): プログラムの実行中、クラス ファイルは java.lang.ClassLoader のサブクラスを通じて動的にロードされ、Java の動的リアルタイム クラス ロード特性を反映します。 。

これら 4 つのクラスローダーの階層関係は次の図に示すようになります。

2. クラスローダーをカスタマイズする必要があるのはなぜですか?

同じ名前のクラスを区別します: Tomcat アプリケーションサーバーでは次のように仮定します。多くの独立したアプリケーションがデプロイされており、同じ名前でバージョンの異なるクラスが多数あります。異なるバージョンのクラスを区別するには、当然のことながら、各アプリケーションに独自の独立したクラス ローダーが必要です。そうしないと、どのクラス ローダーが使用されているかを区別できなくなります。
  1. クラス ライブラリの共有: 各 Web アプリケーションは、Tomcat で独自のバージョンの jar を使用できます。ただし、Servlet-api.jar、Java ネイティブ パッケージ、カスタム追加された Java クラス ライブラリなど、相互に共有できるものもあります。
  2. クラスの強化: クラス ローダーは、Class をロードするときにクラスを書き換えたり上書きしたりすることができ、その間にクラスの機能を強化できます。たとえば、javassist を使用してクラスに関数を追加および変更したり、アスペクト指向プログラミングやデバッグなどの原則で使用される動的プロキシを追加したりできます。
  3. ホット リプレース: アプリケーションを再起動せずに、アプリケーションの実行中にソフトウェアをアップグレードします。たとえば、toccat サーバーでの JSP の更新と置換
  4. #3. カスタム クラス ローダー

3.1 カスタム クラスを実装する ClassLoader の関連メソッドの説明loader

カスタム クラス ローダーを実装するには、まず ClassLoader を継承する必要があります。 ClassLoader クラスは、クラスのオブジェクトをロードする抽象クラスです。カスタム ClassLoader の少なくとも 3 つのメソッド (loadClass、findClass、defineClass) を知っている必要があります。
public Class<?> loadClass(String name) throws ClassNotFoundException {return loadClass(name, false);
protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len)throws ClassFormatError
{return defineClass(name, b, off, len, null);
}

loadClass
: JVM がクラスをロードするとき、loadClass は親委任モードを使用して、ClassLoader のloadClass() メソッドを通じてクラスをロードします。親委任モードを変更したい場合は、loadClass を変更してクラスのロード方法を変更できます。親の委任モデルについては、ここでは詳しく説明しません。

findClass: ClassLoader は、findClass() メソッドを通じてクラスを読み込みます。カスタム クラス ローダーはこのメソッドを実装して、指定されたパス下のファイル、バイト ストリームなどの必要なクラスをロードします。
defineClass: findClass は findClass で使用されます。Class ファイルで渡されたバイト配列を呼び出すことで、メソッド領域に Class オブジェクトを生成できます。つまり、findClass はクラス ロード関数を実装します。
ClassLoader のloadClass ソース コードのセクションを投稿して、本当の姿を確認してください...

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;
    }
}

ソース コードの説明...

/**
* 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
*/

##: 指定されたバイナリ名を使用してクラスをロードします。このメソッドのデフォルトの実装では、次の順序でクラスが検索されます。 findLoadedClass(String) メソッドを呼び出して、このクラスがロードされているかどうかを確認します。ロードされた親が Null の場合、クラス ローダーは仮想マシンの組み込みローダーをロードし、対応するクラスが正常に見つかった場合は findClass(String) メソッドを呼び出します。上記の手順に従い、このメソッドで受け取ったresolveパラメータの値がtrueの場合は、resolveClass(Class)メソッドを呼び出してクラスを処理します。 ClassLoader のサブクラスは、このメソッドの代わりに findClass(String) をオーバーライドすることをお勧めします。オーバーライドしない限り、このメソッドはデフォルトで読み込みプロセス全体を通じて同期 (スレッドセーフ) です。

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

以上がJVM カスタム クラス ローダーは、指定された classPath の下にあるすべてのクラスと jar をどのようにロードしますか?の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はcnblogs.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。