Java类加载实战可参考:https://mp.weixin.qq.com/s/16MKwhwPHVNezxS5ygk-cA

1. 类加载机制的层次结构

类加载机制的层次结构

1.加载:加载阶段会在内存中生成一个代表该类的Class对象,作为访问方法区该类各种数据的入口。加载阶段,虚拟机完成以下工作:

  • 通过一个类的全限定名来获取其定义的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

注意:虚拟机规范并没有指明二进制字节流要从一个Class文件获取,也可以从jar包或war包中读取,或从网络中获取(最典型的应用就是Applet),也可以在运行时生成(动态代理),也可以由其它文件生成(比如将JSP文件转换成对应的Class类)。

2.验证:验证阶段目的在于确保Class文件的字节流中包含信息符合当前虚拟机要求,并且不会危害虚拟机自身安全。主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。

  1. 文件格式验证:验证class文件格式规范,例如: class文件是否以魔数0xCAFEBABE开头 ;主、次版本号是否在当前虚拟机处理范围之内;常量池的常量中是否有不被支持的常量类型(检查常量tag标志)等。

  2. 元数据验证:这个阶段是对字节码描述的信息进行语义分析,以保证描述的信息符合java语言规范要求,例如:这个类是否有父类(除了java.lang.Object之外,所有类都应当有父类);这个类是否继承了不允许被继承的类(被final修饰的类)等。

  3. 字节码验证:进行数据流和控制流分析,确定程序语义是合法的、符合逻辑的,这个阶段对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的行为,例如:保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作(出现指定int类型却加载long类型的本地变量);保证跳转指令不会跳转到方法体以外的字节码指令上等。

  4. 符号引用验证:符号引用验证可以看作是类对自身以外(常量池中的各种符号引用)的信息进行匹配性校验,例如:符号引用中通过字符串描述的全限定名是否能找到对应的类、符号引用类中的类;字段和方法的访问性(private、protected、public、default)是否可被当前类访问等。

    更详细的可以参考博客:https://blog.csdn.net/dufufd/article/details/80538527

3.准备:准备阶段是正式为类变量分配内存并设置类变量的初始值,即在方法区中分配这些变量所使用的内存空间。

注意:这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化的时候随着对象一起分配在Java堆中。

4.解析:解析阶段是指虚拟机将常量池中的符号引用替换为直接引用的过程。这一阶段JVM针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符 7 类引用进行解析。

  • 符号引用:在Java中,在编译时,java类并不知道所引用的类的实际地址,因此只能使用符号引用来代替,符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
  • 直接引用:直接指向目标的指针(比如指向Class对象、类变量、类方法的直接引用);相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量);一个能间接定位到目标的句柄。

5.初始化:初始化阶段是执行类构造器方法的过程。方法是由编译器自动收集类中的类变量的赋值操作和静态语句块中的语句。虚拟机会保证方法执行之前,父类的方法已经执行完毕。

注意以下几种情况不会执行类初始化:

  • 通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化。
  • 定义对象数组,不会触发该类的初始化。
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类。
  • 通过类名获取Class对象,不会触发类的初始化。
  • 通过Class.forName加载指定类时,如果指定参数initialize为false时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化。
  • 通过ClassLoader默认的loadClass方法,也不会触发初始化动作。

2. 类加载器
虚拟机设计团队把加载动作放到JVM外部实现,以便让应用程序决定如何获取所需的类,JVM提供了3种类加载器:

  • 启动类加载器(Bootstrap ClassLoader):负责加载JAVA_HOME\lib目录中的,或通过-Xbootclasspath参数指定路径中的,且被虚拟机认可的类(按文件名识别,如rt.jar,所有java.*开头的类)。
  • 扩展类加载器(Extension ClassLoader):负责加载JAVA_HOME\lib\ext目录中的,或通过java.ext.dirs系统变量指定路径中的类库(如javax.*开头的类)。
  • 应用程序类加载器(Application ClassLoader):负责加载用户路径(classpath)上的类库。

注意:可以通过继承java.lang.ClassLoader实现自定义的类加载器。

这几种类加载器的层次关系如下图所示:

3. 双亲委派模型的过程以及优势。

双亲委派模型的过程:某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才由自己加载。

双亲委派模型的系统实现

在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法(若父加载器为空则默认使用启动类加载器作为父加载器)。如果父加载失败,则捕获ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。

protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) {
        //check the class has been loaded or not
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //if throws the exception ,the father can not complete the load
            }
            if (c == null) {
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

双亲委派模型的优势

Java类随着它的类加载器一起具备了带有优先级的层次关系。例如类java.lang.Object,它存在于rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。

参考链接:两道面试题,带你透彻解析Java类加载机制

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