Java中的类加载机制

Java类加载机制

分为五步:加载、验证、准备、解析、初始化。

  1. 加载:通过类全名获取类的二进制流,装入方法区,生成Class对象
  2. 验证:验证二进制流是否符合格式
  3. 准备:为类的静态变量划分空间
  4. 解析:将常量池内的符号引用转化为直接引用
  5. 初始化:执行静态代码块,为静态变量赋初值

几种类加载器:

  1. 启动类加载器
  2. 拓展类加载器
  3. 应用类加载器
  4. 自定义类加载器

类加载器继承关系

双亲委派机制

核心思想是,当类加载器要加载一个类时,会先将加载任务委托给父类加载器,这个过程会一直递归直到最顶层的启动类加载器。只有当父类加载器无法完成加载任务时,才由子类加载器执行。

优点:

  • 避免类的重复加载
  • 防止核心API被注入篡改,已经被加载过的类不会重复加载,避免了被篡改注入恶意代码的类被加载

Java中的static

Java中的static关键字解析

静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象。既然没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。

我们最常见的static方法就是main方法,而main方法必须是static的,是因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问。

Java规定static不能用来修饰局部变量。

与C/C++中的static不同,Java中的static关键字不会影响到变量或者方法的作用域

1
2
3
4
5
6
7
8
9
10
11
public class Main {
public static void main(String[] args) {
Person.name = "张三";
Person.age = 18; // 报错
}
}

class Person {
public static String name;
private static int age;
}

Q:Java中可以通过this访问静态成员变量吗?下面的代码输出是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Test {
public static void main(String[] args) {
Test test = new Test();
test.printValue();
}

static int value = 0;

private void printValue() {
int value = 1; // 该value只是函数内部的局部变量,与this无关
System.out.println(this.value); // 访问的是静态成员变量value
}
}

A:可以,输出为0

Q:以下代码输出是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Father {
public static void main(String[] args) {
System.out.println("Father中的main函数");
new Son();
}

Other other = new Other("Father");

static {
System.out.println("Father中的静态代码块1");
}

static {
System.out.println("Father中的静态代码块2");
}

{
System.out.println("Father中的非静态代码块");
}

public Father() {
System.out.println("Father中的构造函数");
}
}

public class Son extends Father {
Other other = new Other("Son");

static {
System.out.println("Son中的静态代码块");
}

public Son() {
System.out.println("Son中的构造函数");
}
}

public class Other {
static {
System.out.println("Other中的静态代码块");
}

public Other(String str) {
System.out.println("Other中的构造函数,调用者:" + str);
}

{
System.out.println("Other中的非静态代码块");
}
}

A:输出为

1
2
3
4
5
6
7
8
9
10
11
12
Father中的静态代码块1
Father中的静态代码块2
Father中的main函数
Son中的静态代码块
Other中的静态代码块
Other中的非静态代码块
Other中的构造函数,调用者:Father
Father中的非静态代码块
Father中的构造函数
Other中的非静态代码块
Other中的构造函数,调用者:Son
Son中的构造函数

要运行静态函数main,首先要加载Father类。加载Father类时按顺序执行static块,输出前两行。

随后执行main函数,输出第三行。

再实例化Son。实例化前要先加载Son类,又因Son继承Father,则要先加载Father和Son,而Father已经加载了,于是加载Son,执行Son中的static块,输出第四行。

接着要实例化Son,在构造函数前要先初始化成员变量。因为继承Father所以要先初始化Father,Father有个Other类型的成员变量和一个非static块,按照先后顺序执行,又需要先加载并实例化Other。

于是加载Other,static块输出第五行。Other的非static块输出第六行,构造函数输出第七行。

接着Father初始化完成员变量继续按顺序执行非static块,输出第八行。

现在Son的父类Father成员变量已经初始化,于是Father的构造函数输出第九行。

Son的父类结束,轮到Son初始化成员变量,非static块每次实例化一个对象都会执行一次,因此Other的非static块再次执行输出第十行。其构造函数再输出第十一行。

最后Son的构造函数输出最后一行。Son实例化结束,main函数结束。