类加载机制分析

概述

最近在项目中遇到个问题,一次升级依赖之后,发现线上某台机器日志无输出;这种问题通常都是由于log jar冲突导致,查看依赖果然发现项目中同时存在log4j 1.x和log4j2.x jar,问题本身并不复杂,让我好奇的是为什么部分机器都工作正常,部分不正常呢?

jar加载顺序分析

从上面的问题推测,应该是不同机器加载的jar包顺序存在不一致,那么jar包的加载顺序遵循什么样的规则呢?

按照项目的启动方式不一样,在自己接触的项目中,通常有三种不同的启动方式:

  • spring boot项目
  • tomcat容器项目
  • appassembler-maven-plugin项目

spring boot项目

spring boot项目通常都是用spring-boot-maven-plugin插件打包成flat jar,flat jar的内部结构如下:


屏幕快照 2020-06-08 下午11.05.15.png

其中META-INF目录下的MANIFEST.MF文件内容如下:

Manifest-Version: 1.0
Built-By: developer
Start-Class: com.example.storage.ExampleApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.1.5.RELEASE
Created-By: Apache Maven 3.6.1
Build-Jdk: 1.8.0_121
Main-Class: org.springframework.boot.loader.JarLauncher

其中的Main-Class大家应该很熟悉,执行java -jar命令默认就是执行Main-Class的main方法:


public class JarLauncher
  extends  Launcher
{
  static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
  static final String BOOT_INF_LIB = "BOOT-INF/lib/";
  private final Archive archive;

  public JarLauncher() {}

 private final Archive archive;
  
  public static void main(String[] args) throws Exception { 
        (new JarLauncher()).launch(args); }
}
  
  public JarLauncher() {
    try {
      this.archive = createArchive();
    }
    catch (Exception ex) {
      throw new IllegalStateException(ex);
    } 
  }
protected final Archive createArchive() throws Exception {
    ProtectionDomain protectionDomain = getClass().getProtectionDomain();
    CodeSource codeSource = protectionDomain.getCodeSource();
    URI location = (codeSource != null) ? codeSource.getLocation().toURI() : null;
    String path = (location != null) ? location.getSchemeSpecificPart() : null;
    if (path == null) {
      throw new IllegalStateException("Unable to determine code source archive");
    }
    File root = new File(path);
    if (!root.exists()) {
      throw new IllegalStateException("Unable to determine code source archive from " + root);
    }
    
    return root.isDirectory() ? new ExplodedArchive(root) : new JarFileArchive(root);
  }
 protected void launch(String[] args) throws Exception {
    JarFile.registerUrlProtocolHandler();
    ClassLoader classLoader = createClassLoader(getClassPathArchives());
    launch(args, getMainClass(), classLoader);
  }

protected String getMainClass() throws Exception {
    Manifest manifest = this.archive.getManifest();
    String mainClass = null;
    if (manifest != null) {
      mainClass = manifest.getMainAttributes().getValue("Start-Class");
    }
    if (mainClass == null) {
      throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
    }
    
    return mainClass;
  }

 protected boolean isNestedArchive(Archive.Entry entry) {
    if (entry.isDirectory()) {
      return entry.getName().equals("BOOT-INF/classes/");
    }
    return entry.getName().startsWith("BOOT-INF/lib/");
  }
  protected List<Archive> getClassPathArchives() throws Exception {
    List<Archive> archives = new ArrayList<Archive>(this.archive.getNestedArchives(this::isNestedArchive));
    postProcessClassPathArchives(archives);
    return archives;
  }

  protected String getMainClass() throws Exception {
    Manifest manifest = this.archive.getManifest();
    String mainClass = null;
    if (manifest != null) {
      mainClass = manifest.getMainAttributes().getValue("Start-Class");
    }
    if (mainClass == null) {
      throw new IllegalStateException("No 'Start-Class' manifest entry specified in " + this);
    }
    
    return mainClass;
  }

  protected List<Archive> getClassPathArchives() throws Exception {
    List<Archive> archives = new ArrayList<Archive>(this.archive.getNestedArchives(this::isNestedArchive));
    postProcessClassPathArchives(archives);
    return archives;
  }

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    List<URL> urls = new ArrayList<URL>(archives.size());
    for (Archive archive : archives) {
      urls.add(archive.getUrl());
    }
    return createClassLoader((URL[])urls.toArray(new URL[0]));
  }

 protected ClassLoader createClassLoader(URL[] urls) throws Exception { return new LaunchedURLClassLoader(urls, getClass().getClassLoader()); }
}

  

