JVM 知识 -- 类加载
本文最后更新于:1 个月前
⚡类生命周期
-
加载
-
通过全类名获取二进制字节流 (可操控性最强的阶段,例如通过自定义类加载器去网络上获取字节流加载)
-
将字节流表示的静态存储结构变成方法区中运行时动态的存储结构
-
在内存中生成一个 Class 对象,作为方法区内访问数据的入口
-
-
连接
注意:加载和连接阶段是交叉进行的
-
验证
-
验证文件格式
这是验证字节流是否符合 Class 文件格式的规范
例如字节码开头的
0xCAFEBABY
、主次版本号是否在 JVM 处理范围内 -
验证元数据
这是对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求
例如这个类是否有父类,这个类是否继承了不允许继承的类
-
验证字节码
这是最复杂的阶段,通过数据流和控制流确定程序语义是合法的
例如保证任意时刻操作数栈和指令代码序列都能配合工作
-
-
准备
正式为类变量分配内存并设置初始值
注意:
-
只是类变量而非实例变量
-
初始值指的是零值,具体指定的值在初始化阶段才会赋值
-
上面的条件二在常量(被 final 修饰的变量)时不成立,在准备阶段就会赋予目标值。
-
-
解析
JVM 将常量池中的符号引用替换为直接引用,也就是得到类或者字段、方法在内存中的指针或者偏移量
主要针对:类、接口、字段、类方法、接口方法、方法类型、方法句柄、调用限定符等
直接引用是指向目标的指针、相对偏移量、间接定位到目标的句柄
例如 JVM 要去调用某一个方法,需要知道这个方法在方法表中的偏移量,而每个类都有一个方法表
-
-
初始化
类加载的最后一个阶段
只有当下面几种情况才会初始化:
-
遇到
new
,getstatic
,putstatic
,invokestatic
这几条字节码指令 (new 一个对象、读取静态字段、给静态字段赋值、调用类的静态方法) -
反射调用
Class.forName()
,newInstance()
等 -
初始化子类时先初始化父类
-
包含
main()
方法的那个主类 -
…
-
-
使用
-
卸载
一个类被卸载的必要条件:
-
堆上没有这个类的实例对象
-
类没有被引用
-
该类的类加载器已经被回收
JDK 自带的类加载器加载 JDK 自带的类,不会被回收,只有自定义的类加载器的实例才可以被回收
-
⚡类加载器
⚡几种类加载器
-
BootstrapClassLoader(启动类加载器)
-
ExtensionClassLoader(扩展类加载器)
-
ApplicationClassLoader (应用程序类加载器、系统类加载器)
-
自定义类加载器
上面除了 BootstrapClassLoader 类加载器,其他的 ClassLoader 都必须继承自
java.lang.ClassLoader
类,而 Bootstrap ClassLoader 不继承自 ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader 也随着启动(继承 ClassLoader 默认父类加载器是 ApplicationClassLoader)
不继承 AppClassLoader 的原因是:它和 ExtensionClassLoader 都是 Launcher 的静态类,都是包访问路径权限。
⚡双亲委派模型
-
一个类被加载时,首先判断是否被加载过,如果没有则会先去委派父类加载器处理,因此所有的请求最终都会传到 BootstrapClassLoader 加载器,当父类加载器无法加载的时候才会向下交给子类加载器去加载,如果子类也不能加载就再向下委托,如果最后的子类加载器都无法加载则会抛出
ClassNotFoundException
-
注意:
-
双亲委派模型实际上不是有两个上级加载器的意思,父类加载器只有一个
-
类加载器的父子关系并不是继承实现的,而是通过优先级实现的
-
-
为什么有双亲委派模型
-
安全
当我们写一个自定义的 String 类型时,保证 JVM 加载的还是 JDK 自带的类,因为 BootstrapClassLoader 类加载器就能把这个类加载出来了,ApplicationClassLoader 不会去加载我们自定义的 String 类
即使是我们自己用自定义类加载器去加载,也不会成功,因为 String 类在 JVM 启动时已经被 BootstrapClassLoader 加载了,不会二次重复加载
-
⚡类加载器加载流程
类加载器加载的时候,就是调用 loadClass()
方法:
-
首先检查这个类是否被加载过
-
如果有,直接返回,否则开始加载
-
找到父类加载器,如果不为空,说明父类加载器不是 BootstrapClassLoader,那么直接调用父类加载器的
loadClass()
方法;否则父类加载器就是 BootstrapClassLoader,
注意:
如果类加载器不同,那么即使是同一份字节码交给两个不同的类加载器去加载,得到的类也是不同的
⚡为什么需要自定义类加载器(打破双亲委派)
-
字节码加密解密
-
动态获取类(热部署)
-
从特定源获取类(例如网络,而非本地文件系统)
例子:
-
例如 Tomcat 定义多个类加载器:
-
保证同一个服务器的两个 web 应用程序 Java 类库隔离
-
保证同一个服务器的两个 web 应用程序 Java 类库可以共享
-
保证服务器自身安全不被 web 应用程序影响
-
-
再比如数据库驱动类
Class.forName("com.mysql.dj.Driver");
先多说一句这段代码的原理是:mysql 驱动的 Driver 类里面有一个静态代码块,当 Driver 类被加载的时候这段静态代码块被执行,将 mysql 驱动实例注册到全局的 jdbc 驱动管理器里。
public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver());//首先new一个Driver对象,并将它注册到DriverManage中 } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } } }
而我们之后一般又会这样链接数据库
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
上面的
Class.forName()
方法是使用调用者 Class 对象的 ClassLoader 来加载目标类,但是这样其实没有打破双亲委派,具体的分析看最后的 SPI 扩展相关部分吧
⚡类加载器主要方法
-
loadClass
加载类的方法入口,这个方法默认就遵循双亲委派模型加载类,如果想要破坏双亲委派模型,就要重写这里的方法
-
findClass
如果上面的 loadClass 方法都没有办法加载出来,就会调用这个类加载器自己的 findClass 方法加载。
对于最下层的类加载器来说,如果此处还是返回为空,说明这个类无法找到
-
defineClass
如果类加载器加载了类,获取到字节码,就用这个方法将字节码转换为 Class 对象
class ClassLoader {
// 加载入口,定义了双亲委派规则
Class loadClass(String name) {
// 是否已经加载了
Class t = this.findFromLoaded(name);
if(t == null) {
// 交给双亲
t = this.parent.loadClass(name)
}
if(t == null) {
// 双亲都不行,只能靠自己了
t = this.findClass(name);
}
return t;
}
// 交给子类自己去实现
Class findClass(String name) {
throw ClassNotFoundException();
}
// 组装Class对象
Class defineClass(byte[] code, String name) {
return buildClassFromCode(code, name);
}
}
class CustomClassLoader extends ClassLoader {
Class findClass(String name) {
// 寻找字节码
byte[] code = findCodeFromSomewhere(name);
// 组装Class对象
return this.defineClass(code, name);
}
}
扩展: SPI
在高版本的JDK,已经不需要手动调用class.forName方法了,在DriverManager的源码中可以看到一个静态块
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
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);
}
在 loadInitialDrivers
方法中,最重要的是 ServiceLoader.load(Driver.class)
,这行代码可以把类路径下所有 jar 包中 META-INF/services/java.sql.Driver
文件中定义的类加载上来,此类必须继承自java.sql.Driver
。
其次,driversIterator.next();
还会循环调用上面获取到所有的 Driver 的子类,调用 next 方法,最后调用下面的 nextService 方法:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//重点!使用当前示例的成员变量loader加载,就是上面设置的ThreadContextClassLoader
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
上面的 Class.forName()
方法完成了在 BootstrapClassLoader 加载的类(Driver.class)中通过ThreadContextClassLoader 加载了应用程序的实现类。
这段代码的重点就是 Class.forName()
也就是说,通过SPI的方式把程序员手动做的动作(Class.forName()
)变成框架自动执行。
所以,SPI (service provider interface)其实是一种标准,他定义了接口,而不同服务的厂商去实现这个接口,当用户需要使用的时候,就把相关的实现类放到指定的位置,应用程序会自动找到他们并加载
相比 API,SPI 更接近服务调用方
在 jdk6 里面引进的一个新的特性 ServiceLoader,从官方的文档来说,它主要是用来装载一系列的 service provider。而且 ServiceLoader 可以通过 service provider 的配置文件来装载指定的 service provider。当服务的提供者,提供了服务接口的一种实现之后,我们只需要在 jar 包的 META-INF/services/ 目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该 jar 包 META-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
所以,SPI 的思想是:接口的实现还是由服务提供者来实现,但是服务提供者只用在提交的 jar 包里的 META-INF/services 下根据平台定义的接口新建文件,并添加进相应的实现类内容就好,而服务调用者根据标准接口通过本地服务发现加载具体服务提供商。
这样做的好处是:实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
常用场景有:
-
数据库驱动实现类加载
JDBC 加载不同的数据库驱动
-
日志门面接口加载
Slf4j 加载不同日志实现
-
Spring
本博客所有文章除特别声明外,均采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 。转载请注明出处!