类加载器
把实现类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作的代码模块称为“类加载器”。
将 class 文件二进制数据放入方法区内,然后在堆内(heap)创建一个 java.lang.Class 对象,Class 对象封装了类在方法区内的数据结构,并且向开发者提供了访问方法区内的数据结构的接口。
目前类加载器却在类层次划分、OSGi、热部署、代码加密等领域非常重要,我们运行任何一个 Java 程序都会涉及到类加载器。
类的唯一性和类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。
即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类也不相等。
这里所指的“相等”,包括代表类的 Class 对象的 equals() 方法、 isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。
双亲委派模型
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
这里类加载器之间的父子关系一般不会以继承(Inheritance)的关系来实现,而是都使用组合(Composition)关系来复用父加载器的代码。
Bootstrap 类加载器是用 C++ 实现的,是虚拟机自身的一部分,如果获取它的对象,将会返回 null;扩展类加载器和应用类加载器是独立于虚拟机外部,为 Java 语言实现的,均继承自抽象类 java.lang.ClassLoader ,开发者可直接使用这两个类加载器。
Application 类加载器对象可以由 ClassLoader.getSystemClassLoader()
方法的返回,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
双亲委派模型对于保证 Java 程序的稳定运作很重要,例如类 java.lang.Object
,它存放在 rt.jar 之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。
双亲委派模型的加载类逻辑可参考如下代码:
// 代码摘自《深入理解Java虚拟机》
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
// 首先,检查请求的类是否已经被加载过了
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器抛出ClassNotFoundException
// 说明父类加载器无法完成加载请求
}
if (c == null) {
// 在父类加载器无法加载的时候
// 再调用本身的findClass方法来进行类加载
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
破坏双亲委派模型
双亲委派模型主要出现过 3 较大规模的“被破坏”情况。
- 双亲委派模型在引入之前已经存在破坏它的代码存在了。
双亲委派模型在 JDK 1.2 之后才被引入,而类加载器和抽象类java.lang.ClassLoader
则在 JDK 1.0 时代就已经存在,JDK 1.2之后,其添加了一个新的 protected 方法findClass()
,在此之前,用户去继承 ClassLoader 类的唯一目的就是为了重写loadClass()
方法,而双亲委派的具体逻辑就实现在这个方法之中,JDK 1.2 之后已不提倡用户再去覆盖loadClass()
方法,而应当把自己的类加载逻辑写到findClass()
方法中,这样就可以保证新写出来的类加载器是符合双亲委派规则的。 - 基础类无法调用类加载器加载用户提供的代码。
双亲委派很好地解决了各个类加载器的基础类的统一问题(越基础的类由越上层的加载器进行加载),但如果基础类又要调用用户的代码,例如 JNDI 服务,JNDI 现在已经是 Java 的标准服务,它的代码由启动类加载器去加载(在 JDK 1.3 时放进去的 rt.jar ),但 JNDI 的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的 ClassPath 下的 JNDI 接口提供者(SPI,Service Provider Interface,例如 JDBC 驱动就是由 MySQL 等接口提供者提供的)的代码,但启动类加载器只能加载基础类,无法加载用户类。
为此 Java 引入了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过
java.lang.Thread.setContextClassLoaser()
方法进行设置,如果创建线程时还未设置,它将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。
如此,JNDI 服务使用这个线程上下文类加载器去加载所需要的 SPI 代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java 中所有涉及 SPI 的加载动作基本上都采用这种方式,例如 JNDI、JDBC、JCE、JAXB 和 JBI 等。
- 用户对程序动态性的追求。
代码热替换(HotSwap)、模块热部署(Hot Deployment)等,OSGi 实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个 Bundle 时,就把 Bundle 连同类加载器一起换掉以实现代码的热替换。
在 OSGi 环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当收到类加载请求时,OSGi 将按照下面的顺序进行类搜索:
1)将以 java.* 开头的类委派给父类加载器加载。
2)否则,将委派列表名单内的类委派给父类加载器加载。
3)否则,将 Import 列表中的类委派给 Export 这个类的 Bundle 的类加载器加载。
4)否则,查找当前 Bundle 的 ClassPath,使用自己的类加载器加载。
5)否则,查找类是否在自己的 Fragment Bundle 中,如果在,则委派给 Fragment Bundle 的类加载器加载。
6)否则,查找 Dynamic Import 列表的 Bundle,委派给对应 Bundle 的类加载器加载。
7)否则,类查找失败。
上面的查找顺序中只有开头两点仍然符合双亲委派规则,其余的类查找都是在平级的类加载器中进行的。OSGi 的 Bundle 类加载器之间只有规则,没有固定的委派关系。
自定义类加载器
Java 默认 ClassLoader,只加载指定目录下的 class,如果需要动态加载类到内存,例如要从远程网络下来类的二进制,然后调用这个类中的方法实现我的业务逻辑,如此,就需要自定义 ClassLoader。
自定义类加载器分为两步:
- 继承 java.lang.ClassLoader
- 重写父类的 findClass() 方法
针对第 1 步,为什么要继承 ClassLoader 这个抽象类,而不继承 AppClassLoader 呢?
因为它和 ExtClassLoader 都是 Launcher 的静态内部类,其访问权限是缺省的包访问权限。
static class AppClassLoader extends URLClassLoader{...}
第 2 步,JDK 的 loadCalss()
方法在所有父类加载器无法加载的时候,会调用本身的 findClass()
方法来进行类加载,因此我们只需重写 findClass()
方法找到类的二进制数据即可。
下面我自定义了一个简单的类加载器,并加载一个简单的类。
首先是需要被加载的简单类:
// 存放于D盘根目录
public class Test {
public static void main(String[] args) {
System.out.println("Test类已成功加载运行!");
ClassLoader classLoader = Test.class.getClassLoader();
System.out.println("加载我的classLoader:" + classLoader);
System.out.println("classLoader.parent:" + classLoader.getParent());
}
}
并使用 javac -encoding utf8 Test.java
编译成 Test.class 文件。
类加载器代码如下:
import java.io.*;
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 加载D盘根目录下指定类名的class
String clzDir = "D:\\" + File.separatorChar
+ name.replace('.', File.separatorChar) + ".class";
byte[] classData = getClassData(clzDir);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String path) {
try (InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream()
) {
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
使用类加载器加载调用 Test 类:
public class MyClassLoaderTest {
public static void main(String[] args) throws Exception {
// 指定类加载器加载调用
MyClassLoader classLoader = new MyClassLoader();
classLoader.loadClass("Test").getMethod("test").invoke(null);
}
}
输出信息:
Test.test()已成功加载运行!
加载我的classLoader:class MyClassLoader
classLoader.parent:class sun.misc.Launcher$AppClassLoader
线程上下文类加载器
如上所说,为解决基础类无法调用类加载器加载用户提供代码的问题,Java 引入了线程上下文类加载器(Thread Context ClassLoader)。这个类加载器默认就是 Application 类加载器,并且可以通过 java.lang.Thread.setContextClassLoaser()
方法进行设置。
// Now create the class loader to use to launch the application
try {
loader = AppClassLoader.getAppClassLoader(extcl);
} catch (IOException e) {
throw new InternalError(
"Could not create application class loader" );
}
// Also set the context class loader for the primordial thread.
Thread.currentThread().setContextClassLoader(loader);
那么问题来了,我们使用 ClassLoader.getSystemClassLoader()
方法也可以获取到 Application 类加载器,使用它就可以加载用户类了呀,为什么还需要线程上下文类加载器?
其实直接使用 getSystemClassLoader()
方法获取 AppClassLoader 加载类也可以满足一些情况,但有时候我们需要使用自定义类加载器去加载某个位置的类时,例如Tomcat 使用的线程上下文类加载器并非 AppClassLoader ,而是 Tomcat 自定义类加载器。
以 Tomcat 为例,其每个 Web 应用都有一个对应的类加载器实例,该类加载器使用代理模式,首先尝试去加载某个类,如果找不到再代理给父类加载器这与一般类加载器的顺序是相反的。
这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。
更多关于 Tomcat 类加载器的知识,这里暂时先不讲了。
new一个对象过程中发生了什么?
- 确认类元信息是否存在。当 JVM 接收到 new 指令时,首先在 metaspace 内检查需要创建的类元信息是否存在。 若不存在,那么在双亲委派模式下,使用当前类加载器以 ClassLoader + 包名+类名为 Key 进行查找对应的 class 文件。 如果没有找到文件,则抛出 ClassNotFoundException 异常 , 如果找到,则进行类加载(加载 - 验证 - 准备 - 解析 - 初始化),并生成对应的 Class 类对象。
- 分配对象内存。 首先计算对象占用空间大小,如果实例成员变量是引用变量,仅分配引用变量空间即可,即 4 个字节大小,接着在堆中划分—块内存给新对象。 在分配内存空间时,需要进行同步操作,比如采用 CAS (Compare And Swap) 失败重试、 区域加锁等方式保证分配操作的原子性。
- 设定默认值。 成员变量值都需要设定为默认值, 即各种不同形式的零值。
- 设置对象头。设置新对象的哈希码、 GC 信息、锁信息、对象所属的类元信息等。这个过程的具体设置方式取决于 JVM 实现。
- 执行 init 方法。 初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
最后,推荐与感谢:
深入理解Java虚拟机(第2版)
码出高效:Java开发手册
java new一个对象的过程中发生了什么 - 天风的文章 - 知乎
深入探讨类加载器
Class.forName()用法详解
真正理解线程上下文类加载器(多案例分析)
类加载相关链接
回到目录:深入理解Java虚拟机(全解析)
(转载自:https://www.cnblogs.com/czwbig/p/11127222.html)