类加载子系统
- 通过类加载子系统(Class Loader)加载字节码文件(.class)
- 通过执行引擎来执行
- 如果执行过程中需要调用本地方法(native), 则通过本地方法栈和本地方法接口交互

类的声明周期(类的加载过程)
加载Loading
链接Linking
- 验证
- 准备
- 解析
类初始化Initialization
使用Using
- 实例化对象
- 静态方法
- ……
卸载Unloading
有些类可以卸载, 但有些类不可以卸载
Loading加载阶段
从各种源(class文件, zip压缩包, 动态代理运行时计算)到内存, 并在内存中构建出Java类的原型类模板对象
加载阶段具体任务
- 通过类的全限定名获取定义此类的二进制字节流
- 将这个字节流解析转换为方法区的运行时数据结构(Java类模板)
- 在堆空间中生成一个代表这个类的java.lang.Class对象, 指向Java类模板, 作为方法区中这个类的各种操作的访问入口
Class文件的本质
Class文件本质上是一个二进制流, 保存为(.class)文件在磁盘上存储只是其一种保存形式. 如果通过网络接收到一个符合Class文件要求的二进制比特流, 也可以将其作为某个类加载到内存中. 常见的二进制流(Class文件)的获取方式有:
- .class后缀的文件
- jar包, zip等压缩包
- 存放在数据库中的二进制数据
- 通过http之类的网络协议传输的二进制数据
什么是类模板对象
类模板对象是Java类在JVM中的一个快照, JVM将从字节码文件中解析出来的常量池, 类字段, 类方法等信息存储到类模板中, 这样JVM在运行期便能通过类模板来获取Java类的任意信息, 能够对Java类的成员变量进行遍历, 也能进行Java方法的调用. 这也是Java反射机制的基础, 不需要创建对象, 就可以查看加载类中的方法, 属性等等信息

Linking链接阶段

验证Verify
验证阶段虽然使得拖慢了整个类加载速度, 但是却避免了字节码在运行时还需要进行各种检查. 正所谓磨刀不误砍柴工
格式检查
格式检查实际上会和加载loading同时进行
魔数检查0xCAFEBABE
版本检查
长度检查
语义检查
是否继承final类或者重写了final方法
是否有父类
抽象方法是否实现
是否存在不兼容的方法, 比如仅仅返回值不同的方法签名
字节码验证
跳转指令是否指向正确位置
操作数类型是否合理
符号引用验证
符号引用的直接引用是否存在
准备Prepare
为类中的静态变量分配内存, 并设置默认值0
final修饰的类变量会在编译期间被优化, 本质上相当于一个常量的符号引用, 编译期简单替换即可在方法区中为类变量(
static修饰)分配内存, 并设置默认初始值(广义0)准备阶段不会为实例变量在方法区中分配内存, 实例变量随着对象一起分配在堆中
public class HelloWorld {
public HelloWorld() {
//instanceVariable: 10->30, 无论先后
this.instanceVariable = 30;
}
private int instanceVariable = 10;
// 类变量, 在类加载过程的准备阶段preparation, classVariable01, classVariable02 都会被初始化为0
static {
// 可以为y赋值, 但是不可以调用y
// 之所以可以先赋值, 后声明, 是因为在prepare阶段已经为该变量设置了默认值为0.
// 也就是说只有在initialization初始化阶段才整合static代码块, 此时只需要重新按顺序赋值即可
// classVariable02: 0->20->10
classVariable02 = 20;
// System.out.println(classVariable02); //报错: 非法的前向引用, 除非static变量声明在前面
}
static int classVariable01 = 100;
static int classVariable02 = 10;
//final static常量, 在编译的时候确定
final static int XIONG = 100;
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
System.out.println("hello world");
}
}
解析Resolve
将常量池中的符号引用转换为直接引用的过程
Initialization初始化阶段

