您的位置:

深入探究Java类加载机制

一、概述

Java虚拟机(JVM)是运行Java程序的重要平台。在JVM中,类的加载、连接、初始化是Java程序运行的基础。在Java中,类是按需加载的,也就是在程序首次使用类的时候才会被加载。Java采用的是双亲委派模型的类加载机制,这个机制对于Java的生态系统是非常重要的。本文将从多个方面阐述Java类加载机制。

二、类的生命周期

在了解Java的类加载机制之前,需要了解类的生命周期:

  1. 加载:从文件系统、网络等来源读取二进制数据,并且创建出Class对象。
  2. 连接:包括验证、准备和解析三个阶段。
  3. 初始化:为类的静态变量赋初值,执行<clinit>方法。
  4. 使用:类被调用。
  5. 卸载:类不再被引用,被GC回收。

三、类加载机制

1. 双亲委派模型

Java采用的是双亲委派模型的类加载机制,这个机制对Java的生态系统是非常重要的。它是从Java1.2开始加入的,严格来说应该叫做双亲委派委派模型。简单来说,就是当一个类加载器接收到一个类加载请求后,它会将请求转发给它的父类加载器,直到最顶层的启动类加载器。

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先从缓存中查找已经被加载过的类
            Class<?> c = findLoadedClass(name);
            // 如果没有被加载,再让父亲去试一下
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException ignored) {}
                // 如果父亲都找不到,最后就由自己去加载
                if (c == null) {
                    c = findClass(name);
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

2. 双亲委派的好处

借助于双亲委派机制,Java程序可以避免类的重复加载。这种机制从根本上保证了Java程序的稳定性和安全性。例如,在使用微服务和大型框架的情况下,如果每个服务请求使用不同的类加载器,就会造成类重复加载,从而会导致内存使用量过高、版本不一致等问题。通过引入双亲委派机制,可以将类加载的责任传递给父类加载器,避免了类重复加载问题。

3. 破坏双亲委派模型

虽然双亲委派模型时Java的默认类加载机制,但是它并不是万能的。有时候,我们会遇到一些特殊场景,需要打破这个机制。Java提供了两种方式来打破双亲委派机制:

  1. 线程上下文类加载器
  2. 为了解决上述问题,Java引入了线程上下文类加载器的概念。它是从Java1.2引入的。当默认的类加载器无法满足某个类加载请求时,可以通过线程上下文类加载器来加载。可以通过Thread.currentThread().setContextClassLoader()方法来设置线程上下文类加载器。

        public static void main(String[] args) {
            Thread.currentThread().setContextClassLoader(new MyClassLoader());
            ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
            Class<?> clazz = classLoader.loadClass("com.example.MyClass");
        }
    
  3. 自定义类加载器
  4. 通过自定义类加载器,可以打破双亲委派模型,从而实现更灵活的类加载机制。自定义类加载器需要继承java.lang.ClassLoader类,并重写其中的findClass()方法。调用自定义类加载器的代码应该在类和类加载器分离的情况下,通过newInstance()来创建新的类对象。

        public class MyClassLoader extends ClassLoader {
            @Override
            protected Class<?> findClass(String name) {
                // 从指定的磁盘路径加载类
                byte[] bytes = loadClassBytes(name);
                return defineClass(name, bytes, 0, bytes.length);
            }
        }
    

四、常见问题

1. 类被重复加载了怎么办?

我们可以通过JVM参数-verbose:class来查看类的加载情况。如果出现类重复加载的情况,可以使用强制主动加载的方式来防止重复加载。例如,在使用spring-boot-devtools的场景下,可以在build.gradle文件中添加以下代码:

plugins {
    id 'org.springframework.boot' version '2.3.1.RELEASE' apply false
}
 
ext['spring-boot'] = ["org.springframework.boot:spring-boot-devtools"]
dependencies {
    runtimeOnly "${spring-boot}"
}
 
configurations {
    developmentOnly
    runtimeClasspath {
        extendsFrom developmentOnly
    }
}
 
task developmentJar(type: Jar) {
    classifier = 'development'
    from sourceSets.main.output
    appendix = "-${version}-SNAPSHOT"
}
 
artifacts {
    developmentOnly developmentJar
}
 
bootJar {
    requiresUnpack '**/spring-boot-devtools-*.jar'
}
 
task devtools(additionalResourceDirs: sourceSets.main.output.resourcesDir) {
    doLast {
        files(additionalResourceDirs).visit {
            def file = it.getFile();
            if (file.path.endsWith('.class')) {
                Class.forName(file.path.split('main\\\\classes\\\\', 2)[1].replaceAll('\\\\', '.').replaceAll('.class', ''))
            }
        }
    }
}
 
tasks.compileJava {
    finalizedBy devtools
}

2. 打破双亲委派模型会带来什么问题?

在某些情况下,打破双亲委派机制可能会导致类重复加载、版本不一致等问题。例如:Maven的classpath中存在多个版本的同一依赖包时,使用默认的双亲委派模型无法加载到正确版本的类,这时就可以通过线程上下文类加载器来加载。但需要注意的是,过多地使用自定义类加载器会降低Java程序的性能。

五、总结

Java的类加载机制是Java程序运行的基础,其中双亲委派模型是其重要的机制之一。通过对双亲委派模型的理解和应用,可以保证Java程序的稳定性和安全性。当然,在必要的情况下,也可以通过线程上下文类加载器或者自定义类加载器来打破默认的类加载机制,实现更灵活的类加载方式。