java 类加载机制
类加载
了解 java 类加载机制
理解类加载器 ClassLoader
类加载时机
隐式加载 new 创建类的实例,
显式加载:loaderClass,forName 等
访问类的静态变量,或者为静态变量赋值
调用类的静态方法
使用反射方式创建某个类或者接口对象的 Class 对象。
初始化某个类的子类
直接使用 java.exe 命令来运行某个主类
类加载过程
类加载有五步:
加载 -> 验证 -> 准备 -> 解析 -> 初始化
加载
类加载过程的一个阶段,ClassLoader 通过一个类的完全限定名查找此类字节码文件,并利用字节码文件创建一个 class 对象
验证
目的在于确保 class 文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身的安全,主要包括四种验证:文件格式的验证,元数据的验证,字节码验证,符号引用验证
准备
为类变量(static 修饰的字段变量)分配内存并且设置该类变量的初始值,(如 static int i = 5 这里只是将 i 赋值为 0,在初始化的阶段再把 i 赋值为 5),
这里不包含 final 修饰的 static ,因为 final 在编译的时候就已经分配了。
这里不会为实例变量分配初始化,类变量会分配在方法区中,实例变量会随着对象分配到 Java 堆中
解析
这里主要的任务是把常量池中的符号引用替换成直接引用
初始化
这里是类记载的最后阶段,如果该类具有父类就进行对父类进行初始化,执行其静态初始化器(静态代码块)和静态初始化成员变量。
(前面已经对 static 初始化了默认值,这里我们对它进行赋值,成员变量也将被初始化)
forName 和 loaderClass 区别
Class.forName()得到的 class 是已经初始化完成的
ClassLoader.loaderClass 得到的 class 是还没有链接(验证,准备,解析三个过程被称为链接)的。
双亲委派机制
双亲委派模式要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器,
但是在双亲委派模式中父子关系采取的并不是继承的关系,而是采用组合关系来复用父类加载器的相关代码。
1 | protected Class<?> loadClass(String name, boolean resolve) |
工作原理
通过 loadClass 加载类时,会先通过 findLoadClass 查找该类是否已加载,
如果未加载,会先通过父加载期加载,依次向上递归,到最上级父加载器加载,
如果此时还未加载,再依次向下递归调用 findClass
就是每个儿子都很懒,遇到类加载的活都给它爸爸干,直到爸爸说我也做不来的时候,儿子才会想办法自己去加载。
优势
采用双亲委派模式的好处就是 Java 类随着它的类加载器一起具备一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类的时候,就没有必要子类加载器(ClassLoader)再加载一次。其次是考虑到安全因素,Java 核心 API 中定义类型不会被随意替换,假设通过网路传递一个名为 java.lang.Integer 的类,通过双亲委派的的模式传递到启动类加载器,而启动类加载器在核心 Java API 发现这个名字类,发现该类已经被加载,并不会重新加载网络传递过来的 java.lang.Integer.而之际返回已经加载过的 Integer.class,这样便可以防止核心 API 库被随意篡改。可能你会想,如果我们在 calsspath 路径下自定义一个名为 java.lang.SingInteger?该类并不存在 java.lang 中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器,最终会通过系统类加载器加载该类,但是这样做是不允许的,因为 java.lang 是核心的 API 包,需要访问权限,强制加载将会报出如下异常。
1 | java.lang.SecurityException:Prohibited package name: java.lang |
类与加载器
在 JVM 中标识两个 Class 对象,是否是同一个对象存在的两个必要条件:
- 类的完整类名必须一致,包括包名。
- 加载这个 ClassLoader(指 ClassLoader 实例对象)必须相同。