>  기사  >  Java  >  JVM 사용자 정의 클래스 로더는 지정된 classPath 아래의 모든 클래스와 jar를 어떻게 로드합니까?

JVM 사용자 정의 클래스 로더는 지정된 classPath 아래의 모든 클래스와 jar를 어떻게 로드합니까?

不言
不言앞으로
2018-10-08 15:45:282634검색

이 기사의 내용은 JVM 사용자 정의 클래스 로더가 지정된 classPath 아래의 모든 클래스와 jar를 로드하는 방법에 대한 것입니다. 필요한 친구가 참고할 수 있기를 바랍니다.

1. JVM의 클래스 로더 유형

Java Virtual Machine의 관점에서 보면 클래스 로더는 시작 클래스 로더와 기타 클래스 로더 두 가지뿐입니다.

1. 부트 클래스 로더(Boostrap ClassLoader): 이는 C++로 구현되며 주로 JAVA_HOME/lib 디렉토리 또는 -Xbootclasspath 옵션으로 지정된 jar 패키지의 핵심 API를 로드하는 일을 담당합니다.

2. 기타 클래스 로더: Java로 구현되며 해당 클래스 객체는 메소드 영역에서 찾을 수 있습니다. 이는 여러 로더로 세분화됩니다(

a). 확장 ClassLoader: JAVA_HOME/lib/ext 디렉토리에 있는 파일을 로드하거나 -Djava.ext.dirs 시스템 변수로 지정됩니다. 경로의 모든 클래스 라이브러리(jar)에 대해 , 개발자는 확장 클래스 로더를 직접 사용할 수 있습니다. java.ext.dirs 시스템 변수에 지정된 경로는 System.getProperty("java.ext.dirs")를 통해 확인할 수 있습니다.

b) 애플리케이션 ClassLoader: java -classpath 또는 -Djava.class.path가 가리키는 디렉토리에 클래스 및 jar 패키지를 로드하는 역할을 합니다. 개발자는 이 클래스 로더를 직접 사용할 수 있습니다. 이는 사용자 정의 클래스 로더가 지정되지 않은 경우 프로그램의 기본 로더입니다.

c) 사용자 정의 클래스 로더(User ClassLoader): 프로그램 실행 중에 클래스 파일은 java.lang.ClassLoader의 하위 클래스를 통해 동적으로 로드되며, 이는 Java의 동적 실시간 클래스 로딩 기능을 반영합니다.

이 네 가지 클래스 로더의 계층 관계는 아래 그림과 같습니다.

    

2. 클래스 로더를 커스터마이즈해야 하는 이유

  1. 동일한 이름을 가진 클래스를 구별하려면: Tomcat 애플리케이션 서버에 배포된 독립 애플리케이션이 많고 클래스가 많다고 가정합니다. 이름은 같지만 버전이 다릅니다. 다양한 버전의 클래스를 구별하려면 물론 각 애플리케이션에 자체 독립적인 클래스 로더가 있어야 합니다. 그렇지 않으면 어떤 버전이 사용되는지 구별하는 것이 불가능합니다.

  2. 클래스 라이브러리 공유: 각 웹 애플리케이션은 tomcat에서 자체 jar 버전을 사용할 수 있습니다. 그러나 서로 공유할 수 있는 Servlet-api.jar, Java 기본 패키지 및 사용자 정의 추가 Java 클래스 라이브러리와 같은 것들이 있습니다.

  3. Enhance 클래스: 클래스 로더는 Class를 로드할 때 클래스를 다시 작성하고 덮어쓸 수 있으며, 이 동안 클래스 기능이 향상될 수 있습니다. 예를 들어 javassist를 사용하여 클래스에 함수를 추가 및 수정하거나 관점 지향 프로그래밍, 디버깅 및 기타 원칙에 사용되는 동적 프록시를 추가합니다.

  4. 핫 교체: 애플리케이션이 실행되는 동안 소프트웨어를 업그레이드할 수 있으며 애플리케이션을 다시 시작할 필요가 없습니다. 예를 들어 toccat 서버에서 JSP 업데이트 및 교체

3. 사용자 정의 클래스 로더

3.1 ClassLoader 사용자 정의 클래스 로더 관련 메소드 설명 구현

사용자 정의 클래스 로더를 구현하려면 먼저 ClassLoader를 상속해야 합니다. ClassLoader 클래스는 클래스의 객체를 로드하는 추상 클래스입니다. 사용자 정의 ClassLoader의 메소드 중 적어도 세 가지(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은 클래스를 로드할 때 ClassLoader의 loadClass() 메서드를 통해 클래스를 로드합니다. loadClass는 상위 위임 모드를 사용합니다. 상위 위임 모드를 변경하려면 loadClass를 수정하여 클래스가 로드되는 방식을 변경할 수 있습니다. 여기서는 부모 위임 모델에 대해 자세히 설명하지 않습니다.
findClass: ClassLoader는 findClass() 메서드를 통해 클래스를 로드합니다. 사용자 정의 클래스 로더는 지정된 경로의 파일, 바이트 스트림 등과 같은 필수 클래스를 로드하기 위해 이 메소드를 구현합니다.
DefinedClass: DefinedClass는 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) 메소드를 호출하여 이 클래스가 로드되었는지 확인합니다. 상위 로더가 다음과 같은 경우 loadClass(String) 메소드를 호출합니다. Null인 경우, 클래스 로더는 가상 머신의 내장 로더를 로드하고 findClass(String).) 메소드를 호출하여 위 단계에 따라 해당 클래스를 성공적으로 찾았으며 이에 의해 수신된 해결 매개변수의 값은 다음과 같습니다. 메소드가 true인 경우, 클래스를 처리하기 위해 해결클래스(클래스) 메소드가 호출됩니다. 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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 cnblogs.com에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제