Java类加载器机制及应用

kyaa111 1年前 ⋅ 462 阅读

0x00 何时触发类加载动作


显式加载

  • 通过ClassLoader的loadClass方法
  • 通过ClassLoader的findClass方法
  • 通过Class.forName

隐式加载

  • 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时
  • 对类进行反射调用时
  • 当初始化一个类时, 如果其父类还没有初始化, 优先加载其父类并初始化
  • 虚拟机启动时, 需指定一个包含main函数的主类, 优先加载并初始化这个主类

0x01 类加载的特性


延迟加载

例: 在main方法中, new了一个 Test 类, 那么这个Test类在new动作发生时, 才会被ClassLoader加载. 若 Test 类被两个不同的类加载器加载了, 那么对应的实例Class是完全不同的, JVM只保证同一个加载器内不会有重复的类

传递

例: main方法中, 调用了Test的静态方法 run , run内又调用了User类的静态方法 jump , 那么User同样也是被Test类的加载器加载的

双亲委派

双亲委派不是强制性的规范.

《深入理解Java虚拟机》

双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美,如果基础类又要调用回用户的代码,那该怎么办? 这并非是不可能的事情,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器去加载(在JDK 1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码,但启动类加载器不可能“认识”这些代码啊!那该怎么办? 为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoaser() 方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。 有了线程上下文类加载器,就可以做一些“舞弊”的事情了,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

在Java SPI中, JDK提供了一种线程上下文加载器的机制规避以上问题

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
                                            ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

JDK提供了一种帮第三方实现者加载服务(如数据库驱动、日志库)的便捷方式,只要遵循约定(把类名写在/META-INF里),那使用ServiceLoader时, JDK会去扫描所有jar包里符合约定的类名,但加载ServiceLoader的ClassLoader ( BootClassLoader ) 是没法加载服务实现类的,那就要使用线程上下文类的加载器进行加载 (默认是AppClassLoader)

java.sql.DriverManager#loadInitialDrivers

private static void loadInitialDrivers() {
    ...
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            // 使用了下层加载器进行加载 破坏了双亲委派
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
        }
    });
    ...
}

0x02 对核心类的保护机制


在调用ClassLoader#defineClassxxx之前, 都会调用preDefineClass校验包名, 如果包名不符合规范, 则抛出SecurityException异常, 如果使用反射直接调用defineClassxxx, JVM中也会对包名进行校验, 同样抛出异常

/hotspot/src/share/vm/classfile/systemDictionary.cpp

SystemDictionary::resolve_from_stream

Klass* SystemDictionary::resolve_from_stream(Symbol* class_name,
                                             Handle class_loader,
                                             Handle protection_domain,
                                             ClassFileStream* st,
                                             bool verify,
                                             TRAPS) {

  ...
  const char* pkg = "java/";
  size_t pkglen = strlen(pkg);
  if (!HAS_PENDING_EXCEPTION &&
      !class_loader.is_null() &&
      parsed_name != NULL &&
      parsed_name->utf8_length() >= (int)pkglen &&
      !strncmp((const char*)parsed_name->bytes(), pkg, pkglen)) {
    // 如果待加载类的名称是以java/开头(解析过程中会将 . 替换为 / ), 即类的全限定名称以 java. 开头, 则抛出异常
    // 除非加载器是BootClassLoader
    ResourceMark rm(THREAD);
    char* name = parsed_name->as_C_string();
    char* index = strrchr(name, '/');
    assert(index != NULL, "must be");
    *index = '\0';
    while ((index = strchr(name, '/')) != NULL) {
      *index = '.';
    }
    const char* fmt = "Prohibited package name: %s";
    size_t len = strlen(fmt) + strlen(name);
    char* message = NEW_RESOURCE_ARRAY(char, len);
    jio_snprintf(message, len, fmt, name);
    Exceptions::_throw_msg(THREAD_AND_LOCATION,
      vmSymbols::java_lang_SecurityException(), message);
  }
 ...
}

0x03 SpringBoot应用


对比下SpringBoot和普通Java工程打包后的文件就会发现, SpringBoot打包后的FAT Jar仅仅是多了一个BOOT-INF目录, 在该目录中, 存放了用户的所有代码以及依赖jar.

若使用默认的AppClassLoader类加载器肯定是无法加载到自定义路径的代码以及依赖, 所以需要用到自定义类加载器

在SpringBoot的FAT Jar中的MANIFEST.MF指明了实际的启动类为org.springframework.boot.loader.JarLauncher

JarLauncher依次调用到org.springframework.boot.loader.Launcher#launch(java.lang.String[])

	protected void launch(String[] args) throws Exception {
		if (!isExploded()) {
			JarFile.registerUrlProtocolHandler();
		}
		ClassLoader classLoader = createClassLoader(getClassPathArchivesIterator());
		String jarMode = System.getProperty("jarmode");
		String launchClass = (jarMode != null && !jarMode.isEmpty()) ? JAR_MODE_LAUNCHER : getMainClass();
		launch(args, launchClass, classLoader);
	}

最终通过反射调用用户的main方法

	public void run() throws Exception {
         // 使用org.springframework.boot.loader.Launcher#launch(java.lang.String[])创建的自定义类加载器
		Class<?> mainClass = Class.forName(this.mainClassName, false, Thread.currentThread().getContextClassLoader());
		Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
		mainMethod.setAccessible(true);
		mainMethod.invoke(null, new Object[] { this.args });
	}

0x04 JDK9的变化


首先 ExtensionClassLoader被平台类加载器PlatformClassLoader取代。因为整个JDK都基于模块化进行构建,其中的个java类库就已天然满足了可扩展的需求,所以扩展类加载器就完成了他自己的使命

另外,JDK 9之后出现了BootClassLoader这样一个类,启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器,但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null,而不会得到BootClassLoader实例

最后,委派关系发生了一些变动,当PlatFormCLassLoader以及ApplicationClassLoader收到加载请求的时候,在委派给父加载器前,会先判断该类是否能够归属到某个系统模块中去,如果可以,就会优先委派给那个模块的加载器完成加载