执行类构造器方法
<clinit>()的过程, 该方法不需要定义, 是javac编译器自动收集类中所有的static相关的代码合并而来, 按照源文件中的顺序执行.<clinit>()只会执行一次, 且虚拟机会为该过程加锁, 不需要在源代码中显式加锁
<clinit>()不同于类构造器, 类构造器在字节码文件中作为<init>()父类clinit<>() -> 子类clinit()class Father { public static int x = 1; } public class Son extends Father { // 初始化顺序: Father.x -> Son.x -> Son.y public static int y = x; public static void main(String[] args) { System.out.println(Son.y); } }
clinit的线程安全问题
可能发生死锁的现象描述:
线程A先加载 ClassA, 线程B先加载 ClassB, 而在 ClassA 的 clinit<>() 方法中需要加载 ClassB, 但在 ClassB 的 <clinit>() 方法中需要加载 ClassA
类的主动使用和被动使用
代码中出现的类都会被加载, 但是却不一定都进行初始化, 只有符合主动使用的类才会被初始化, 即被动使用的类只加载但不初始化.
主动使用
被动使用
Using使用阶段
Unloading卸载阶段
变量默认初始化和显示赋值总结
public class Application {
// static final + 字面量 => 编译阶段初始化赋值, prepare阶段显式赋值
public static final int INTSta = 1;
public static final int NUM = new Random().nextInt(10);
public static int anIntSta = 2;
public static final Integer INTEGERSTA = 10;
public static Integer integerSta = 20;
public static final String s0 = new String("hello world 0");
public static final String s1 = "hello world 1";
public static String s2 = new String("hello world 2");
public static String s3 = "hello world 3";
}
最终结论:
除static final + 字面量赋值外的类变量 |
static final + 字面量 | 实例变量 | final 实例变量 | |
|---|---|---|---|---|
| 默认初始化 | prepare阶段 | 编译阶段 | 实例对象内存空间开辟后 | |
| 显式赋值 | initialization阶段, 即字节码的<clinit>()方法 |
prepare阶段 | 构造器方法中首行, 即字节码的<init>()方法中 |
只要赋值涉及到构造器或者类静态方法的调用, 都需要在<clinit>()中进行赋值
并不是使用
static final修饰的量即为常量, 但不使用static final修饰的一定不是常量. 除字面量方式赋值static final String s = "hello world";的形式外, 其他涉及引用类型变量的任何形式, 都不是常量, 包括static final String s = new String("hello world");


Integer和int
- Integer和Integer比较: 直接比较地址
- Integer和int比较: Integer调用
intValue()进行自动拆箱, 然后和int进行值的比较 - Integer的
valueOf()方法, 对于值在[-128, 127]之间的, 通过缓存获取同一个内存对象, 否则才新建对象
源代码
public class IntegerAndInt {
public static void main(String[] args) {
Integer x1 = 5;
Integer x2 = 5;
int y = 5;
Integer z1 = 128;
Integer z2 = 128;
int w = 128;
System.out.println(x1 == x2);//true
System.out.println(x1 == y); //true
System.out.println(z1 == z2);//false
System.out.println(z1 == w); //true
}
}
字节码
实例变量的初始化过程
类变量: 在Prepare阶段设置默认值, 在Initialization阶段顺序赋值覆盖
实例变量(实例字段): 创建对象时先在堆空间开辟空间, 设置默认值, 然后开始调用构造函数. 在构造器被调用后对字段赋值覆盖
class Person {
int x = 10;
public Person() {
print();
x = 20;
}
public void print() {
System.out.println("Person.x : " + x);
}
}
class Student extends Person {
int x = 30;
public Student() {
print();
x = 40;
}
@Override
public void print() {
System.out.println("Student.x : " + x);
}
}
public class ExtendMain {
public static void main(String[] args) {
// 1. 测试父类 <- 父类
Person p = new Person();
System.out.println(p.x);
System.out.println("##################");
// 2. 测试父类 <- 子类, 用父类接受子类对象
Person person = new Student();
System.out.println("##################");
// 属性不存在多态性, 即在Student对象中, Person.x和Student.x是共存的
System.out.println(person.x);
// 方法被重写, Student对象中只剩下一个print()方法, 无论通过person来调用还是强转为student来调用, 都会执行Student类中的print()
person.print();
System.out.println("##################");
// 强制类型转换
Student sp = (Student) person;
System.out.println(sp.x);
System.out.println("##################");
// 3. 测试子类 <- 子类
Student student = new Student();
System.out.println(student.x);
System.out.println("##################");
}
}

变量的初始化和赋值流程
设置默认值 -> 显式初始化 -> 构造器中初始化 -> 对象.属性赋值
- 默认初始化: 随着对象在堆空间中分配内存时, 设置默认值
- 显式初始化: 在实例变量(实例字段)的变量声明是显式使用
private int x = 10; - 构造器初始化: 在构造器中进行初始化
x = 20; - 赋值: 此时不属于初始化过程, 在对象创建完成后, 通过
Obj.x = 90;进行赋值
final修饰的不可变量
使用final修饰的不可变量
- 可以在初始化过程中修改值, 但是不可以对其进行赋值. 即对象完成构建后不能修改
- 必须在显式初始化和构造器初始化中二选一, 并且只能进行一次初始化(不考虑设置默认值)
public class AddMain {
// static final 修饰的NUM即常量10的符号引用, 是一个常量, 必须在声明的时候指定
private static final int NUM = 10;
// final修饰的COUNT是一个不可变量, 会在实例对象创建时
// 1. 先分配堆空间内存, 并设置默认值
// 2. 再对COUNT进行显式初始化或者构造器初始化
private final int COUNT = 20;
private int number = 2;
private int num = 1;
public AddMain() {
// COUNT = 10; // COUNT显式初始化和构造器初始化二选一
}
public int add() {
num = num + 1;
return num;
}
public int addOne() {
num++;
return num;
}
public int addOne2() {
++num;
return num;
}
public static void main(String[] args) {
AddMain app = new AddMain();
// app.COUNT = 10; //报错, final变量不可以赋值
app.num = 10;
}
}
面试题
