参考资料:
Java 类加载机制(阿里面试题)
从经典面试题看java中类的加载机制
面试官:请你谈谈Java的类加载过程
深入理解Java类加载器(ClassLoader)
类的加载是由类加载器完成的,类加载器包括:启动类加载器(BootStrap)、扩展类加载器(ExtClassLoader)、应用程序类加载器(AppClassLoader)和自定义类加载器(java.lang.ClassLoader的子类)。
一般用本地代码实现,负责加载JVM基础核心类库,即 JAVA_HOME\lib 目录下的类。
继承自启动类加载器,加载 \lib\ext 下的类,或者被 java.ext.dirs 系统变量指定的类。
继承自扩展类加载器,加载 ClassPath 中的类,或者系统变量 java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。
继承自 ClassLoader 类。
为什么要自定义类加载器
一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。
另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。
当一个类加载器负责加载某个 Class 时,该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显式指定另外一个类加载器来载入。
如果一个类加载器收到了 Class 加载的请求,它首先不会自己去尝试加载这个 Class ,而是把请求委托给父加载器去完成,依次向上。因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的 Class 时,即无法完成该加载,子加载器才会尝试自己去加载该 Class 。
这样做的好处是:
1. 避免同一个类被多次加载
2. 安全,Java 核心 API 中定义的类不会被随意替换
3. 每个加载器只能加载自己范围内的类
所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区寻找该 Class ,只有当缓存区不存在时,系统才会去读取该 Class 对应的二进制数据,并将其转换成 Class 对象,存入缓存区。
这就是为什么修改了 Class 后,必须重启JVM,程序的修改才会生效。
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先从缓存查找该class对象,找到就不用重新加载
Class> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果找不到,则委托给父类加载器去加载
c = parent.loadClass(name, false);
} else {
//如果没有父类,则委托给启动加载器去加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果都没有找到,则通过自定义实现的findClass去查找并加载
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;
}
}
流程:
缓存 -> 父类加载器 -> 没有父类 -> 启动类加载器 -> 自己的 findClass() 方法
由自己负责加载类的方法。
在自定义类加载器时,需要重写该方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的 Class 对象。
将 byte 字节流解析成 JVM 能够识别的 Class 对象。
解析 Class 对象,即将字节码文件中的符号引用转换为直接引用。
符号引用与直接引用
符号引用:即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。
直接引用:可以理解为一个内存地址,或者一个偏移量。
举个例子,现在调用方法 hello(),这个方法的地址是 1234567 ,那么 hello 就是符号引用,1234567 就是直接引用。
类加载分为三个步骤:加载,连接,初始化
根据一个类的全限定名(如 java.lang.String )来读取该类的二进制字节流,解析成 JVM 能够识别的 Class 对象。
确保 Class 文件的字节流中包含信息符合虚拟机要求,不会危害虚拟机的安全。
主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。
为类的静态变量分配内存并且设置初始值,这里的初始值指的是不同类型的默认值,如 int 默认值为0,引用的默认值为 null。
而 final 修饰的静态常量,因为 final 在编译的时候就会分配了,所以此时的值为代码中设置的值。
注意
类的静态变量会分配在方法区中,而实例变量是随着对象一起分配到 Java 堆中。
将常量池内的符号引用替换为直接引用。
将静态变量和静态方法块按顺序从上到下初始化,即为准备阶段的静态变量重新赋值,设置为代码中指定的值。
执行构造函数。
如果该类具有父类,先初始化父类。