一. 什么是类的加载
java文件经过编译,变成了.class文件。之后类加载会将.class文件加载至java虚拟机的内存区域(存在于方法区),之后在堆区创建java.lang.Class对象,用来封装类在方法内的数据结构。所以其实类装载器的作用就是类的加载。
二. 类加载过程
类从被加载到虚拟机内存开始,到卸载出内存为止,它的生命周期包括:加载,验证,准备,解析,初始化,使用和卸载七个阶段。
其中类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。
下面就一个一个去分析一下这几个过程。
加载:
1)通过“类全名”来获取此定义类的二进制流。
2)在字节流所代表的静态存储结构转换为方法区的运行时数据结构。
3)堆中生成一个class对象,作为方法区数据的访问入口。
加载阶段是通过 ClassLoader来完成的,类加载器可以由用户自定义完成。所以开发人员可以通过定义类加载器去控制字节流的获取方式。加载之后,二进制文件会被读入到虚拟机所需的格式存储在方法区中,方法区中存储格式由虚拟机自己定义,然后在 java的堆中实例化一个 java.lang.class类对象,通过这个对象访问方法区中的数据。
验证:
验证阶段是链接阶段的第一步,目的就是确保class文件的字节流中包含的信息符合虚拟机的要求,不能危害虚拟机自身安全。验证阶段主要包括四个检验过程:文件格式验证、元数据验证、字节码验证和符号引用验证。
1)文件格式验证
验证class文件格式规范
2)元数据验证
就是对字节码描述的信息进行语义分析,保证描述的信息符合java语言规范。验证点可能包括(这个类是否有父类(除Object)、这个类是否继承了不允许被继承的类(final修饰的)、如果这个类的父类是抽象类,是否实现了父类或接口中要求实现的方法)。
3)字节码验证
进行数据流和控制流分析,这个阶段对类的方法体进行校验,保证被校验的方法在运行时不会做出危害虚拟机的行为。
4)符号引用验证
符号引用中通过字符串描述的权限定名是否能找到对应的类、符号引用类中的类,字段和方法的访问性(private protected public default)是否能被当前类访问。
此时发生在运行期,但是有些验证其实在编译器就会做了。比如受检异常。运行期再做一遍的原因是编译器生成的class文件,可能会被替换。
准备:
此阶段就是给类变量分配内存并设置初始值的阶段。这里指的是static修饰的类变量。 pulic static int a = 10; 此时,会给变量a分配 a = 0的初始值。这个操作就是在准备阶段完成的。但是如果此时为 final修饰,则会直接赋值。
解析:
解析阶段是虚拟机常量池的符号引用替换为直接引用的过程。(好抽象 ...)
符号引用:符号引用是一组符号来描述所用的对象,符号可以是任何形式的字面量,只要使用时能定位到目标即可,符号引用与虚拟机实现的内存布局无关,引用的目标对象并不一定加载到内存中。
直接引用:直接引用可以是直接指向目标对象的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机内存布局实现相关,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同,如果有了直接引用,那引用的目标必定在内存中。
解析过程主要针对类或接口,字段,类方法,接口方法四类符号等引用进行。
上面的解释其实太过于抽象。简单点的举个栗子:假设A对象中有个字段引用了B对象。
例如:
Object b = new Object();
编译时,此时A类编译并不知道b对象的内存地址是什么。(编译器没有分配内存空间)此时就暂时用一个符号暂时表示b对象地址。在运行期,此时b对象在堆内存中已经实际存在,此时就直接引用b对象的地址。
这就是符号引用 -> 直接引用。
现在通过javap来解析一下类文件,分析一下字节码指令,更深入了解一下。
javap -c TestClassLoader.class > D:\testClassLoader.txt
java代码如下:
public class TestClassLoader {
Object object;
{
object = new Object();
}
public static void main(String[] args) {
Object object = new Object();
System.out.println(object);
}
}
解析的类文件信息如下图所示:
可以看到 在main方法指令的 第7行处 有一个astore的指令。这个指令指的是将引用地址从操作数栈取出,放入局部变量表。 此时能很明显的明白,这个时候是有引用地址的,而引用地址则表示直接引用。
三. 双亲委派机制
当某个类加载器需要加载某个class文件的时候,首先会把这个任务委托给他的上级类加载器,递归这个操作,如果上级的类加载器没有加载,自己才会去加载这个类。
类加载器的类别
BootstrapClassLoader(启动类加载器)
c++
编写,加载java
核心库 java.*
,构造ExtClassLoader
和AppClassLoader
。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作
ExtClassLoader (标准扩展类加载器)
java
编写,加载扩展库,如classpath
中的jre
,javax.*
或者 java.ext.dir
指定位置中的类,开发者可以直接使用标准扩展类加载器。
AppClassLoader(系统类加载器)
java编写,加载程序所在的目录,如`user.dir`所在的位置的class
CustomClassLoader(用户自定义类加载器)
java
编写,用户自定义的类加载器,可加载指定路径的class
文件
这里瞅瞅源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查这个classsh是否已经加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// c==null表示没有加载,如果有父类的加载器则让父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果父类的加载器为空 则说明递归到bootStrapClassloader了
//bootStrapClassloader比较特殊无法通过get获取
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
//如果bootstrapClassLoader 仍然没有加载过,则递归回来,尝试自己去加载class
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) {
resolveClass(c);
}
return c;
}
}
没有问题!通过分析源码不难发现,当去加载.class文件时,首先判断一下这个class是否已经load了。如果没有load,此时就会找到父加载器,调用父加载器的loadClass方法(此时存在递归调用) 当找到根加载器也没有加载过的时候,这个时候就自己去试试看能不能加载这个class文件。
双亲委派机制的作用:
1、防止重复加载同一个.class
。通过委托去向上面问一问,加载过了,就不用再加载一遍。
2、保证安全因素。譬如此时有人也恶意的定义了一个 java.lang.String的Class文件。这个类含有恶意代码,此时jvm将其加载至内存时,发现此时java.lang.String已经被加载。(已经加载了核心库的类加载器)这样保护了核心类库代码不会被随意篡改。
我新建了一个java.lang.String的类。编写了main方法。但是此时运行的时候会报如下错误:
这是为什么呢?
原因其实很简单:由于双亲委派机制,此时java.lang.string的class文件已经由根加载器加载了,此时你自定义的这个java.lang.String直接沿用了 jdk提供的 String.class。