为了方便阅读,上面的代码我做了部分处理;从上面可以看到,spring boot loader使用的类加载器是LaunchedURLClassLoader,该类加载器继承自URLClassLoader,那么该类加载器会从哪里加载类呢?

protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
    List<URL> urls = new ArrayList<URL>(archives.size());
    for (Archive archive : archives) {
      urls.add(archive.getUrl());
    }
    return createClassLoader((URL[])urls.toArray(new URL[0]));
  }
public List<Archive> getNestedArchives(Archive.EntryFilter filter) throws IOException {
    List<Archive> nestedArchives = new ArrayList<Archive>();
    for (Archive.Entry entry : this) {
      if (filter.matches(entry)) {
        nestedArchives.add(getNestedArchive(entry));
      }
    } 
    return Collections.unmodifiableList(nestedArchives);
  }

protected Archive getNestedArchive(Archive.Entry entry) throws IOException {
    File file = ((FileEntry)entry).getFile();
    return file.isDirectory() ? new ExplodedArchive(file) : new JarFileArchive(file);
  }

可以看到类加载器是从BOOT-INF/classes/BOOT-INF/lib/加载类的,那么加载的顺序是如何确定的呢?

 private Iterator<File> listFiles(File file) {
      File[] files = file.listFiles();
      if (files == null) {
        return Collections.emptyList().iterator();
      }
      Arrays.sort(files, this.entryComparator);
      return Arrays.asList(files).iterator();
    }

 private static class EntryComparator
      extends Object
      implements Comparator<File>
    {
      private EntryComparator() {}

      
      public int compare(File o1, File o2) { return o1.getAbsolutePath().compareTo(o2.getAbsolutePath()); }
    }
  }

可以看到是基于文件的全路径来排序的,另外可以判定BOOT-INF/classes/下面的类的加载顺序是优先于BOOT-INF/lib/的;

tomcat容器项目

查看tomcat源码可以看到,tomcat默认的类加载器为ParallelWebappClassLoader:

