概述
“虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制”
Java虚拟机对类的加载都是在程序运行期间进行的,增加了性能开销,但带来灵活度,可以在运行时从网络或者本地加载一个二进制流作为程序运行代码的一部分。
那虚拟机启动时具体初始化哪些类呢?有这样5中情况:
- 遇到new、getstatic、putstatic、invokestatic时,如果类没有初始化,会对其执行初始化。
- 使用反射调用时会初始化。
- 遇到初始化一个类,如果有父类,先对其初始化
- 虚拟机启动时,虚拟机会初始化用户指定的主类(main方法)
- java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
本文的重点在于类和类加载器。
类的唯一性
在编写代码时,我们通过包名+类名定位一个类,但对虚拟机来说,还需要算上类加载器:
加载类的类加载器+全类名
也就是说,同一个类被不同的加载器加载,也认为不是一个类。看一个示例:
public static void main(String[] args) throws Exception {
// 自定义一个加载器,细节先不用管
ClassLoader myLoader = new ClassLoader() {
@Override
public Class<?>loadClass(String name) throws ClassNotFoundException {
try {
String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
InputStream is = getClass().getResourceAsStream(fileName);
if (is == null) {
return super.loadClass(name);
}
byte[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b,0,b.length);
} catch (IOException e) {
throw new ClassNotFoundException(name);
}
}
};
// 加载并初始化
Object obj = myLoader.loadClass("com.thoreauz.bootlearn.ClassLoaderTest").newInstance();
// 验证
System.out.println(obj instanceof ClassLoaderTest);
System.out.println(obj.getClass().getClassLoader());
System.out.println(ClassLoaderTest.class.getClassLoader());
}
false com.thoreauz.bootlearn.ClassLoaderTest$1@9e89d68 sun.misc.Launcher$AppClassLoader@18b4aac2
示例中自义定一个类加载器(根据指定类名从classpath下加载),直接加载类,而ClassLoaderTest.class.getClassLoader()是在main方法启动初始化时,被AppClassLoader加载的。加载器不同,所以不是同一个类。
类加载器
广义来说,存在两种类加载器。虚拟机自带的Bootstrap ClassLoader和java实现的外部加载器。细致一点有以下分类:
- Bootstrap ClassLoader:加载核心类库,加载rt.jar,i18n.jar等;
- Extention ClassLoader:扩展的类加载器,机JRE_HOME/lib/ext
- AppLoader: 加载class path下的类,默认情况下是
.
,可以通过-classpath
或者-cp
指定,还可以设置环境变量–CLASSPATH(命令会覆盖环境变量)。
- 默认加载class,也是按照这个顺序依次寻找。如果不是自定义加载器,一般我们也无需关心加载顺序,只需要关心类冲突。
- classpath 除了手动指定之外,如果在classpath下的jar的manifest有Class-Path 属性,也会去指定目录查找。
从上到下的加载顺序,如下图:
双亲委派模型
双亲委派模型是java类加载的一个常规模型:
- 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。
- 每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索路径中没有找到所需的类)时,子加载器才会尝试自己去加载。
一般来说就是按上图的加载顺序,但这个规范不是强制的,比如上文为了验证类唯一性,我自定义的类加载器,就直接通过loadClass方法加载了类,而没有请求父类加载器(示例中是AppClassLoader)。
双亲委派优点:
- 避免重复加载:优先通过父类加载,父类可以缓存起来,供后续使用,也提升了速度。
- 保证核心类安全:比如java.lang.String,如果不是委托父类而是应用appClassLoader加载,那就可以随便在应用程序或者二方包中覆盖掉java核心类。
自定义加载器
- 继承ClassLoader,允许指定一个父加载器,不指定默认分配一个(初始化时的类加载器?)。
- 覆写findClass()方法:加载到类字节流,通过defineClass加载进来。 不提倡覆盖loadClass(),而是把加载逻辑写在findClass(),这样就可以保证新写出来的类加载器是符合双亲委派规则的。看java.lang.ClassLoader#loadClass的逻辑就清楚了,委托逻辑已经写好了。
类加载器使用场景
- 隔离:很多框架和容器都自定义了加载器,避免和业务代码使用的jar包冲突。比如tomcat。
- 热部署:虚拟机自然不会监测文件变化自动更新,如果要热部署,需要在自己的类加载器实现相应逻辑。
- 加密: 比如从网络加载class,就可以加密它,然后在自定义加载器的findClass解密。
线程上下文加载器
SPI(Service Provider Interface),我们先来看看在java中使用DriverManager获取jdbc连接,mysql为例:onnector-j-examples,把jar包放在classpath下后,通过一行代码,就可以创建链接:
DriverManager.getConnection("jdbc:mysql://localhost/test?user=minty&password=greatsqldb");
然而,DriverManager是java提供的核心类,在rt.java下,通过BootstrapClassLoader加载,它是怎么找到具体驱动driver实现类的呢?看看DriverManager的静态方法:
static {
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();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
return null;
}
});
// ...
}
其中ServiceLoader.load(Driver.class);
通过ServiceLoader.load和iterator去找Driver的实现类。
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// PREFIX = "META-INF/services/
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
总结起来就是:解析META-INF/services/下的配置文件,使用Thread.currentThread().getContextClassLoader();
加载。
mysql-connector-java.jar
- META-INF/
- services
- java.sql.Driver
- com
- org
# java.sql.Driver 文件内容
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
driver初始化时,它的加载器是Bootstrap ClassLoader,它并不认识classpath路径,所以它压根找不到实现类。根据双亲委派机制,不能反过来委托子加载器加载。再设计bootstrap加载器去识别特定路径?这样双亲委派机制的优势就没了。所以只能通过线程上下文类加载器来加载。Java应用的线程上下文类加载器默认是AppClassLoader,这样ServiceLoader就可以成功加载SPI的实现类了,也就突破了双亲委派的限制。
适用场景:
- SPI等父加载器需要子加载器帮助加载时。
- 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。
Class.forName 和 ClassLoader.loadClass
两个都可以用来加载类:
class A{
public void m(){
A.class.getClassLoader.loadClass("B");
}
}
- CCL: 当前类加载器,对应A的加载器;无法获取引用??
- SCL:指定类加载器,代码中通过A.class.getClassLoader获取了加载器。
- TCCL:线程上下文加载器。Thread.currentThread().getContextClassLoad()
class A{
public void m(){
B b = new B();
B b2 = Class.forName("B");// 等B.class
}
}
- Class.forName(“B”),会使用Class.class.getClassLoader().loadClass(“B”)加载,也就是利用Class的加载器加载。但实际上真正用到的加载器还是A的加载器:
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
Reflection.getCallerClass()能获取调用 Class.forName的类的ClassLoader,其实这也是突破双亲委派的解决方式。
还有一种方式,反序列化时,通过序列化中的类型名,怎么找到客户端呢? Class.forName(Reflection.getCallerClass())不行,因为 Class.forName是在ObjectInputStream中执行,而ObjectInputStream的加载器是bootStrapClassLoader,自然加载不到自定义在classpath下的类。解决方式是通过查询栈信息:通过sun.misc.VM.latestUserDefinedLoader()
返回第一个非null的loader(bootstrap class loader是空的)。
ObjectInputStream in = new ObjectInputStream(fileInputStream);
B b = (B) in.readObject();
这两个加载类的方式,可以用在SPI模式下么?
也可以,Class.forName("com.mysql.jdbc.Driver")
还是之前推荐的用法,不过有限制,必须要保证驱动在classpath下。
假设我写了一个jdbc-common库,封装了DriverManager,在子定义加载器加载指定目录下的驱动。此时如果在apploader下调用jdbc-common。
- 假设DriverManager使用Class.forName:调用此方法是在DriverManager类,它的加载器是Boot,显然无法加载到mysql实现。
- 假设使用
sun.misc.VM.latestUserDefinedLoader()
加载:最近一个非空class loader是AppClassLoader,由于没在启动classpath下,也是无法加载。
这种情况下,需要显示指定类加载器,不能用这种隐式传递的类加载器,也就是只能用线程上下文加载器的原因。特别在容器、框架等比较常用。一般如下使用
// APP
ClassLoader old = Thread.currentThread().getContextClassLoader();
//TCCL= shardingClassLoader
Thread.currentThread().setContextClassLoader(customClassLoader);
try {
// 启动容器等
}finally {
// 恢复上下文加载器
Thread.currentThread().setContextClassLoader(old);
}
引用: