前面的文章已经介绍过很多关于JVM、线程方面的相关知识了,这是都是在面试中很容易被问到的,之后还会介绍GC调优,分布式事务,SQL锁等相关知识。希望大家可以在一起多交流交流,能够多学一点东西。
今天介绍一下类加载过程和双亲委派模型。
概述
一般来说,Java类加载过程分为三个主要步骤:加载、链接、初始化。接下来,对这三个方面简要介绍一下:
1、加载
是Java将字节码数据从不同的数据源中读取到JVM中的,并且映射为JVM可以识别的数据结构(Class对象),这里的数据源,可以是jar,可以是class文件,甚至是网络数据源;如果输入数据不是ClassFile的结构,就会抛出异常ClassFormatError。
加载阶段是用户参与的,可以自定义类加载器,实现自己的类加载过程。
2、链接
这是核心的步骤,简单来说就是把原始的类定义信息平滑的转入JVM运行的过程中,这里分为三步:
- 验证,这是JVM安全的保障,JVM需要检验字节码的数据结构是否满足要求,不然就抛出VerifyError,这样就防止了恶意信息或者不合规的信息危害JVM的运行,验证阶段有可能触发更多的class加载。
- 准备,创建类或接口的静态变量,并且初始化静态变量的初始值。但是这里的“初始化”和下面的显示初始化是有区别的,侧重点在于分配所需要的内存空间不会去执行更进一步的JVM指令。
- 解析,在这一步将常量池中的符号引用替换为直接引用。
3、初始化
这一步真正执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义的静态初始化块中的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优于当前类型的逻辑。
双亲委派模型
简单的说就是当类加载器(Class-Loader)试图加载某个类型的时候,除非父加载器找不到相应类型,否则尽量将这个任务代理给当前加载器的父加载器。使用委派模型的目的是避免重复加载Java类型。
深入分析
准备阶段谈到静态变量,那么对于常量和不同静态变量有什么区别?
请看下面代码:
public static int a = 100;
public static final int INT_CONSTANT = 1000;
public static final Integer INTEGER_CONSTANT = Integer.valueOf(10000);
普通原始类型静态变量和引用类型(即使是常量),是需要额外调用putstatic等JVM指令的,这些是在显示初始化阶段执行,而不是在准备阶段调用;而原始类型常量,则不需要这样的步骤。
关于类加载的过程的更多细节,可以参考如下:
- 如果要真正理解双亲委派模型,需要理解Java中类加载器的架构和职责,至少要懂得具体有哪些内建的类加载器;以及如何自定义类加载器。
- 从应用角度,加入Java启动很慢,有没有办法尽量减少Java类加载的开销?
知识扩展
首先,从架构角度,一起来看看Java 8以前的各种类加载器的结构,下面是三种Oracle JDK内建的类加载器。
1、启动类加载器( Bootstrap Class-Loader)
加载 jre/lib下面的jar文件,如rt.jar。它是个超级公民,即使是在开启了Security Manager的时候, JDK仍赋予了它加载的程序AllPermission。
对于做底层开发的工程师,有的时候可能不得不去试图修改JDK的基础代码,也就是通常意义上的核心类库,我们可以使用下面的命令行参数。
用法其实很易懂,例如,使用最常见的 “/p”,既然是前置,就有机会替换个别基础类的实现。
我们一般可以使用下面方法获取父加载器,但是在通常的JDK/JRE实现中,扩展类加载器getParent()都只能返回null。
2、扩展类加载器( Extension or Ext Class-Loader)
负责加载我们放到jre/lib/ext/目录下面的jar包, 这就是所谓的extension机制。该目录也可以通过设置 “java.ext.dirs” 来覆盖。
java -Djava.ext.dirs=your_ext_dir HelloWorld
3、应用类加载器( Application or App Class-Loader)
加载classpath。通常来说,其默认就是JDK内建的应用类加载器,但是它同样是可能被修改的,比如:
java -Djava.sysem.class.loader=com.yourcorp.YourClassLoader HelloWorld
如果指定了这个参数,那么JDK内建的应用类加载器就会成为定制加载器的父亲,这种方式通常用在类似需要改变双亲委派模式的场景。
至于被问到双亲委派模型,参考这个结构图更容易理解。试想,如果不同的类加载器都自己加载需要的某个类型,那么就会出现多次重复加载,完全是种浪费。
通常类加载机制有三个基本特征:
- 双亲委派模型。
虽然不是每个类都遵守这个模型,但是有的时候,启动类加载器所加载的类型,需要加载用户代码,比如JDK内部的serviceLoader机制,用户就可以在标准框架中实现项目。
但是像jdbc,file system,jndi就不会有双亲委派,而是用上下文加载器。
- 可见性
子类加载器都可以访问父加载器的类型,但是反过来是不允许的。
- 单一性
父加载器中加载过的类型,不会在子加载器中重复加载。
自定义加载器
常见的场景有:
- 实现类似进程内隔离,类加载器实际上用作不同的命名空间以提供类似容器、模块化的效果。
- 应用需要从不同的数据源获取类定义信息
- 需要自己操纵字节码,动态修改或者生成类型
总体上简单理解自定义类加载过程:
- 通过指定名称,找到二进制实现
- 创建Class对象,并完成类加载过程