Java将class文件,加载到内存中,转换为可以使用的对象的过程就是一个类加载的过程。
类加载过程
类加载过程:加载 -> 连接 -> 初始化。 ⇒ 这个过程是在启动的时候处理的,所以启动的时候会有一些性能开销。
连接过程又可分为三步:验证 -> 准备 -> 解析。
什么时候触发第一个加载的阶段?
- 遇到 new 实例化对象的时候
- 读取第一个static字段的时候,初始化这个class (在编译期将结果放到常量池的字面量除外)
- 调用一个类型的静态方法的时候
- 反射
- 子类初始化的时候,先需要初始化父类 ==》 反之不会初始化
- jdk8的default方法,接口需要先被初始化
类加载的全过程
stage 1 加载
- 按照类的全限定名中获取类的二进制字节流 - 没限制类字节流的来源 - 给了很多的可操作空间
- 来自 zip 包 : jar包
- 网络获取
- 运行时计算 - 动态代理技术
- 其他文件,如 jsp 文件
- 将字节流转换为方法区的运行时的数据结构 — 虽然JDK8开始使用永久代代替了方法区,是的类的存储也在堆中的了,但是这里逻辑上描述还是使用方法区
- 在堆中实例化一个 class 对象 ,作为程序访问方法区中的类型数据的外部接口
也有一些比较特殊的数据结构的加载,比如数组类型
如果数据类型的元素是引用的,那么就是交给类加载器完成
如果是基础类型的,那么就不需要类加载其去加载类文件,那么这个数组就与引导类加载器关联
stage 2 连接 - 验证
对加载的字节流进行基础的格式验证
- 文件格式验证:文件魔术数】常量池的常量类型、常量编码; 保证字节流可以正确解析并且存储到方法区内
- 元数据验证:类是否存在父类、接口是否完全实现了等语义校验
- 字节码验证:程序流分析和数据流分析,判断语义是否合法;
- 符号引用验证:类是否缺少外部依赖类、方法、资源等; IllegalAccessErrir , NoSuchFieldError , NoSuchMethodError
stage 3 连接 - 准备
为类中的变量、静态变量分配内存,设置类变量的初始值 - JDK8之后这里就是在堆中了
没有final static 修改的字面量的数据初始化在这里重新计算和分配空间
如果是 final static 修饰的字面量 , javac 阶段已经将内容存储在了 ConstantValue 属性里面了,这里直接赋值就可以了
stage 4 连接 - 解析
将常量池中的符号引用替换为直接引用的过程
stage 5 初始化
在加载和连接阶段,已经将涉及到的class全部加载完成了 ,这时应该是部分完成初始化
在初始化阶段,JVM开始真正执行主程序代码,开始初始化运行期的class内容了,将主导权交给了应用程序
类加载器
使用类的全限定名获取二进制流的操作独立到虚拟机外部去做,这个逻辑为类加载器完成。
一个 class 由不同的类加载器完成加载到虚拟机内部,那么本质上说就是两个不同的class对象。
JVM 中内置了三个重要的 ClassLoader,除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader
:
- BootstrapClassLoader(启动类加载器) :最顶层的加载类,由 C++ 实现,负责加载
%JAVA_HOME%/lib
目录下的 jar 包和类或者被Xbootclasspath
参数指定的路径中的所有类。 - ExtensionClassLoader(扩展类加载器) :主要负责加载
%JRE_HOME%/lib/ext
目录下的 jar 包和类,或被java.ext.dirs
系统变量所指定的路径下的 jar 包。 - AppClassLoader(应用程序类加载器) :面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
双亲委派模型
每一个类都有一个对应它的类加载器。系统中的 ClassLoader 在协同工作的时候会默认使用 双亲委派模型 。
即在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回(存在缓存机制),否则才会尝试加载。加载的时候,首先会把该请求委派给父类加载器的 loadClass()
处理,因此所有的请求最终都应该传送到顶层的启动类加载器 BootstrapClassLoader
中。
当父类加载器无法处理时,才由自己来处理。当父类加载器为 null 时,会使用启动类加载器 BootstrapClassLoader
作为父类加载器。
public Class<?> loadClass(String name)throws ClassNotFoundException {
return loadClass(name, false);
}
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 {
//如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native Class findBootstrapClass(String name)
c = findBootstrapClass0(name);
}
} catch (ClassNotFoundException e) {
// 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
每个类加载都有一个父类加载器,我们通过下面的程序来验证。
public class ClassLoaderDemo {
public static void main(String[] args) {
System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
}
}
Output
ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@1b6d3586
The GrandParent of ClassLodarDemo's ClassLoader is null
AppClassLoader
的父类加载器为ExtClassLoader
, ExtClassLoader
的父类加载器为 null,null 并不代表ExtClassLoader
没有父类加载器,而是 BootstrapClassLoader
The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.
对于tomcat
class org.apache.catalina.loader.ParallelWebappClassLoader class java.net.URLClassLoader class sun.misc.LauncherExtClassLoader
双亲委派模型保证了 Java 程序的稳定运行,可以避免类的重复加载(JVM 区分不同类的方式不仅仅根据类名,相同的类文件被不同的类加载器加载产生的是两个不同的类),也保证了 Java 的核心 API 不被篡改。如果没有使用双亲委派模型,而是每个类加载器加载自己的话就会出现一些问题,比如我们编写一个称为 java.lang.Object
类的话,那么程序运行的时候,系统就会出现多个不同的 Object
类。
类的加载
类加载有三种方式:
1、命令行启动应用时候由JVM初始化加载 2、通过Class.forName()方法动态加载 3、通过ClassLoader.loadClass()方法动态加载
package com.pdai.jvm.classloader;
public class loaderTest {
public static void main(String[] args) throws ClassNotFoundException {
ClassLoader loader = HelloWorld.class.getClassLoader();
System.out.println(loader);
//使用ClassLoader.loadClass()来加载类,不会执行初始化块
loader.loadClass("Test2");
//使用Class.forName()来加载类,默认会执行初始化块
// Class.forName("Test2");
//使用Class.forName()来加载类,并指定ClassLoader,初始化时不执行静态块
// Class.forName("Test2", false, loader);
}
}
public class Test2 {
static {
System.out.println("静态初始化块执行了!");
}
}
分别切换加载方式,会有不同的输出结果。
Class.forName()和ClassLoader.loadClass()区别?
- Class.forName(): 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
- ClassLoader.loadClass(): 只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行static块。
- Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance()方法采用调用构造函数,创建类的对象 。
破坏双亲委派模型 - loadClass
load -> find -> define
如果不打破双亲委派机制,重写findClass方法即可
如果打破双亲委派机制,重写整个loadClass方法
破坏双亲委派模型 - SPI
JNDI 是在 JDK1.3的时候加入到 rt.jar , 对资源进行查找和集中管理 , 需要调用其他厂商的 classpath 下面的 SPI 接口代码。
但是因为 JNDI 管理类是一个基础类,由 BootstracpClassloader 加载的, 但是 SPI 是 AppClassloader 加载到的。
于是引入了线程上下文加载器 : ContextClassLoader 是一种与线程相关的类加载器,类似 ThreadLocal,每个线程对应一个上下文类加载器,主要是用了打破类加载器中的委托机制的。 即在 Thread 类里面加一个线程的上下文(默认是 AppClassLoader),方便其他的类加载器加载的class获取到这个上下文,如JNDI , 以此来完成对SPI 的加载。获取到加载的内存class对象。
此时即当SPI只有一个的时候,可以直接硬编码获取到上下文的 AppClassLoader 完成对 Driver的加载获取到 class , 然后被JNDI 管理。
但是当SPI存在多个,如多个驱动的时候,就无法完成了 直到在JDK6 的时候,使用 ServicelLoader机制,加载 META-INF/services 里面的配置信息完成 SPI 代码的加载
ServiceLoader<Driver> load = ServiceLoader.load(Driver.class);
上面的内部实现中,其实也是获取到ContextClassLoader完成加载,不同的是,SPI的配置允许加载多个Driver了
public final class ServiceLoader<S> implements Iterable<S> {
public static <S> ServiceLoader<S> load(Class<S> service) {
// (5)获取当前线程上下文加载器
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);
}
//(6)
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = svc;
loader = cl;
reload();
}
破坏双亲委派模型 - OSGi
OSGI实现了模块级的热插拔,每一个Bundle都可以声明自己import的package 和自己 export 的package。那么对于类加载,自己import , 肯定是交给export它的bundle的类加载器完成加载。
使用自定义类加载机制,让每一个 Bundle 都有一个自己的类加载器,需要替换BUndle的时候,就把Bundle和类加载器一个替换掉。
此时,类加载不再是树结构了,而是网状结构了。
那么这个结构,可能就会出现A B 两个bundle相互依赖,loadclass 又是一个同步方法,很容易出现死锁。
JDK7之后,ClassLoader增加了 registerAsParallelCapable 方法对并行的类加载注册声明,将锁级别从Classloader降低到加载的这个类名的级别。
双亲委派的案例 - Tomcat
对于web服务器,各个不同的web程序的类加载器都有一个公共的jar需要加载,这些jar一般都是使用一个公共的类加载器完成加载,不然会浪费内存中的方法区空间。
tomcat6之后,/lib下面就是存储这些jar的位置;
对于WEB-iNF里面的,很显然就是web程序自己的classloader完成加载,其父亲就是lib的classloader。
对于JSP文件也是如此,JSP是文本文件,修改后重新替换实现的原理就是每一个JSP文件都存在一个子的JasperLoader,当检测到JSP修改的时候,会替换掉目前JasperLoader的实例来实现HotSpot
URLClassloader
源码我们可以发现AppClassLoader和ExtClassLoader都是Launcher的静态内部类,继承自URLClassLoader
class URLClassLoader extends SecureClassLoader implements Closeable
- SecureClassLoader:扩展了ClassLoader,并为定义具有相关代码源和权限的类提供了额外支持,这些代码源和权限默认情况下由系统策略检索。
- URLClassLoader:继承自SecureClassLoader,支持从jar文件和文件夹中获取class,继承于classloader,加载时首先去classloader里判断是否由启动类加载器加载过。
推荐阅读
https://blog.csdn.net/xyang81/article/details/7292380
https://juejin.im/post/5c04892351882516e70dcc9b
http://gityuan.com/2016/01/24/java-classloader/
https://jawhiow.github.io/2019/04/24/java/详解常用类加载器:ContextClassLoader/