이 기사의 내용은 JVM 사용자 정의 클래스 로더가 지정된 classPath 아래의 모든 클래스와 jar를 로드하는 방법에 대한 것입니다. 필요한 친구가 참고할 수 있기를 바랍니다.
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의 동적 실시간 클래스 로딩 기능을 반영합니다.
이 네 가지 클래스 로더의 계층 관계는 아래 그림과 같습니다.
동일한 이름을 가진 클래스를 구별하려면: Tomcat 애플리케이션 서버에 배포된 독립 애플리케이션이 많고 클래스가 많다고 가정합니다. 이름은 같지만 버전이 다릅니다. 다양한 버전의 클래스를 구별하려면 물론 각 애플리케이션에 자체 독립적인 클래스 로더가 있어야 합니다. 그렇지 않으면 어떤 버전이 사용되는지 구별하는 것이 불가능합니다.
클래스 라이브러리 공유: 각 웹 애플리케이션은 tomcat에서 자체 jar 버전을 사용할 수 있습니다. 그러나 서로 공유할 수 있는 Servlet-api.jar, Java 기본 패키지 및 사용자 정의 추가 Java 클래스 라이브러리와 같은 것들이 있습니다.
Enhance 클래스: 클래스 로더는 Class를 로드할 때 클래스를 다시 작성하고 덮어쓸 수 있으며, 이 동안 클래스 기능이 향상될 수 있습니다. 예를 들어 javassist를 사용하여 클래스에 함수를 추가 및 수정하거나 관점 지향 프로그래밍, 디버깅 및 기타 원칙에 사용되는 동적 프록시를 추가합니다.
핫 교체: 애플리케이션이 실행되는 동안 소프트웨어를 업그레이드할 수 있으며 애플리케이션을 다시 시작할 필요가 없습니다. 예를 들어 toccat 서버에서 JSP 업데이트 및 교체
사용자 정의 클래스 로더를 구현하려면 먼저 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.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
JVM中class和Meta信息存放在PermGen space区域(JDK1.8之后存放在MateSpace中)。如果加载的class文件很多,那么可能导致元数据空间溢出。引起java.lang.OutOfMemory异常。对于有些Class我们可能只需要使用一次,就不再需要了,也可能我们修改了class文件,我们需要重新加载 newclass,那么oldclass就不再需要了。所以需要在JVM中卸载(unload)类Class。
JVM中的Class只有满足以下三个条件,才能被GC回收,也就是该Class被卸载(unload):
该类所有的实例都已经被GC。
该类的java.lang.Class对象没有在任何地方被引用。
加载该类的ClassLoader实例已经被GC。
很容易理解,就是要被卸载的类的ClassLoader实例已经被GC并且本身不存在任何相关的引用就可以被卸载了,也就是JVM清除了类在方法区内的二进制数据。
JVM自带的类加载器所加载的类,在虚拟机的生命周期中,会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象。因此这些Class对象始终是可触及的,不会被卸载。而用户自定义的类加载器加载的类是可以被卸载的。虽然满足以上三个条件Class可以被卸载,但是GC的时机我们是不可控的,那么同样的我们对于Class的卸载也是不可控的。
经过以上几个点的说明,现在可以实现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 중국어 웹사이트의 기타 관련 기사를 참조하세요!