目录
我们首先看几个面试题。
- 我们能够通过一定的手段,覆盖 HashMap 类的实现么?
- 有哪些地方打破了 Java 的类加载机制?
- 如何加载一个远程的 .class 文件?怎样加密 .class 文件?
- <cinit> 方法和 <init> 方法有什么区别?
类加载过程?
JVM 通过加载 .class 文件,能够将其中的字节码解析成操作系统机器码。那这些文件是怎么加载进来的呢?又有哪些约定?加载过程如图:
浅绿的两个部分表示类的生命周期,就是从类的加载到类实例的创建与使用,再到类对象不再被使用时可以被 GC 卸载回收。这里要注意一点,由 Java 虚拟机自带的三种类加载器加载的类在虚拟机的整个生命周期中是不会被卸载的,只有用户自定义的类加载器所加载的类才可以被卸载。
注:几种变量的关系:
1、加载
将外部的 .class 文件,加载到 Java 的方法区内。将外部*.class加载到方法区,并创建一个Class对象。加载阶段主要是找到并加载类的二进制数据,比如从 jar 包里或者 war 包里找到它们。
2、验证
不符合规范的将抛出 java.lang.VerifyError 错误。像一些低版本的 JVM,是无法加载一些高版本的类库的,就是在这个阶段完成的。
3、准备
将为一些类变量(static修饰的变量,不包含用 final 修饰的静态变量)分配内存,并将其初始化为默认值。此时,实例对象还没有分配内存,所以这些动作是在方法区上进行的。
类变量有两次赋初始值的过程:
- 准备阶段,赋予默认初始值;
- 初始化阶段,赋予程序员定义的值。
而局部变量不存在准备阶段赋予初值。
范例:类变量与局部变量初值赋予
code-snippet 1:
public class A {
static int a ;
public static void main(String[] args) {
System.out.println(a);
}
}
code-snippet 2:
public class A {
public static void main(String[] args) {
int a ;
System.out.println(a);
}
}
code-snippet 1 将会输出 0,而 code-snippet 2 将无法通过编译。
4、解析
解析字段、接口、方法,将符号引用替换为直接引用的过程:
- 符号引用:一种定义,可以是任何字面上的含义;
- 直接引用:直接指向目标的指针、相对偏移量。
具体可以分为:
- 类或接口的解析
- 类方法解析
- 接口方法解析
- 字段解析
我们来看几个经常发生的异常,就与这个阶段有关:
- java.lang.NoSuchFieldError 根据继承关系从下往上,找不到相关字段时的报错。
- java.lang.IllegalAccessError 字段或者方法,访问权限不具备时的错误。
- java.lang.NoSuchMethodError 找不到相关方法时的错误。
解析过程保证了相互引用的完整性
5、初始化
主要完成静态块执行与静态变量的赋值,这里有两个规则:
- 规则一:static 语句块,只能访问到定义在 static 语句块之前的变量。
- 规则二:JVM 会保证在子类的初始化方法执行之前,父类的初始化方法已经执行完毕。
范例:规则一
static {
b = b + 1;
}
static int b = 0;
无法通过编译的。
范例:规则一
public class A {
static int a = 0 ;
static {
a = 1;
b = 1;
}
static int b = 0;
public static void main(String[] args) {
System.out.println(a);
System.out.println(b);
}
}
结果是 a = 1 、 b = 0;
变量的初始化过程:准备阶段->cinit->init
面试题:<cinit> 方法和 <init> 方法有什么区别?
详解类加载器
Java怎么保证核心API不被篡改?
如上图所示,Java 自带的三种类加载器分别是:BootStrap 启动类加载器、扩展类加载器和应用加载器(也叫系统加载器)。图右边的桔黄色文字表示各类加载器对应的加载目录。启动类加载器加载 java home 中 lib 目录下的类,扩展加载器负责加载 ext 目录下的类,应用加载器加载 classpath 指定目录下的类。除此之外,可以自定义类加载器。
Java 的类加载使用双亲委派模式,即一个类加载器在加载类时,先把这个请求委托给自己的父类加载器去执行,如果父类加载器还存在父类加载器,就继续向上委托,直到顶层的启动类加载器,如上图中蓝色向上的箭头。如果父类加载器能够完成类加载,就成功返回,如果父类加载器无法完成加载,那么子加载器才会尝试自己去加载。如图中的桔黄色向下的箭头。
这种双亲委派模式的好处,可以避免类的重复加载,另外也避免了 Java 的核心 API 被篡改。
打破双亲委派机制的案例