美文网首页
类加载机制分析

类加载机制分析

作者: allanYan | 来源:发表于2020-06-08 23:35 被阅读0次

    概述

    最近在项目中遇到个问题,一次升级依赖之后,发现线上某台机器日志无输出;这种问题通常都是由于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项目

    相关文章

      网友评论

          本文标题:类加载机制分析

          本文链接:https://www.haomeiwen.com/subject/fzxjtktx.html