public class WebappLoader extends LifecycleMBeanBase
    implements Loader, PropertyChangeListener {

    private static final Log log = LogFactory.getLog(WebappLoader.class);

    // ----------------------------------------------------------- Constructors

    /**
     * Construct a new WebappLoader with no defined parent class loader
     * (so that the actual parent will be the system class loader).
     */
    public WebappLoader() {
        this(null);
    }


    /**
     * Construct a new WebappLoader with the specified class loader
     * to be defined as the parent of the ClassLoader we ultimately create.
     *
     * @param parent The parent class loader
     */
    public WebappLoader(ClassLoader parent) {
        super();
        this.parentClassLoader = parent;
    }


    // ----------------------------------------------------- Instance Variables

    /**
     * The class loader being managed by this Loader component.
     */
    private WebappClassLoaderBase classLoader = null;


    /**
     * The Context with which this Loader has been associated.
     */
    private Context context = null;


    /**
     * The "follow standard delegation model" flag that will be used to
     * configure our ClassLoader.
     */
    private boolean delegate = false;


    /**
     * The Java class name of the ClassLoader implementation to be used.
     * This class should extend WebappClassLoaderBase, otherwise, a different
     * loader implementation must be used.
     */
    private String loaderClass = ParallelWebappClassLoader.class.getName();
``

查看ParallelWebappClassLoader的loadClass方法,可以发现其最终调用的方法为:
```java
StandardRoot.java

 private final List<List<WebResourceSet>> allResources =
            new ArrayList<>();
    {
        allResources.add(preResources);
        allResources.add(mainResources);
        allResources.add(classResources);
        allResources.add(jarResources);
        allResources.add(postResources);
    }

 @Override
    public WebResource getClassLoaderResource(String path) {
        return getResource("/WEB-INF/classes" + path, true, true);
    }
 protected final WebResource getResourceInternal(String path,
            boolean useClassLoaderResources) {
        WebResource result = null;
        WebResource virtual = null;
        WebResource mainEmpty = null;
        for (List<WebResourceSet> list : allResources) {
            for (WebResourceSet webResourceSet : list) {
                if (!useClassLoaderResources &&  !webResourceSet.getClassLoaderOnly() ||
                        useClassLoaderResources && !webResourceSet.getStaticOnly()) {
                    result = webResourceSet.getResource(path);
                    if (result.exists()) {
                        return result;
                    }
                    if (virtual == null) {
                        if (result.isVirtual()) {
                            virtual = result;
                        } else if (main.equals(webResourceSet)) {
                            mainEmpty = result;
                        }
                    }
                }
            }
        }

        // Use the first virtual result if no real result was found
        if (virtual != null) {
            return virtual;
        }

        // Default is empty resource in main resources
        return mainEmpty;
    }

protected void startInternal() throws LifecycleException {
        mainResources.clear();

        main = createMainResourceSet();

        mainResources.add(main);

        for (List<WebResourceSet> list : allResources) {
            // Skip class resources since they are started below
            if (list != classResources) {
                for (WebResourceSet webResourceSet : list) {
                    webResourceSet.start();
                }
            }
        }

        // This has to be called after the other resources have been started
        // else it won't find all the matching resources
        processWebInfLib();
        // Need to start the newly found resources
        for (WebResourceSet classResource : classResources) {
            classResource.start();
        }

        cache.enforceObjectMaxSizeLimit();

        setState(LifecycleState.STARTING);
    }

从上面的代码可以看到WEB-INF/classes下面类的加载顺序是优先于WEB-INF/lib的

 protected void processWebInfLib() throws LifecycleException {
        WebResource[] possibleJars = listResources("/WEB-INF/lib", false);

        for (WebResource possibleJar : possibleJars) {
            if (possibleJar.isFile() && possibleJar.getName().endsWith(".jar")) {
                createWebResourceSet(ResourceSetType.CLASSES_JAR,
                        "/WEB-INF/classes", possibleJar.getURL(), "/");
            }
        }
    }

这边列出lib目录下的jar文件,使用的是File.list方法,该方法在jdk8中的实现如下:

JNIEXPORT jobjectArray JNICALL
Java_java_io_UnixFileSystem_list(JNIEnv *env, jobject this,
                                 jobject file)
{
    DIR *dir = NULL;
    struct dirent64 *ptr;
    struct dirent64 *result;
    int len, maxlen;
    jobjectArray rv, old;

    WITH_FIELD_PLATFORM_STRING(env, file, ids.path, path) {
        dir = opendir(path);
    } END_PLATFORM_STRING(env, path);
    if (dir == NULL) return NULL;

    ptr = malloc(sizeof(struct dirent64) + (PATH_MAX + 1));
    if (ptr == NULL) {
        JNU_ThrowOutOfMemoryError(env, "heap allocation failed");
        closedir(dir);
        return NULL;
    }

    /* Allocate an initial String array */
    len = 0;
    maxlen = 16;
    rv = (*env)->NewObjectArray(env, maxlen, JNU_ClassString(env), NULL);
    if (rv == NULL) goto error;

    /* Scan the directory */
    while ((readdir64_r(dir, ptr, &result) == 0)  && (result != NULL)) {
        jstring name;
        if (!strcmp(ptr->d_name, ".") || !strcmp(ptr->d_name, ".."))
            continue;
        if (len == maxlen) {
            old = rv;
            rv = (*env)->NewObjectArray(env, maxlen <<= 1,
                                        JNU_ClassString(env), NULL);
            if (rv == NULL) goto error;
            if (JNU_CopyObjectArray(env, rv, old, len) < 0) goto error;
            (*env)->DeleteLocalRef(env, old);
        }
#ifdef MACOSX
        name = newStringPlatform(env, ptr->d_name);
#else
        name = JNU_NewStringPlatform(env, ptr->d_name);
#endif
        if (name == NULL) goto error;
        (*env)->SetObjectArrayElement(env, rv, len++, name);
        (*env)->DeleteLocalRef(env, name);
    }
    closedir(dir);
    free(ptr);

    /* Copy the final results into an appropriately-sized array */
    old = rv;
    rv = (*env)->NewObjectArray(env, len, JNU_ClassString(env), NULL);
    if (rv == NULL) {
        return NULL;
    }
    if (JNU_CopyObjectArray(env, rv, old, len) < 0) {
        return NULL;
    }
    return rv;

 error:
    closedir(dir);
    free(ptr);
    return NULL;
}

使用的是readdir64_r系统调用,查询了资料,该函数的实现和文件系统有关,例如Ext4,文件顺序与目录文件的大小是否超过一个磁盘块和文件系统计算的Hash值有关;

appassembler-maven-plugin项目