类加载机制
虚拟机把描述类的Class文件(一串二进制字符流,无论何种存在形式)加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是JVM的类加载机制。
类的生命周期
类从加载到内存到卸载的整个生命周期如下图:
加载阶段
Java虚拟机规范并没有指定什么时候开始加载阶段,由虚拟机实现自己选择。加载阶段的工作由类加载器来完成,在JVM中一个类的唯一性由类本身与加载它的类加载器共同确定。
加载阶段的主要工作为:
- 读取字符流:通过一个类的全限定名来获取该类的二进制字符流
- 数据结构转储:将这个字符流所代表的静态存储结构转化为方法区的运行时数据结构
- 生成访问入口:在堆中生成一个Class对象并初始化静态域,作为方法区该类数据的访问入口
有一个特殊的类是数组类,数组类由虚拟机直接创建,不由类加载器加载,然而数组类分两种类型:
- 引用类型数组:由类加载器加载引用类,与引用类类加载器关联
- 基本类型数组:与启动类类加载器关联
类加载器与加载模型
类加载阶段的工作由类加载器来完成,Java系统提供的类加载器主要有以下三个,绝大部分Java应用启动都需要这三个类加载器参与。
- 启动类加载器(Bootstrap Classloader):主要加载< JAVA_HOME >\lib目录下的类,由JVM内部实现。
- 拓展类加载器(Extension Classloader):主要加载< JAVA_HOME >\lib\ext目录下的类,类名sun.misc.Launcher$ExtClassLoader
- 应用程序加载器(Application Classloader):主要加载用户类路径(classpath)上的类,类名sun.misc.Launcher$AppClassLoader
双亲委派模型
Java提供的类加载器
上述三个类加载器具有层次关系:某个类加载器需要加载某个类时,它会先将这个加载请求委托给它的父加载器去加载,直到顶层类加载器,如果父加载器加载不成功,再由其子加载器来加载。这种层次关系称为类加载器的双亲委派模型。注意这里的父子关系与语言中的继承父子关系不同,这里的父子关系基于组合。
这种加载模型为Java程序的稳定运行提供了可靠的保证:Java核心类库的加载可以由启动类和拓展类加载器可靠加载,用户编写的与Java核心类库同名的类因为系统中已经加载过而不会二次加载保证了Java核心类库的安全。
双亲委派模型图:
Java类库中的类加载器实现类继承结构图:
自定义类加载器
Java允许程序员自定义类加载器,如上图所示,可以将自定义类加载器添加至现有的双亲委派模型中。
为什么需要自定义类加载器
- 安全性:普通class文件的代码很容易被反编译,将class文件加密后,加载类时需要解密,这时只能由具备解密功能的类加载器才能加载成功。
- 合并与隔离:如web容器tomcat,装了多个webapp,需要将多个webapp的共用框架类如spring等合并加载,又需要将各app的业务逻辑类库相互隔离。就需要定义新的类加载器加载合并类,为每个app定义新的类加载器以解决隔离。
- hotswap:在JSP这类改动非常频繁的场景下,每次都重新启动应用时间浪费严重。hotswap监控文件的改动,在改动发生后,用新的类加载器重新加载改动后的类并周知应用。
- 打破双亲委派模型:在OSGi中,模块化Java应用,需要自定义类加载器为每个Bundle的类加载负责
如何自定义类加载器
Java定义了ClassLoader抽象模板类,用户可以继承该类实现自己的类加载器。实现自定义类加载器中关键又容易迷惑几个方法:
- loadClass:加载类,封装了双亲委派模型逻辑、findClass、define
- findClass:根据完全限定名寻找字节码
- defineClass:将二进制字节码转化为方法区的类表示,并返回该类的Class对象
先来看loadClass方法的源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
//双亲委派模型:委托给父加载器加载
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//如果父加载器加载失败,由当前加载器尝试加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//注意:此时调用的是findClass方法
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
而findClass方法:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
这意味着从抽象类ClassLoader出发定义类加载器必须重写findClass方法。ClassLoader中定义了defineClass方法却没有在loadClass方法中体现出来,原因在于defineClass方法的调用被包含在了子类定义的findClass中了。
看一个ClassLoader的子类URLClassLoader重写的findClass方法:
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
try {
return AccessController.doPrivileged(
new PrivilegedExceptionAction<Class>() {
public Class run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
//此时调用了defineClass方法
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
throw new ClassNotFoundException(name);
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
}
对于不同的场景需求,自定义类加载器有不同的实现方式:
- 打破双亲委派模型:重写loadClass方法
- 加载自定义路径上的类:重写findClass方法或者直接使用URLClassLoader
- hotswap:文件每变更一次,用新的类加载器实例重新加载一次并周知应用
- 安全性:重写defineClass方法先解密
具体需要什么样的自定义类加载器,视不同的场景而定。
自定义类加载器典型应用:Tomcat
Tomcat作为webapp容器,在类加载上的需求是既需要各app隔离也需要公共库共享,同时还要保证容器本身的安全性,最后还要实现JSP文件的hotswap。
在此需求下,tomcat将自己的程序目录做了如下划分:
- common目录:类库可被tomcat和所有webapp共同使用
- server目录:仅供tomcat使用
- shared目录:仅供所有webapp使用
- 某webapp/WEB-INF目录:仅供该webapp使用
一个webapp对应一个类加载器,我们至少需要四个类加载器来加载这四个目录下的类,同时还需要一个类加载器来实现JSP文件的hotswap,最终tomcat的类加载器结构图如下:
由tomcat中Bootstrap类的initClassLoaders方法出发,看一下其common,server和shared目录的类加载器实现:
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
最终追溯到ClassLoaderFactory的createClassLoader方法:
public static ClassLoader createClassLoader(File unpacked[],
File packed[],
final ClassLoader parent)
throws Exception {
if (log.isDebugEnabled())
log.debug("Creating new class loader");
// Construct the "class path" for this class loader
Set<URL> set = new LinkedHashSet<URL>();
//省略部分中间代码
// Construct the class loader itself
final URL[] array = set.toArray(new URL[set.size()]);
return AccessController.doPrivileged(
new PrivilegedAction<URLClassLoader>() {
@Override
public URLClassLoader run() {
if (parent == null)
return new URLClassLoader(array);
else
return new URLClassLoader(array, parent);
}
});
}
可以发现这三个类加载器都是由Java提供的URLClassLoader直接支持的,只是不同的实例。
而webapp类加载器由专门的WebappClassLoader类来实现,该类是Java ClassLoader的子类,具体实现逻辑比较复杂,这里暂时先不展开写了。
对于JSP的hotswap,使用的是专门的JasperLoader类加载器,该类是URLClassLoader的子类,JSP对应的Java类没有定义构造器,则默认是无参构造器,在tomcat检测到JSP文件被修改后,新生成一个JasperLoader的实例去重新加载JSP对应的Java类,然后由该类的Class对象的newInstance方法生成新的JSP对象给应用使用。
自定义类加载器典型应用:OSGi
OSGi的类加载器结构不同于tomcat这种的绝对中心化的双亲委派模型,OSGi中的每个Bundle都有自己的类加载器,各个Bundle之前有依赖时,将类加载的工作委托给各个Bundle的类加载器来完成,即各个Bundle之前由依赖关系引出了类加载关系,构成了一个网状的结构,而不是严格的层次结构。
OSGi的这种设计结构,将类加载的责任移交给Bundle自身,实现了对类加载的更加精准的控制,带来了更多的灵活性,同时也为Bundle的热插拔的实现提供了基础。不过因为复杂的依赖关系,可能出现死锁的情况。
特殊的类加载器:线程上下文类加载器
在双亲委派模型中,下层类可以调用上层类,但是上层类要调用下层类就没有那么方便了,第一个遇到的问题就是上层类在下层类还未加载的时候并不知道去哪里加载下层类。
这时候我们需要一个类加载器,这个类加载器知道去哪里找需要的下层类库。这个类加载器就是线程上下文类加载器。Thread类有个字段为contextClassLoader,这个字段可以通过setContextClassLoader方法进行设置。其默认值为其父线程的contextClassLoader。
这个contextClassLoader对于上层类来说就好像是一个钩子,在调用到下层类时即通过此类加载器去加载需要的类。
典型的应用即SPI,SPI是一套规范接口,当上层代码需要管理SPI时却不一定知道具体的SPI实现在哪里,需要通过线程上下文类加载器去加载实现类。
深入理解Java虚拟机
https://blog.csdn.net/seu_calvin/article/details/52315125