- 类加载过程:加载->连接->初始化。
- 连接过程又可分为三步:验证->准备->解析。
加载是类加载过程的第一步,主要完成下面 3 件事情:
- 通过全类名获取定义此类的二进制字节流
- 将字节流所代表的静态存储结构转换为方法区的运行时数据结构
- 在内存中生成一个代表该类的 Class 对象,作为方法区这些数据的访问入口
类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。
每个 Java 类都有一个引用指向加载它的 ClassLoader。不过,数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。
- 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
- 每个 Java 类都有一个引用指向加载它的 ClassLoader。
- 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
类加载器的主要作用是加载Java类的字节码(.class文件)到JVM中,在内存中生成一个代表该类的class对象.
字节码可以是由Java源程序(.java文件)经过javac编译得到的.class文件,可以是通过工具动态生成或者是网络下载得来.
类加载器加载规则
JVM在启动时,不会一次性加载所有的类,而是根据需要动态的加载.大部分类在具体用到的时候才会去加载,这样可以减少内存消耗;
已经加载过的类会放在ClassLoader中,在类加载的时候,系统会首先判断当前类是否被加载过.已经被加载过的类会直接返回,否则进行加载;
对于一个类加载器而言,相同二进制名称的类,只会进行一次加载;
JVM内置三个重要的ClassLoader:
- BootstrapClassLoader(启动类加载器):最顶层的加载类,由 C++实现,通常表示为 null,并且没有父级,主要用来加载 JDK 内部的核心类库( %JAVA_HOME%/lib目录下的 rt.jar、resources.jar、charsets.jar等 jar 包和类)以及被 -Xbootclasspath参数指定的路径下的所有类。
- ExtensionClassLoader(扩展类加载器):主要负责加载 %JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类。
- AppClassLoader(应用程序类加载器):面向我们用户的加载器,负责加载当前应用 classpath 下的所有 jar 包和类。
rt.jar: rt代表RunTime , rt.jar时Java的基础类库,包含常用的内置库;
除了这三种类加载器,用户可以自定义类加载器,可以对Java类进行加密,加载时使用自定义类加载器进行解密加载.
除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。
每个 ClassLoader 可以通过getParent()获取其父 ClassLoader,如果获取到 ClassLoader 为null的话,那么该类是通过 BootstrapClassLoader 加载的。因为BootstrapClassLoader是由C++实现,所以在Java中没有与之对应的类;
class PrintClassLoaderTree {
public static void main(String[] args) {
ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();
StringBuilder split = new StringBuilder("|--");
boolean needContinue = true;
while (needContinue){
System.out.println(split.toString() + classLoader);
if(classLoader == null){
needContinue = false;
}else{
classLoader = classLoader.getParent();
split.insert(0, "\t");
}
}
}
}
结果:
自定义加载类:
除了BootstrapClassLoader其他类加载器都是由Java实现的,继承于java.lang.ClassLoader;
ClassLoader类有两个关键的方法:
- protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制 。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class<?> c) 方法解析该类。
- protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。
如果我们不想打破双亲委派模型,就重写 ClassLoader 类中的 findClass() 方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法
双亲委派模型介绍:
ClassLoader 类使用委托模型来搜索类和资源。每个 ClassLoader 实例都有一个相关的父类加载器。需要查找类或资源时,ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
虚拟机中被称为 "bootstrap class loader"的内置类加载器本身没有父类加载器,但是可以作为 ClassLoader 实例的父类加载器。
从上面的介绍可以看出:
- ClassLoader 类使用委托模型来搜索类和资源。
- 双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。
- ClassLoader 实例会在试图亲自查找类或资源之前,将搜索类或资源的任务委托给其父类加载器。
下图展示的各种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。
双亲委派模型不是强制性约束,只是JDK官方推荐,双亲委派模型的意思是把任务委托给其父类加载器;
类加载器的父子关系一般不是以继承的关系来实现的;而是通常使用组合关系来复用父类加载器的代码
面向对象编程: 组合优于继承 ,多用组合少用继承;
执行流程:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
//用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}
- 在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载(每个父类加载器都会走一遍这个流程)。
- 类加载器在进行类加载的时候,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成(调用父加载器 loadClass()方法来加载类)。这样的话,所有的请求最终都会传送到顶层的启动类加载器 BootstrapClassLoader 中。
- 只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载(调用自己的 findClass() 方法来加载类)。
- 如果子类加载器也无法加载这个类,那么它会抛出一个 ClassNotFoundException 异常。
JVM 判定两个 Java 类是否相同的具体规则:JVM 不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。即使两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相同;
双亲委派模型保证Java程序稳定运行,可以避免类的重复加载,也保证了Java核心的API不被篡改,主要在于一旦父类加载器发现以及加载过这个类,就会直接返回,而不是让子类加载器加载;