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提供了一种线程上下文加载器的机制规避以上问题
代码语言:javascript复制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 = '