ClassLoader是java的核心组件,所有的class都是由ClassLoader进行加载的。ClassLoader负责通过个各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例,然后交给虚拟机进行链接、初始化等操作。
一、类的加载分类
class文件的显示和隐式加载的方式指jvm加载class文件到内存的方式
1、显示加载
通过ClassLoader加载class对象
2、隐式加载
不直接调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中。
二、类加载器分类
jvm支持两种类型的类加载器,分别为引导类加载器和自定义类加载器(所有派生于ClassLoader的类加载器)
1、启动类加载器
(1)使用c/c++实现嵌套在jvm内部
(2)加载java核心库
JAVA_HOME/jre/lib/rt.jar
sun.boot.class.path路径下的内容
(3)不继承ClassLoader,没有父类加载器
(4)只加载包名为java,javax,sun开头的类
(5)加载扩展类加载器和应用程序类加载器
2、扩展类加载器
(1)java语言,sun.misc.Launcher.$ExtClassLoader实现
(2)继承于ClassLoader类
(3)父类加载器为启动类加载器
(4)加载java.ext.dirs/jre/lib/ext指定的目录
3、应用程序类加载器
(1)java语言,sun.misc.Launcher.$AppClassLoader实现
(2)继承于ClassLoader类
(3)父类加载器为扩展类加载器
(4)加载环境变量classpath或者系统属性java.class.path指定路径下的类
(5)用户自定义类加载器的默认父加载器
三、ClassLoader源码解析
1、获取父类加载器
public final ClassLoader getParent()
2、根据类的全限定名加载(双亲委派机制)
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
name:类的全限定名,resolve:是否需要解析
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 {
//parent==null,当前加载器为扩展类加载器
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();
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;
}
}
3、findClass
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
查找二进制名称为name的类,返回结果为java.lang.Class类的实例,该方法会在检查完父类加载器之后被loadClass()方法调用
在jdk1.2之前,自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义类加载类。
在jdk1.2之后,不再建议用户覆盖loadClass()方法,而是建议把自定义的类加载逻辑写在findClass()方法中,findClass()方法是在loadClass()方法中被调用的。
当loadClass()方法中父类加载器加载失败后,则会调用自己的findClass()方法来完成类加载,这样就可以保证自定义的类加载器也符合双亲委派模式。
一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象。
4、defineClass
protected final Class<?> defineClass(byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(null, b, off, len, null);
}
根据给定的字节数组b转换为Class实例,off和len参数表示Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。
举例:
5、resolveClass
protected final void resolveClass(Class<?> c) {
resolveClass0(c);
}
private native void resolveClass0(Class<?> c);
链接指定一个java类,使用该方法可以使用类的Class对象创建完成的同时也被解析。
6、findLoadedClass
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}
private native final Class<?> findLoadedClass0(String name);
查找名称为name的已经被加载过的类,返回Class实例
四、SecureClassLoader和URLClassLoader
1、SecureClassLoader
扩展了ClassLoader,新增了几个于与使用相关的代码源,对代码源的位置及其证书的验证和权限的定义类验证
2、URLClassLoader
是ClassLoader的具体实现类,实现了findClass,findResource等方法。
新增了URLClassPath类协助获取Class字节码流相关功能。
五、双亲委派机制
从jdk1.2开始类的加载过程采用双亲委派机制,该机制规定了加载顺序是,引导类加载器先加载,如果加载不了,由扩展类加载器加载,若还加载不了,才会是系统类加载器或者在自定义类加载器进行加载。
1、优势
(1)避免类的重复加载,确保一个类的全局唯一性
(2)保护程序安全,防止核心API被随意篡改
2、劣势
顶层的ClassLoader无法访问底层的ClassLoader所加载的类
比如Tomcat中,类加载器采用先自己加载,如果加载失败,再委派给父类加载器加载,servlet规范推荐使用该做法。
3、自定义类加载器重写loadClass方法,去除双亲委派,那么自定义类加载器可以加载核心类库?
jdk为核心类库提供了一层保护机制,不管自定义加载器,系统类加载器或者扩展类加载器都会调用defineClass方法,该方法会执行preDefineClass()方法,该方法提供对jdk的核心库的保护。
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
六、破坏双亲委派机制
1、线程上下文类加载器
双亲委派模型很好的解决了各个类的加载器协作时基础类型的一致问题(越基础的类越由上层加载器加载),那么如果基础类型需要调用用户代码该如何解决?
比如JNDI服务,它的代码由启动类加载器加载,因为JNDI在jdk1.3加入到rt.jar包中。但JNDI的目的是对资源的查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的classpath下的JNDI服务提供接口(Service Provider Interface,SPI)
为了解决双亲委派模型的缺陷,java设计团队,引入一个不太优雅的设计,就是线程上下文加载器(Thread Context ClassLoader)。
这个类加载器可以通过Thread类的setContextClassLoader()进行设置,如果创建线程时未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过,那么该类加载器默认是应用程序类加载器。
JNDI使用线程上下文加载器加载SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为,该行为打破了双亲委派模型的一般性原则。
和JNDI类似的还有JDBC,JCE,JAXB和JBI,不过当SPI的服务提供者多于一个的时候,代码要使用硬编码判断。为了消除不优雅的实现方式,jdk6提供java.util.ServiceLoader类,以META-INF/services中的配置信息,辅以责任链模式,给SPI加载提供了一种相对合理的解决方案。
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
//设置应用类加载器
Thread.currentThread().setContextClassLoader(this.loader);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
2、代码热替换与模块热部署
(1)模块热部署
IBM公司实现模块化热部署的关键是自定义的类加载器机制的实现,每一个程序模块(OSGI中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现热部署。在OSGI环境下,类加载器不再使用双亲委派机制。
(2)代码热替换
热替换是指程序的运行过程中,不停止服务,只通过替换程序文件来修改程序的行为。
由不同的ClassLoader加载的同名类属于不同的类型,不能相互转换和兼容,根据这个特点,可以实现热替换
七、自定义类加载器
1、目的
(1)隔离加载类
(2)修改类加载的方式
(3)扩展加载源
(4)防止源码泄露
2、实现方式
(1)重写loadClass()方法(不推荐)
loadClass是实现双亲委派模型的逻辑方法,最好不覆写该方法。
(2)重写findClass()方法(推荐)
八、扩展
1、设置启动加载类加载器的加载路径
-Xbootclasspath参数设置
(1)使用新路径替换原有路径
java -Xbootclasspath:<new bootclasspath>
(2)在原有路径后面追加
java -Xbootclasspath/a:<追加的路径>
(3)在原有路径前面追加
这种方式可以替换掉核心类的加载,一般是jvm开发者使用。
java -Xbootclasspath/p:<追加的路径>