Moralok's Blog

你在幼年时当快乐

如果你准备过 Java 的面试,应该看到过一个问题:“String s1 = new String("abc"); 这个语句创建了几个字符串对象”。这个问题曾经困扰我,当时的我不能理解这个问题想要考察的是什么?
答案中或许提及了字符串常量池,但是如果细究起来,会发现答案并不完善,有些令人困惑,甚至问题本身就有一定的误导作用。它很容易让初学者以为创建一个字符串对象和创建一个其他类型的对象在过程上是有一些区别的。
其实关键的地方在于 “abc” 而不是 new String("abc")

字符串常量池的作用

字符串字面量

字面量(literal)是用于表达源代码中的一个固定值的表示法(notion),比如代码中的整数、浮点数、字符串。简而言之,字符串字面量就是双引号包裹的字符串,例如:

1
String s1 = "a";

在 Java 中,字符串对象就是一个 String 类型的对象,因此在程序运行时,String 类型的变量 s1 指向的一定是一个 String 对象。字面量 “a” 在某一个时刻,没有经过 new 关键字,变成了一个 String 对象

接下来我们来思考一个问题,程序中每一个字符串字面量都要对应着生成一个单独的 String 对象吗?考虑到 Java 中 String 对象是不可变的,显然相同的字符串字面量完全可以共用一个 String 对象从而避免重复创建对象。JVM 也是这样设计的,这些可以共用的 String 对象组成了一个字符串常量池。

  1. 第一次遇到某一个字符串字面量时,会在字符串常量池中创建一个 String 对象,以后遇到相同的字符串字面量,就复用该对象,不再重复创建。
  2. 每一次 new 都会创建一个新的 String 对象。

ps: 以上的“遇到某一个字符串字面量”就是很纯粹地指代程序的源代码中出现用双引号括起来的字符串字面量。

进入字符串常量池的两种情况

因此,如果字符串常量池中没有值为 “abc” 的 String 对象new String("abc") 语句将涉及两个 String 对象的创建,第一个是因为括号里的 “abc” 而在字符串常量池中生成的,第二个才是 new 关键字在堆中创建的;否则只会涉及一个 String 对象的创建。
为什么上面改用如果字符串常量池中没有值为 “abc” 的 String 对象呢?这是因为,字符串常量池里保留的 String 对象有两种产生来源:

  1. 因为第一次遇到字符串字面量而生成的字符串对象。
  2. 使用 java.lang.String#intern 主动地尝试将字符串对象放入字符串常量池。

常量池的分类

  1. Class 文件中的常量池(Constant Pool)
  2. 运行时常量池(Runtime Constant Pool)
  3. 字符串常量池
1
2
3
4
5
6
7
public class StringTableTest_1 {  
public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
}
}

使用 javap -v .\StringTableTest_1.class 进行反编译,摘取重要部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab

#25 = Utf8 a
#26 = Utf8 b
#27 = Utf8 ab



0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
  • Class 文件中的常量池 Constant pool 会记录代码中出现的字面量(文本文件)。
  • 运行时常量池是方法区的一部分,Class 文件中的常量池的内容,在类加载后,就进入了运行时常量池中(内存中的数据)。
  • 字符串常量池,记录 interned string 的一个全局表,JDK 6 前在方法区,后移到堆中。

字符串常量池的位置和形式

在《深入理解Java虚拟机》提到:字符串常量池的位置从 JDK 7 开始,从永久代中移到了堆中。在这句话中,字符串常量池像是一个特定的内存区域,存储了 interned string 的实例。

验证字符串常量池的位置

书中使用了以下方式来验证字符串常量池的位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StringTableTest_8 {  

// JDK 1.8 设置 -Xmx10m -XX:-UseGCOverheadLimit
// JDK 1.6 设置 -XX:MaxPerSize=10m
public static void main(String[] args) {
List<String> list = new ArrayList<>();
int i = 0;
try {
for (int j = 0; j < 260000; j++) {
list.add(String.valueOf(j).intern());
i++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}

在 JDK 8 中异常如下:

1
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

在 JDK 6 中异常如下:

1
java.lang.OutOfMemoryError: PermGen space

同时书中也提到了,在字符串常量池的位置改变后,它只用保存第一次出现时字符串对象的引用。JDK 8 中的 intern 方法可以印证该说法,方法注释中提到:如果字符串常量池中已存在相等(equals)的字符串,那就返回已存在的对象(这样原先准备加入的对象就可以释放);否则,将字符串对象加入字符串常量池中,直接返回对该对象的引用(不用像 JDK 6 时,复制一个对象加入常量池,返回该复制对象的引用)。

关于 intern 的实验

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
public class StringTableTest_5 {  

public static void main(String[] args) {
// "a"、"b" 作为字符串字面量,会解析得到字符串对象放入字符串常量池
// 但是 new String("a") 创建出来的字符串对象,不会进入字符串常量池
String s1 = new String("a") + new String("b");
// intern 方法尝试将 s1 放入 StringTable,无则放入,返回该对象引用,有则返回已存在对象的引用
String s2 = s1.intern();

String x = "ab";

System.out.println(s2 == x);
System.out.println(s1 == x);
}
}

public class StringTableTest_6 {

public static void main(String[] args) {
// 将 "ab" 的赋值语句提前到最开始,"ab" 生成的字符串对象进入字符串常量池
String x = "ab";
String s1 = new String("a") + new String("b");
// intern 方法尝试将 s1 放入 StringTable,无则放入,返回该对象引用,有则返回已存在对象的引用
String s2 = s1.intern();

System.out.println(s2 == x);
System.out.println(s1 == x);
}
}

实验结果证实了上述说法。

字符串常量池到底是什么?

但是 xinxi 提及:字符串常量池,也称为 StringTable,本质上是一个惰性维护的哈希表,是一个纯运行时的结构,只存储对 java.lang.String 实例的引用,而不存储 String 对象的内容。当我们提到一个字符串进入字符串常量池其实是说在这个 StringTable 中保存了对它的引用,反之,如果说没有在其中就是说 StringTable 中没有对它的引用。
zyplanke 分析 StringTable 在内存中的形式时,也表达了类似的观点。

尽管这个疑问似乎不妨碍我们理解很多东西,但是深究之后,真的让人困惑,网上也没有搜集到更多的信息。字符串常量池和 StringTable 是否等价?字符串常量池更准确的说法是否是“一个保存引用的 StringTable 加上分布在堆(JDK 6 以前的永久代)中的字符串实例”?
已经好几次打开 jvm 的源码,却看不懂它到底什么意思啊!!!!!难道是时候开始学 C++ 了吗。

进入字符串常量池的时机

前面提到了第一次遇到的字符串字面量会在某一个时刻,生成对应的字符串对象进入字符串常量池,同时也提到了,字符串常量池(StringTable)的维护是懒惰的,那么这些究竟是什么时候发生的呢?

1
2
3
4
5
6
public class StringTableTest_12 {

public static void main(String[] args) throws IOException {
new String("ab");
}
}
1
2
3
4
5
6
 0: new           #2                  // class java/lang/String
3: dup
4: ldc #3 // String ab
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: pop
10: return

RednaxelaFX 的文章提到:

在类加载阶段,JVM 会在堆中创建对应这些 class 文件常量池中的字符串对象实例,并在字符串常量池中驻留其引用。具体在 resolve 阶段执行。这些常量全局共享。

xinxi 的文章中补充到:

这里说的比较笼统,没错,是 resolve 阶段,但是并不是大家想的那样,立即就创建对象并且在字符串常量池中驻留了引用。 JVM规范里明确指定resolve阶段可以是lazy的。
……
就 HotSpot VM 的实现来说,加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,不会进入全局的字符串常量池(即在 StringTable 中并没有相应的引用,在堆中也没有对应的对象产生)。

《深入理解Java虚拟机》中提到:

《Java虚拟机规范》之中并未规定解析阶段发生的具体时间,只要求了在执行ane-warray、checkcast、getfield、getstatic、instanceof、invokedynamic、invokeinterface、invoke-special、invokestatic、invokevirtual、ldc、ldc_w、ldc2_w、multianewarray、new、putfield和putstatic这17个用于操作符号引用的字节码指令之前,先对它们所使用的符号引用进行解析。所以虚拟机实现可以根据需要来自行判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析,还是等到一个符号引用将要被使用前才去解析它。

综上可知,字符串字面量的解析是属于类加载的解析阶段,但是《Java虚拟机规范》并未规定解析发生的具体时间,只要求在执行一些字节码指令前进行,其中包括了 ldc 指令。虚拟机的具体实现,比如 Hotspot 就在执行 ldc #indexNumber 前触发解析,根据字符串常量池中是否已存在字符串对象决定是否创建对象,并将对象推送到栈顶。
这也证实了前文中提到的字符串字面量生成字符串对象和 new 关键字无关。

验证延迟实例化

使用 IDEA memory 功能,观察字符串对象的个数逐个变化。

  1. 直到第一次运行到字符串字面量时,才会创建对应的字符串对象。
  2. 相同的字符串常量,不会重复创建字符串对象。
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
public class StringTableTest_4 {  

public static void main(String[] args) {
System.out.println();

System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("0");
System.out.println("1");
System.out.println("2");
System.out.println("3");
System.out.println("4");
System.out.println("5");
System.out.println("6");
System.out.println("7");
System.out.println("8");
System.out.println("9");
System.out.println("0");
}
}

字符串常量池的垃圾回收和性能优化

垃圾回收

前文提到字符串常量池在 JDK 7 开始移到堆中,是因为考虑在方法区中的垃圾回收是比较困难的,同时随着字节码技术的发展,CGLib 等会大量动态生成类的技术的运用使得方法区的内存紧张,将字符串常量池移到堆中,可以有效提高其垃圾回收效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StringTableTest_9 {  

// -Xmx10m -XX:+PrintStringTableStatistics -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {
int i = 0;
try {
// 0->100->10000,观察统计信息中数量的变化以及垃圾回收记录
for (int j = 0; j < 10000; j++) {
String.valueOf(j).intern();
i++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(i);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
[GC (Allocation Failure) [PSYoungGen: 2048K->488K(2560K)] 2048K->856K(9728K), 0.0007745 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 


StringTable statistics:
Number of buckets : 60013 = 480104 bytes, avg 8.000
Number of entries : 7277 = 174648 bytes, avg 24.000
Number of literals : 7277 = 421560 bytes, avg 57.930
Total footprint : = 1076312 bytes
Average bucket size : 0.121
Variance of bucket size : 0.125
Std. dev. of bucket size: 0.354
Maximum bucket size : 3

性能优化

调整 buckets size

当 size 过小,哈希碰撞增加,链表变长,效率会变低,需要增大 buckets size。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StringTableTest_10 {  

// -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
// 默认->200000->1009(最小值),观察耗时
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/resources/linux.words"), StandardCharsets.UTF_8))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = br.readLine();
if (line == null) {
break;
}
line.intern();
}
System.out.println("cost: " + (System.nanoTime() - start) / 1000000) ;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

主动运用 intern 的场景

当你需要大量缓存重复的字符串时,使用 intern 可以大大减少内存占用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class StringTableTest_11 {  

// -Xms500m -Xmx500m -XX:StringTableSize=200000 -XX:+PrintStringTableStatistics
public static void main(String[] args) throws IOException {
List<String> words = new ArrayList<>();
System.in.read();
for (int i = 0; i < 10; i++) {
try (BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/resources/linux.words"), StandardCharsets.UTF_8))) {
String line = null;
long start = System.nanoTime();
while (true) {
line = br.readLine();
if (line == null) {
break;
}
// words.add(line);
words.add(line.intern());
}
System.out.println("cost: " + (System.nanoTime() - start) / 1000000) ;
}
}
System.in.read();
}
}

使用 VisualVM 观察字符串和 char[] 内存占用情况,可以发现提升显著。

字符串拼接

变量的拼接

字符串变量的拼接,底层是使用 StringBuilder 实现:new StringBuilder().append("a").append("b").toString(),而 toString 方法使用拼接得到的 char 数组创建一个新的 String 对象,因此 s3 和 s4 是不相同的两个对象。

1
2
3
4
5
6
7
8
9
public class StringTableTest_2 {  

public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 0: ldc           #2                  // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: return

常量的拼接

字符串常量的拼接是在编译期间,因为已知结果而被优化为一个字符串常量。又因为 “ab” 字符串在 StringTable 中是已存在的,所以不会重新创建新对象。

1
2
3
4
5
6
7
8
9
10
public class StringTableTest_3 {

public static void main(String[] args) {
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
String s5 = "a" + "b";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 0: ldc           #2                  // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: new #5 // class java/lang/StringBuilder
12: dup
13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V
16: aload_1
17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
20: aload_2
21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
27: astore 4
29: ldc #4 // String ab
31: astore 5
33: return

参考文章

  1. Java 中new String(“字面量”) 中 “字面量” 是何时进入字符串常量池的? - xinxi的回答 - 知乎
  2. 请别再拿“String s = new String(“xyz”);创建了多少个String实例”来面试了吧
  3. JVM中字符串常量池StringTable在内存中形式分析

堆的组成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class JvmGcTest_1 {

public static final int _512KB = 512 * 1024;
public static final int _1MB = 1024 * 1024;
public static final int _6MB = 6 * 1024 * 1024;
public static final int _7MB = 7 * 1024 * 1024;
public static final int _8MB = 8 * 1024 * 1024;

// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
// -XX:+UseSerialGC 避免幸存区比例动态调整
public static void main(String[] args) {

}
}
1
2
3
4
5
6
7
8
9
Heap
def new generation total 9216K, used 2010K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 24% used [0x00000000fec00000, 0x00000000fedf68c8, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3288K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 348K, capacity 388K, committed 512K, reserved 1048576K

根据打印的信息,组成如下:

  • Heap: 堆。
    • def new generation: 新生代。
    • tenured generation: 老年代。
    • Metaspace: 元空间,实际上并不属于堆, -XX:+PrintGCDetails 将它的信息一起输出。

堆空间的比例

新生代中的空间占比 eden:from:to 在默认情况下是 8:1:1,与观察到的数据 8192K:1024K:1024K 一致。
新生代的空间 eden + from + to 为 10240K,符合 -Xmn10M 设置的大小。
total 显示为 9216K,即 eden + from 的大小,是因为 to 的空间不计算在内。新生代可用的空间只有 eden + fromto 空间只是在使用标记-复制算法进行垃圾回收时使用。
老年代的空间为 10240K。
目前仅 eden 中已用 2010K,约占 eden 空间的 24%。

从内存地址分析堆空间

内存地址为 16 位的 16 进制的数字,64 位机器。
[0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000) 分别表示地址空间的开始、已用、结束的地址指针。
新生代 [0x00000000fec00000, 0x00000000ff600000),老年代 [0x00000000ff600000, 0x0000000100000000),计算可得空间大小均为 10MB。
eden 中已用的空间地址为 [0x00000000fec00000, 0x00000000fedf68c8),空间大小为 2058440 byte,约等于 2010K。

显而易见,新生代和老生代是一片完全连续的地址空间。

堆的垃圾回收

1
2
3
4
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
}
1
2
3
4
5
6
7
8
9
10
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0105099 secs] 2013K->721K(19456K), 0.0105455 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
def new generation total 9216K, used 8135K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 90% used [0x00000000fec00000, 0x00000000ff33d8c0, 0x00000000ff400000)
from space 1024K, 70% used [0x00000000ff500000, 0x00000000ff5b45f0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K

Allocation Failure,正常情况下,新对象总是分配在 Eden,分配空间失败,eden 的剩余空间不足以存放 7M 大小的对象,新生代发生 minor GC
[DefNew: 2013K->721K(9216K), 0.0105099 secs],新生代在垃圾回收前后空间的占用变化和耗时。
2013K->721K(19456K), 0.0105455 secs,整个堆在垃圾回收前后空间的占用变化和耗时。

GC 类型

  • GC: minor GC。
  • Fulle GC: full GC。

from 和 to 的角色变换

from 的已用空间的地址为 [0x00000000ff500000, 0x00000000ff5b45f0),空间大小为 738800 byte,约 721K,与 GC 后的新生代空间占用大小一致。在垃圾回收后,eden 区域存活的对象全部转移到了原 to 空间,fromto 空间的角色相互转换(从地址空间的信息可以看到此时 to 的地址指针比 from 的地址指针小)。
eden 的已用空间的地址为 [0x00000000fec00000, 0x00000000ff33d8c0),空间大小为 7592128 byte,约 7.24M,比 7M 大不少。此时 eden 区域除了 byte[] 对象外,还存储了其他对象,比如为了创建 List<byte[]> 对象而新加载的类对象。

eden 空间足够时不发生 GC

1
2
3
4
5
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
}
1
2
3
4
5
6
7
8
9
10
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0011172 secs] 2013K->721K(19456K), 0.0011443 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
def new generation total 9216K, used 8647K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 96% used [0x00000000fec00000, 0x00000000ff3bd8d0, 0x00000000ff400000)
from space 1024K, 70% used [0x00000000ff500000, 0x00000000ff5b45f0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 0K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 0% used [0x00000000ff600000, 0x00000000ff600000, 0x00000000ff600200, 0x0000000100000000)
Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K

由于 eden 区域还能放下 512K 的对象,所以仍然只会发生一次垃圾回收。
eden 区域的已用空间比例上升到 96%,已用空间的地址为 [0x00000000fec00000, 0x00000000ff3bd8d0),空间大小为 8116432 byte,约 7.74M,比上一次增加了 524304 byte,即 512 * 1024 + 16。显然第二次添加时,不再因为创建 List<byte[]> 而创建额外的对象,只有创建对象所需的 512K 和 16 字节的对象头。这一刻数值的精确让人欣喜hhh

新生代空间不足,部分对象提前晋升到老年代

1
2
3
4
5
6
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_7MB]);
list.add(new byte[_512KB]);
list.add(new byte[_512KB]);
}
1
2
3
4
5
6
7
8
9
10
11
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0013580 secs] 2013K->721K(19456K), 0.0013932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8565K->512K(9216K), 0.0046378 secs] 8565K->8396K(19456K), 0.0046540 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 1350K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 10% used [0x00000000fec00000, 0x00000000fecd1a20, 0x00000000ff400000)
from space 1024K, 50% used [0x00000000ff400000, 0x00000000ff480048, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 7884K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 77% used [0x00000000ff600000, 0x00000000ffdb33a0, 0x00000000ffdb3400, 0x0000000100000000)
Metaspace used 3354K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K

在第三次添加时,由于 eden 空间不足,因此又发生了第二次垃圾回收。
[DefNew: 8565K->512K(9216K), 0.0046378 secs],新生代的空间占用下降到了 512K,应该是在 from 中留下了第二次添加时的 512K。
在第二次添加完成后,eden [0x00000000fec00000, 0x00000000ff3bd8d0)from [0x00000000ff500000, 0x00000000ff5b45f0) 占用的空间为 8116432 + 738800 = 8855232 约 8647.7K,略大于 8565K。很奇怪,第二次垃圾回收前,新生代的空间占用为什么有小幅度下降。
8565K->8396K(19456K), 0.0046540 secs,堆的占用空间并未发生明显下降。部分对象因为新生代空间不足,提前晋升到了老年代中。8396K - 512 K 剩余 7884K,全部晋升到老年代,符合 77% 的统计数据。
eden 中加入了第三次添加时的对象,大于 512K 不少。
此时 edenfromtenured 中均有不好确认成分的空间占用,比如 from 中多了 56 字节。

新生代空间不足,大对象直接在老年代创建

1
2
3
4
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
}
1
2
3
4
5
6
7
8
9
Heap
def new generation total 9216K, used 2177K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee20730, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8192K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 80% used [0x00000000ff600000, 0x00000000ffe00010, 0x00000000ffe00200, 0x0000000100000000)
Metaspace used 3353K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 360K, capacity 388K, committed 512K, reserved 1048576K

在 Eden 空间肯定不足而老年代空间足够的情况下,大对象会直接在老年代中创建,此时不会发生 GC。

内存不足 OOM

1
2
3
4
5
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
waiting...
[GC (Allocation Failure) [DefNew: 4711K->928K(9216K), 0.0017245 secs][Tenured: 8192K->9117K(10240K), 0.0021690 secs] 12903K->9117K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0039336 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 9117K->9063K(10240K), 0.0014352 secs] 9117K->9063K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0014614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
at com.moralok.jvm.gc.JvmGcTest.lambda$main$0(JvmGcTest.java:27)
at com.moralok.jvm.gc.JvmGcTest$$Lambda$1/2003749087.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)

Heap
def new generation total 9216K, used 1502K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 18% used [0x00000000fec00000, 0x00000000fed77a00, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 9063K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 88% used [0x00000000ff600000, 0x00000000ffed9c50, 0x00000000ffed9e00, 0x0000000100000000)
Metaspace used 4787K, capacity 4884K, committed 4992K, reserved 1056768K
class space used 522K, capacity 558K, committed 640K, reserved 1048576K

当新生代和老年代的空间均不足时,在尝试 GC 和 Full GC 后仍不能成功分配对象,就会发生 OutOfMemoryError

线程中发生内存不足,不会影响其他线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();

new Thread(() -> {
list.add(new byte[_8MB]);
list.add(new byte[_8MB]);
}).start();

System.out.println("waiting...");
try {
System.in.read();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
[GC (Allocation Failure) [DefNew: 2013K->721K(9216K), 0.0012274 secs][Tenured: 8192K->8912K(10240K), 0.0113036 secs] 10205K->8912K(19456K), [Metaspace: 3345K->3345K(1056768K)], 0.0125751 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[Full GC (Allocation Failure) [Tenured: 8912K->8895K(10240K), 0.0011880 secs] 8912K->8895K(19456K), [Metaspace: 3345K->3345K(1056768K)], 0.0012009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 246K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 3% used [0x00000000fec00000, 0x00000000fec3d890, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 8895K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffeafce0, 0x00000000ffeafe00, 0x0000000100000000)
Metaspace used 3380K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 363K, capacity 388K, committed 512K, reserved 1048576K
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.moralok.jvm.gc.JvmGcTest.main(JvmGcTest.java:21)

Thread-0 发生 OutOfMemoryError 后,main 线程仍然正常运行。

大对象的划分指标

当创建的大对象 + 对象头的容量小于等于 eden,如果 GC 后的存活对象可以放入 to,那么还是会先在 eden 中创建大对象。
在本案例中,又会马上发生一次 GC,大对象提前晋升到老年代中。

1
2
3
4
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
list.add(new byte[_8MB - 16]);
}
1
2
3
4
5
6
7
8
9
10
11
[GC (Allocation Failure) [DefNew: 2013K->693K(9216K), 0.0015517 secs] 2013K->693K(19456K), 0.0015828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 8885K->0K(9216K), 0.0048110 secs] 8885K->8885K(19456K), 0.0048264 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
Heap
def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66958, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
tenured generation total 10240K, used 8885K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffead580, 0x00000000ffead600, 0x0000000100000000)
Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 354K, capacity 388K, committed 512K, reserved 1048576K

尽管最终大部分对象提前晋升到老年代,但是可以看到第二次 GC 前的新生代空间占用,可见数组分配时,所需空间刚好为 Eden 空间大小时,还是会在 eden 创建对象。

注意事项

  • 正常情况下,新对象都是在 eden 中创建。
  • 空间足够的意思并非空间占用相加的值仍小于总额,而是有连续的一片内存可供分配。因此紧凑才能利用率高。
  • 正常情况下,GC 前 to 区域总是为空,GC 后 eden 区域总是为空。
  • 正常情况下,GC 后 eden 和 from 的存活对象要么去了 to,要么去老年代。
  • 只要 GC 后腾空 eden,创建在 eden 中的新对象的空间占用可以等于 eden 的大小。

尽管总体上有迹可循,但是 GC 的具体情况,仍然需要具体分析,有很多分支情况未一一确认。

Spring Bean 生命周期

获取 Bean

获取指定 Bean 的入口方法是 getBean,在 Spring 上下文刷新过程中,就依次调用 AbstractBeanFactory#getBean(java.lang.String) 方法获取 non-lazy-init 的 Bean。

1
2
3
4
public Object getBean(String name) throws BeansException {
// 具体工作由 doGetBean 完成
return doGetBean(name, null, null, false);
}

deGetBean

作为公共处理逻辑,由 AbstractBeanFactory 自己实现。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
// 转换名称:去除 FactoryBean 的前缀 &,将别名转换为规范名称
final String beanName = transformedBeanName(name);
Object bean;

// 检查单例缓存中是否已存在
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// ...
// 如果已存在,直接返回该实例或者使用该实例(FactoryBean)创建并返回对象
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
else {
// 如果当前 Bean 是一个正在创建中的 prototype 类型,表明可能发生循环引用
// 注意:Spring 并未解决 prototype 类型的循环引用问题,要抛出异常
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}

// 如果当前 beanFactory 没有 bean 定义,去 parent beanFactory 中查找
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
String nameToLookup = originalBeanName(name);
if (args != null) {
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else {
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
}

if (!typeCheckOnly) {
// 标记为至少创建过一次
markBeanAsCreated(beanName);
}

try {
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);

// 确保 bean 依赖的 bean(构造器参数) 都已实例化
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
for (String dep : dependsOn) {
if (isDependent(beanName, dep)) {
// 注意:Spring 并未解决构造器方法中的循环引用问题,要抛异常
}
// 注册依赖关系,确保先销毁被依赖的 bean
registerDependentBean(dep, beanName);
// 递归,获取依赖的 bean
getBean(dep);
}
}
}

if (mbd.isSingleton()) {
// 如果是单例类型(绝大多数都是此类型)
// 再次从缓存中获取,如果仍不存在,则使用传入的 ObjectFactory 创建
sharedInstance = getSingleton(beanName, new ObjectFactory<Object>(
{
@Override
public Object getObject() throws BeansException {
try {
// 创建 bean
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
// 由于可能已经提前暴露,需要显示地销毁
destroySingleton(beanName);
throw ex;
}
}
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
else if (mbd.isPrototype()) {
// 如果是原型类型,每次都新创建一个
// ...
}
else {
// 如果是其他 scope 类型
// ...
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}

getSingleton

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
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
// 加锁
synchronized (this.singletonObjects) {
// 再次从缓存中获取(和调用前从缓存中获取构成双重校验)
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
if (this.singletonsCurrentlyInDestruction) {
// 如果正在销毁单例,则抛异常
// 注意:不要在销毁方法中调用获取 bean 方法
}
// 创建前,先注册到正在创建中的集合
// 在出现循环引用时,第二次进入 doGetBean,用此作为判断标志
beforeSingletonCreation(beanName);
boolean newSingleton = false;
// ...
try {
// 使用传入的单例工厂创建对象
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
catch (IllegalStateException ex) {
// 如果异常的出现是因为 bean 被创建了,就忽略异常,否则抛出异常
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex;
}
}
catch (BeanCreationException ex) {
// ...
}
finally {
// ...
// 创建后,从正在创建中集合移除
afterSingletonCreation(beanName);
}
if (newSingleton) {
// 添加单例到缓存
addSingleton(beanName, singletonObject);
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
}

创建 Bean

createBean 是创建 Bean 的入口方法,由 AbstractBeanFactory 定义,由 AbstractAutowireCapableBeanFactory 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) throws BeanCreationException {
// ...
try {
// 给 Bean 后置处理器一个返回代理的机会
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean;
}
}
// ...
// 常规的创建 Bean
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
return beanInstance;
}

doCreateBean

常规的创建 Bean 的具体工作是由 doCreateBean 完成的。

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
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final Object[] args) throws BeanCreationException {
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
// 使用相应的策略创建 bean 实例,例如通过工厂方法或者有参、无参构造器方法
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = (instanceWrapper != null ? instanceWrapper.getWrappedInstance() : null);
Class<?> beanType = (instanceWrapper != null ? instanceWrapper.getWrappedClass() : null);
mbd.resolvedTargetType = beanType;

// ...

// 使用 ObjectFactory 封装实例并缓存,以解决循环引用问题
boolean earlySingletonExposure = (mbd.isSingleton()
&& this.allowCircularReferences
&& isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
addSingletonFactory(beanName, new ObjectFactory<Object>() {
@Override
public Object getObject() throws BeansException {
return getEarlyBeanReference(beanName, mbd, bean);
}
});
}

Object exposedObject = bean;
try {
// 填充属性(包括解析依赖的 bean)
populateBean(beanName, mbd, instanceWrapper);
if (exposedObject != null) {
// 初始化 bean
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
}
// ...

// 如有需要,将 bean 注册为一次性的,以供 beanFactory 在关闭时调用销毁方法
try {
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
// ...

return exposedObject;
}

createBeanInstance

创建 Bean 实例,并使用 BeanWrapper 封装。实例化的方式:

  1. 工厂方法
  2. 构造器方法
    1. 有参
    2. 无参

populateBean

为创建出的实例填充属性,包括解析当前 bean 所依赖的 bean。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) {
PropertyValues pvs = mbd.getPropertyValues();
// ...

// 给 InstantiationAwareBeanPostProcessors 一个机会,
// 在设置 bean 属性前修改 bean 状态,可用于自定义的字段注入
boolean continueWithPropertyPopulation = true;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) {
continueWithPropertyPopulation = false;
break;
}
}
}
}

// 是否继续填充属性的流程
if (!continueWithPropertyPopulation) {
return;
}

if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME
|| mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
// 根据名称注入
if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
autowireByName(beanName, mbd, bw, newPvs);
}

// 根据类型注入
if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
autowireByType(beanName, mbd, bw, newPvs);
}
pvs = newPvs;
}

// 是否存在 InstantiationAwareBeanPostProcessors
boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors();
// 是否需要检查依赖
boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE);

if (hasInstAwareBpps || needsDepCheck) {
PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
if (hasInstAwareBpps) {
// 后置处理 PropertyValues
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
if (pvs == null) {
return;
}
}
}
}
if (needsDepCheck) {
checkDependencies(beanName, mbd, filteredPds, pvs);
}
}
// 将属性应用到 bean 上(常规情况下,前面的处理都用不上)
applyPropertyValues(beanName, mbd, bw, pvs);
}

initializeBean

在填充完属性后,实例就可以进行初始化工作:

  1. invokeAwareMethods,让 Bean 通过 xxxAware 接口感知一些信息
  2. 调用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
  3. invokeInitMethods,调用初始化方法
  4. 调用 BeanPostProcessor 的 postProcessAfterInitialization 方法
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
protected Object initializeBean(final String beanName, final Object bean, RootBeanDefinition mbd) {
// 处理 Aware 接口的相应方法
if (System.getSecurityManager() != null) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
invokeAwareMethods(beanName, bean);
return null;
}
}, getAccessControlContext());
}
else {
invokeAwareMethods(beanName, bean);
}

// 应用 BeanPostProcessor 的 postProcessBeforeInitialization 方法
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
// 调用初始化方法
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(
(mbd != null ? mbd.getResourceDescription() : null),
beanName, "Invocation of init method failed", ex);
}

if (mbd == null || !mbd.isSynthetic()) {
// 应用 BeanPostProcessor 的 postProcessAfterInitialization 方法
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}
return wrappedBean;
}
处理 Aware 接口的相应方法

让 Bean 在初始化中,感知(获知)和自身相关的资源,如 beanName、beanClassLoader 或者 beanFactory。

1
2
3
4
5
6
7
8
9
10
11
12
13
private void invokeAwareMethods(final String beanName, final Object bean) {
if (bean instanceof Aware) {
if (bean instanceof BeanNameAware) {
((BeanNameAware) bean).setBeanName(beanName);
}
if (bean instanceof BeanClassLoaderAware) {
((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader());
}
if (bean instanceof BeanFactoryAware) {
((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this);
}
}
}
调用初始化方法
  1. 如果 bean 实现 InitializingBean 接口,调用 afterPropertiesSet 方法
  2. 如果自定义 init 方法且满足调用条件,同样进行调用
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
protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) throws Throwable {
// 是否实现 InitializingBean 接口,是的话调用 afterPropertiesSet 方法
// 给 bean 一个感知属性已设置并做出反应的机会
boolean isInitializingBean = (bean instanceof InitializingBean);
if (isInitializingBean
&& (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
if (System.getSecurityManager() != null) {
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
@Override
public Object run() throws Exception {
((InitializingBean) bean).afterPropertiesSet();
return null;
}
}, getAccessControlContext());
}
catch (PrivilegedActionException pae) {
throw pae.getException();
}
}
else {
((InitializingBean) bean).afterPropertiesSet();
}
}

// 如果存在自定义的 init 方法且方法名称不是 afterPropertiesSet,判断是否调用
if (mbd != null) {
String initMethodName = mbd.getInitMethodName();
if (initMethodName != null
&& !(isInitializingBean && "afterPropertiesSet".equals(initMethodName))
&& !mbd.isExternallyManagedInitMethod(initMethodName)) {
invokeCustomInitMethod(beanName, bean, mbd);
}
}
}
BeanPostProcessor 处理

在调用初始化方法前后,BeanPostProcessor 先后进行两次处理。其实和 BeanPostProcessor 相关的代码都非常相似:

  1. 获取 Processor 列表
  2. 判断 Processor 类型是否是当前需要的
  3. 对 bean 进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) throws BeansException {

Object result = existingBean;
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
result = beanProcessor.postProcessBeforeInitialization(result, beanName);
if (result == null) {
return result;
}
}
return result;
}

public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) throws BeansException {

Object result = existingBean;
for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) {
result = beanProcessor.postProcessAfterInitialization(result, beanName);
if (result == null) {
return result;
}
}
return result;
}

再思 Bean 的初始化

以下代码片段一度让我困惑,从注释看,初始化 Bean 实例的工作包括了 populateBean 和 initializeBean,但是 initializeBean 方法的含义就是初始化 Bean。在 initializeBean 方法中,调用了 invokeInitMethods 方法,其含义仍然是调用初始化方法。
在更熟悉代码后,我有一种微妙的、个人性的体会,在 Spring 源码中,有时候视角的变化是很快的,痕迹是很小的。如果不加以理解和区分,很容易迷失在相似的描述中。以此处为例,“初始化 Bean 和 Bean 的初始化”扩展开来是 “Bean 工厂初始化一个 Bean 和 Bean 自身进行初始化”。

1
2
3
4
5
6
7
8
// Initialize the bean instance.
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
if (exposedObject != null) {
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
}

在注释这一行,视角是属于 BeanFactory(AbstractAutowireCapableBeanFactory)。从工厂的视角,面对一个刚刚创建出来的 Bean 实例,需要完成两方面的工作:

  1. 为 Bean 实例填充属性,包括解析依赖,为 Bean 自身的初始化做好准备。
  2. Bean 自身的初始化。

在万事俱备之后,就是 Bean 自身的初始化工作。由于 Spring 的高度扩展性,这部分并不只是单纯地调用初始化方法,还包含 Aware 接口和 BeanPostProcessor 的相关处理,前者偏属于 Java 对象层面,后者偏属于 Spring Bean 层面。
在认同 BeanPostProcessor 的处理属于 Bean 自身初始化工作的一部分后,@PostConstruct 注解的方法被称为 Bean 的初始化方法也就不那么违和了,因为它的实现原理正是 BeanPostProcessor,这仍然在 initializeBean 的范围内。

context 刷新流程简单图解

刷新流程

刷新流程中的组件

上下文刷新 AbstractApplicationContext#refresh

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
public void refresh() throws BeansException, IllegalStateException {
// 刷新和销毁的同步监视器。
synchronized (this.startupShutdownMonitor) {
// 1. 准备 context 以供刷新。
prepareRefresh();
// 2. 告诉 context 子类刷新内部 beanFactory 并返回。
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 3. 准备 beanFactory 以供在此 context 中使用。
prepareBeanFactory(beanFactory);
try {
// 4. 允许 context 子类对 bean 工厂进行后处理。
postProcessBeanFactory(beanFactory);
// 5. 调用在 context 中注册为 bean 的工厂后处理器。
invokeBeanFactoryPostProcessors(beanFactory);
// 6. 注册拦截 Bean 创建的 Bean 后处理器。
registerBeanPostProcessors(beanFactory);
// 7. 初始化 context 的消息源。
initMessageSource();
// 8. 为 context 初始化事件多播器。
initApplicationEventMulticaster();
// 9. 在特定 context 子类中初始化其他特殊 bean。
onRefresh();
// 10. 检查监听器 beans 并注册。
registerListeners();
// 11. 实例化所有剩余的(非惰性初始化)单例。
finishBeanFactoryInitialization(beanFactory);
// 12. 最后一步:发布相应的事件。
finishRefresh();
}
catch (BeansException ex) {
// ...
// 销毁已经创建的单例,以防止资源未正常释放。
destroyBeans();
// 重置 'active' flag.
cancelRefresh(ex);
throw ex;
}
finally {
resetCommonCaches();
}
}
}

准备 context 以供刷新 prepareRefresh

准备此 context 以供刷新,设置其启动日期和活动标志以及执行属性源的初始化。

编写管理资源的容器时,可以参考。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected void prepareRefresh() {
this.startupDate = System.currentTimeMillis();
this.closed.set(false);
this.active.set(true);
if (logger.isInfoEnabled()) {
logger.info("Refreshing " + this);
}
// 初始化 PropertySource,默认什么都不做。
initPropertySources();
// 校验所有被标记为 required 的 properties 都可以被解析。
getEnvironment().validateRequiredProperties();
// 允许收集早期的 ApplicationEvents,当事件多播器可用就发送。
this.earlyApplicationEvents = new LinkedHashSet<ApplicationEvent>();
}

obtainFreshBeanFactory

告诉子类刷新内部 bean 工厂并返回,返回的实例类型为 DefaultListableBeanFactory。在这里完成了配置文件的读取,初步注册了 bean 定义。

我大概这辈子都不会想理清楚这里面关于 XML 文件的解析过程,但是我知道可以在这里观察到 beanFactory 因为配置文件注册了哪些 bean。

1
2
3
4
5
6
7
8
9
10
11
protected ConfigurableListableBeanFactory obtainFreshBeanFactory() {
// 刷新 beanFactory
// 使用 XmlBeanDefinitionReader 加载配置文件中的 bean 定义
refreshBeanFactory();
// 返回 beanFactory
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (logger.isDebugEnabled()) {
logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory);
}
return beanFactory;
}

准备 beanFactory

配置 BeanFactory 以供在此 context 中使用,例如 context 的类加载器和一些后处理器,手动注册一些单例。

  1. 为 beanFactory 配置 context 相关的资源,如类加载器
  2. 添加 Bean 后处理器
    • ApplicationContextAwareProcessor,context 回调,注入特定类型时可触发自定义逻辑
    • ApplicationListenerDetector,检测 ApplicationListener
  3. 手动注册单例

ignoreDependencyInterface 和 registerResolvableDependency 在理解之后比单纯地记忆它们有趣许多。

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
protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// 告诉内部 beanFactory 使用 context 的类加载器等等。
beanFactory.setBeanClassLoader(getClassLoader());
beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader()));
beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment()));

// 为内部 beanFactory 配置 context 回调。
beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this));
// 如果一个 bean 的依赖实现了以下接口,忽略该依赖的检查和自动装配。
// 例如在 populateBean 时,如果 bena 的依赖存在 set 方法,就会去解析,调用 getBean
// 被设置 ignoreDependencyInterface 的依赖,仍然可以通过后置处理器进行依赖注入,例如以下的类型会使用上面那个后置处理器的回调方法注入。
// 因此 @Autowire 这些通过后置处理器实现依赖注入的注解,也不会受影响
// 这样设计的一个可能是往往注入这些类型时,希望触发某些事件。
beanFactory.ignoreDependencyInterface(EnvironmentAware.class);
beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class);
beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class);
beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class);
beanFactory.ignoreDependencyInterface(MessageSourceAware.class);
beanFactory.ignoreDependencyInterface(ApplicationContextAware.class);

// BeanFactory 之类的接口没有在普通工厂中注册为可解析类型,直接为它们指定 bean。
beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory);
beanFactory.registerResolvableDependency(ResourceLoader.class, this);
beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this);
beanFactory.registerResolvableDependency(ApplicationContext.class, this);

// 提前注册后处理器以检测内部 beans 是否是一个 ApplicationListener。
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this));

// 检测是否有 LoadTimeWeaver,如果存在就准备编织。
if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
// Set a temporary ClassLoader for type matching.
beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
}

// 手动注册默认的环境 beans。
if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment());
}
if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties());
}
if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) {
beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment());
}
}

在创建 Bean 开始前注册的单例,都属于手动注册的单例 manualSingletonNames

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
super.registerSingleton(beanName, singletonObject);

if (hasBeanCreationStarted()) {
// Cannot modify startup-time collection elements anymore (for stable iteration)
synchronized (this.beanDefinitionMap) {
if (!this.beanDefinitionMap.containsKey(beanName)) {
Set<String> updatedSingletons = new LinkedHashSet<String>(this.manualSingletonNames.size() + 1);
updatedSingletons.addAll(this.manualSingletonNames);
updatedSingletons.add(beanName);
this.manualSingletonNames = updatedSingletons;
}
}
}
else {
// Still in startup registration phase
if (!this.beanDefinitionMap.containsKey(beanName)) {
this.manualSingletonNames.add(beanName);
}
}

clearByTypeCache();
}

postProcessBeanFactory

在标准初始化后修改内部 beanFactory,默认什么都不做。

invokeBeanFactoryPostProcessors

实例化并调用所有在 context 中注册的 beanFactory 后处理器,需遵循顺序规则。具体的处理被委托给 PostProcessorRegistrationDelegate。

1
2
3
4
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
// ...
}

invokeBeanFactoryPostProcessors 方法堪比裹脚布。

关于调用顺序的规则

  1. BeanFactoryPostProcessor 分为 context 添加的和 beanFactory 注册的,前者优于后者
  2. BeanFactoryPostProcessor 又可分为常规的和 BeanDefinitionRegistryPostProcessor,后者优于前者
  3. PriorityOrdered 优于 Ordered 优于剩余的

可能新增 beanDefinition 的情况:

  1. BeanDefinitionRegistryPostProcessor 可能在 beanFactory 中引入新的 beanDefinition
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
public static void invokeBeanFactoryPostProcessors(
ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {

// 存储已处理过的后处理器
Set<String> processedBeans = new HashSet<String>();

// 第一阶段:BeanDefinitionRegistryPostProcessor
if (beanFactory instanceof BeanDefinitionRegistry) {
// 如果 beanFactory 同时是 BeanDefinitionRegistry 类型
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
// 存储常规的 BeanFactoryPostProcessor
List<BeanFactoryPostProcessor> regularPostProcessors = new LinkedList<BeanFactoryPostProcessor>();
// 存储 BeanDefinitionRegistryPostProcessor
List<BeanDefinitionRegistryPostProcessor> registryProcessors = new LinkedList<BeanDefinitionRegistryPostProcessor>();

// 第 0 轮,先对 context 注册的 BeanFactoryPostProcessor 进行分类
for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) {
if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) {
BeanDefinitionRegistryPostProcessor registryProcessor =
(BeanDefinitionRegistryPostProcessor) postProcessor;
// 分类的同时,直接调用 BeanDefinitionRegistryPostProcessor
registryProcessor.postProcessBeanDefinitionRegistry(registry);
registryProcessors.add(registryProcessor);
}
else {
regularPostProcessors.add(postProcessor);
}
}

// 将 BeanDefinitionRegistryPostProcessors 按是否实现 PriorityOrdered,Ordered,以及剩余的进行分类
List<BeanDefinitionRegistryPostProcessor> currentRegistryProcessors = new ArrayList<BeanDefinitionRegistryPostProcessor>();

// 第 1 轮,先处理 beanFactory 中实现了 PriorityOrdered 的 BeanDefinitionRegistryPostProcessor
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
// getBean 并添加到当前的后处理器集合
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
// 排序后添加
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();

// 第 2 轮,Ordered
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();

// 第 3 轮, 调用剩余的 BeanDefinitionRegistryPostProcessors 直到没有新的出现。
// 后出现的 PriorityOrdered 不比前面的 Ordered 更早被处理
boolean reiterate = true;
while (reiterate) {
reiterate = false;
postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false);
for (String ppName : postProcessorNames) {
if (!processedBeans.contains(ppName)) {
currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class));
processedBeans.add(ppName);
reiterate = true;
}
}
sortPostProcessors(currentRegistryProcessors, beanFactory);
registryProcessors.addAll(currentRegistryProcessors);
invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
currentRegistryProcessors.clear();
}

// 调用目前出现的 BeanFactoryPostProcessors
// 仍遵循 PriorityOrdered、Ordered、Regular(registry)、Regular(context 添加的) 的顺序
invokeBeanFactoryPostProcessors(registryProcessors, beanFactory);
invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory);
}

else {
// 否则,直接调用 context 注册的 beanFactoryPostProcessors
invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory);
}

// 第二阶段:BeanFactoryPostProcessor
String[] postProcessorNames =
beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false);

// 将 BeanFactoryPostProcessor 按是否实现 PriorityOrdered,Ordered,以及剩余的进行分类
List<BeanFactoryPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
List<String> orderedPostProcessorNames = new ArrayList<String>();
List<String> nonOrderedPostProcessorNames = new ArrayList<String>();
for (String ppName : postProcessorNames) {
if (processedBeans.contains(ppName)) {
// 第一阶段已处理,跳过
}
else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class));
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// 第 1 轮,BeanFactoryPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory);

// 第 2 轮,BeanFactoryPostProcessors that implement Ordered.
List<BeanFactoryPostProcessor> orderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
for (String postProcessorName : orderedPostProcessorNames) {
orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
sortPostProcessors(orderedPostProcessors, beanFactory);
invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory);

// 第 3 轮,剩余的 BeanFactoryPostProcessors.
List<BeanFactoryPostProcessor> nonOrderedPostProcessors = new ArrayList<BeanFactoryPostProcessor>();
for (String postProcessorName : nonOrderedPostProcessorNames) {
nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class));
}
invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory);

// 清理缓存的 merged bean definitions 因为 post-processors 可能已经修改了原来的 metadata
beanFactory.clearMetadataCache();
}

registerBeanPostProcessors

注册拦截 bean 创建的 bean 后处理器。具体的处理被委托给 PostProcessorRegistrationDelegate。

1
2
3
protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) {
PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this);
}

registerBeanPostProcessors 相比之下是一条清新的裹脚布。这里特别区分 3 种类型的 Bean 后处理器:

  • MergedBeanDefinitionPostProcessor
  • InstantiationAwareBeanPostProcessor 感知实例化
  • DestructionAwareBeanPostProcessor 感知销毁

ApplicationListenerDetector 既是 MergedBeanDefinitionPostProcessor,又是 DestructionAwareBeanPostProcessor,在初始化后将 listener 加入,在销毁前将 listener 移除。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public static void registerBeanPostProcessors(
ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) {
// 获取 BeanPostProcessor 的名称
String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);

// 注册 BeanPostProcessorChecker,在 BeanPostProcessor 实例化期间创建 bean 时,记录一条消息。
// 即,当一个 bean 不能被所有 BeanPostProcessors 处理时,记录。
int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length;
beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount));

// 将 BeanPostProcessors 按 implement PriorityOrdered,Ordered,和剩余的进行分类。
List<BeanPostProcessor> priorityOrderedPostProcessors = new ArrayList<BeanPostProcessor>();
List<BeanPostProcessor> internalPostProcessors = new ArrayList<BeanPostProcessor>();
List<String> orderedPostProcessorNames = new ArrayList<String>();
List<String> nonOrderedPostProcessorNames = new ArrayList<String>();
for (String ppName : postProcessorNames) {
if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
priorityOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
orderedPostProcessorNames.add(ppName);
}
else {
nonOrderedPostProcessorNames.add(ppName);
}
}

// 第 1 轮,注册 BeanPostProcessors that implement PriorityOrdered.
sortPostProcessors(priorityOrderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors);

// 第 2 轮,注册 BeanPostProcessors that implement Ordered.
List<BeanPostProcessor> orderedPostProcessors = new ArrayList<BeanPostProcessor>();
for (String ppName : orderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
orderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
sortPostProcessors(orderedPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, orderedPostProcessors);

// 第 3 轮,注册剩余的 regular BeanPostProcessors.
List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList<BeanPostProcessor>();
for (String ppName : nonOrderedPostProcessorNames) {
BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class);
nonOrderedPostProcessors.add(pp);
if (pp instanceof MergedBeanDefinitionPostProcessor) {
internalPostProcessors.add(pp);
}
}
registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors);

// 最后(第 4 轮), 排序并注册 internal BeanPostProcessors.
sortPostProcessors(internalPostProcessors, beanFactory);
registerBeanPostProcessors(beanFactory, internalPostProcessors);

// Re-register post-processor for detecting inner beans as ApplicationListeners,
// moving it to the end of the processor chain (for picking up proxies etc).
// 重新注册用于检测 ApplicationListeners 的 Bean 后处理器,将其移动到处理器链的最后(用于获取代理)。
beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext));
}

添加 BeanPostProcessor 时

  1. 先移除
  2. 再添加
  3. 判断类型并记录标记
    • 感知实例化的后处理器
    • 感知销毁的后处理器
1
2
3
4
5
6
7
8
9
10
11
public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) {
Assert.notNull(beanPostProcessor, "BeanPostProcessor must not be null");
this.beanPostProcessors.remove(beanPostProcessor);
this.beanPostProcessors.add(beanPostProcessor);
if (beanPostProcessor instanceof InstantiationAwareBeanPostProcessor) {
this.hasInstantiationAwareBeanPostProcessors = true;
}
if (beanPostProcessor instanceof DestructionAwareBeanPostProcessor) {
this.hasDestructionAwareBeanPostProcessors = true;
}
}

initMessageSource

初始化消息源。 如果在此 context 中未定义,则使用父级的。

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
protected void initMessageSource() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(MESSAGE_SOURCE_BEAN_NAME)) {
this.messageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, MessageSource.class);
// 设置 parent MessageSource.
if (this.parent != null && this.messageSource instanceof HierarchicalMessageSource) {
HierarchicalMessageSource hms = (HierarchicalMessageSource) this.messageSource;
if (hms.getParentMessageSource() == null) {
// 只有当 parent MessageSource 尚未注册才将 parent context 设置为 parent MessageSource
hms.setParentMessageSource(getInternalParentMessageSource());
}
}
if (logger.isDebugEnabled()) {
logger.debug("Using MessageSource [" + this.messageSource + "]");
}
}
else {
// 使用代理 messageSource,以此接收 getMessage 调用。
DelegatingMessageSource dms = new DelegatingMessageSource();
dms.setParentMessageSource(getInternalParentMessageSource());
this.messageSource = dms;
beanFactory.registerSingleton(MESSAGE_SOURCE_BEAN_NAME, this.messageSource);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate MessageSource with name '" + MESSAGE_SOURCE_BEAN_NAME +
"': using default [" + this.messageSource + "]");
}
}
}

initApplicationEventMulticaster

初始化 ApplicationEventMulticaster。 如果上下文中未定义,则使用 SimpleApplicationEventMulticaster。可以看得出代码的结构和 initMessageSource 是类似的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
if (logger.isDebugEnabled()) {
logger.debug("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
}
}
else {
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
if (logger.isDebugEnabled()) {
logger.debug("Unable to locate ApplicationEventMulticaster with name '" +
APPLICATION_EVENT_MULTICASTER_BEAN_NAME +
"': using default [" + this.applicationEventMulticaster + "]");
}
}
}

onRefresh

可以重写模板方法来添加特定 context 的刷新工作。默认情况下什么都不做。

registerListeners

获取侦听器 bean 并注册。无需初始化即可添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
protected void registerListeners() {
// 注册静态指定的 ApplicationListener,和 beanFactoryPostProcessor 类似,context 可以提前添加好。
for (ApplicationListener<?> listener : getApplicationListeners()) {
getApplicationEventMulticaster().addApplicationListener(listener);
}

// Do not initialize FactoryBeans here: We need to leave all regular beans
// uninitialized to let post-processors apply to them!
// 这段注释看到不止一次,但是不太理解,感觉和代码联系不起来?
String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
for (String listenerBeanName : listenerBeanNames) {
getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
}

// 现在我们终于有了多播器,发布早期的应用事件。
Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
this.earlyApplicationEvents = null;
if (earlyEventsToProcess != null) {
for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
getApplicationEventMulticaster().multicastEvent(earlyEvent);
}
}
}

添加 ApplicationListener。

后处理器 ApplicationListenerDetector 在 processor chain 的最后,最终会将创建的代理添加为监听器。什么情况下会出现代码中预防的情况呢?

1
2
3
4
5
6
7
8
9
10
11
public void addApplicationListener(ApplicationListener<?> listener) {
synchronized (this.retrievalMutex) {
// 如果已经注册,需要显式地删除代理,以避免同一监听器的双重调用。
Object singletonTarget = AopProxyUtils.getSingletonTarget(listener);
if (singletonTarget instanceof ApplicationListener) {
this.defaultRetriever.applicationListeners.remove(singletonTarget);
}
this.defaultRetriever.applicationListeners.add(listener);
this.retrieverCache.clear();
}
}

finishBeanFactoryInitialization

实例化所有剩余的(非惰性初始化)单例。以 context 视角,是完成内部 beanFactory 的初始化。

几乎可以只关注最后的 beanFactory.preInstantiateSingletons()

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
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// 为 context 初始化转换服务
if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
beanFactory.setConversionService(
beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
}

// 如果之前没有任何 bean 后处理器(例如 PropertyPlaceholderConfigurer)注册,则注册默认的嵌入值解析器,主要用于解析注释属性值。
// 接口 ConfigurableEnvironment 继承自 ConfigurablePropertyResolver
if (!beanFactory.hasEmbeddedValueResolver()) {
beanFactory.addEmbeddedValueResolver(new StringValueResolver() {
@Override
public String resolveStringValue(String strVal) {
return getEnvironment().resolvePlaceholders(strVal);
}
});
}

// 尽早初始化 LoadTimeWeaverAware,以便尽早注册其转换器。
String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
for (String weaverAwareName : weaverAwareNames) {
getBean(weaverAwareName);
}

// 停止使用临时类加载器进行类型匹配。
beanFactory.setTempClassLoader(null);

// 允许缓存所有 bean 定义的元数据,而不期望进一步更改。
beanFactory.freezeConfiguration();

// 实例化所有剩余的(非惰性初始化)单例。
beanFactory.preInstantiateSingletons();
}

确保所有非惰性初始化单例都已实例化,同时还要考虑 FactoryBeans。 如果需要,通常在工厂设置结束时调用。

加载 Bean 的流程分析在此

先对集合进行 Copy 再迭代是很常见的处理方式,可以有效保证迭代时不受原集合影响,也不会影响到原集合。

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
51
52
53
54
55
56
57
58
59
60
61
@Override
public void preInstantiateSingletons() throws BeansException {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Pre-instantiating singletons in " + this);
}

// 拷贝一份 beanDefinitionNames
List<String> beanNames = new ArrayList<String>(this.beanDefinitionNames);

// 触发所有非惰性初始化单例的实例化
for (String beanName : beanNames) {
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
if (isFactoryBean(beanName)) {
// FactoryBean
final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
boolean isEagerInit;
if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
isEagerInit = AccessController.doPrivileged(new PrivilegedAction<Boolean>() {
@Override
public Boolean run() {
return ((SmartFactoryBean<?>) factory).isEagerInit();
}
}, getAccessControlContext());
}
else {
isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
}
if (isEagerInit) {
// 是否立即初始化
getBean(beanName);
}
}
else {
// 常规 Bean(重要)
getBean(beanName);
}
}
}

// 触发所有适用 bean 的初始化后回调
for (String beanName : beanNames) {
Object singletonInstance = getSingleton(beanName);
if (singletonInstance instanceof SmartInitializingSingleton) {
final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
if (System.getSecurityManager() != null) {
AccessController.doPrivileged(new PrivilegedAction<Object>() {
@Override
public Object run() {
smartSingleton.afterSingletonsInstantiated();
return null;
}
}, getAccessControlContext());
}
else {
smartSingleton.afterSingletonsInstantiated();
}
}
}
}

finishRefresh

最后一步,完成 context 刷新,比如发布相应的事件。

1
2
3
4
5
6
7
8
9
10
protected void finishRefresh() {
// 1. 为此 context 初始化生命周期处理器。
initLifecycleProcessor();
// 2. 将刷新传播到生命周期处理器。
getLifecycleProcessor().onRefresh();
// 3. 发布 ContextRefreshedEvent。
publishEvent(new ContextRefreshedEvent(this));
// 4. 参与 LiveBeansView MBean(如果处于活动状态)。
LiveBeansView.registerApplicationContext(this);
}

组织类加载工作:loadClass

Java 程序启动的时候,Java 虚拟机会调用 java.lang.ClassLoader#loadClass(java.lang.String) 加载 main 方法所在的类。

1
2
3
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}

根据注释可知,此方法加载具有指定二进制名称的类,它由 Java 虚拟机调用来解析类引用,调用它等同于调用 loadClass(name, false)

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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 以二进制名称获取类加载的锁进行同步
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 首先检查类是否已加载,根据该方法注释可知:
// 如果当前类加载器已经被 Java 虚拟机记录为具有该二进制名称的类的加载器(initiating loader),Java 虚拟机可以直接返回 Class 对象。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果类还未加载,先委派给父·类加载器进行加载,如果父·类加载器为 null,则使用虚拟机内建的类加载器进行加载
if (parent != null) {
// 递归调用
c = parent.loadClass(name, false);
} else {
// 递归调用的终结点
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// 当父·类加载器长尝试加载但是失败,捕获异常但是什么都不做,因为接下来,当前类加载器需要自己也尝试加载。
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
// 父·类加载器未找到类,当前类加载器自己找。
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

根据注释可知,java.lang.ClassLoader#loadClass(java.lang.String, boolean) 同样是加载“具有指定二进制名称的类”,此方法的实现按以下顺序搜索类:

  1. 调用 findLoadedClass(String) 以检查该类是否已加载。
  2. 在父·类加载器上调用 loadClass 方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。
  3. 调用 findClass(String) 方法来查找该类。

如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 true,则此方法将对生成的 Class 对象调用 resolveClass(Class) 方法。鼓励 ClassLoader 的子类重写 findClass(String),而不是此方法。除非被重写,否则此方法在整个类加载过程中以 getClassLoadingLock 方法的结果进行同步。

注意:父·类加载器并非父类·类加载器(当前类加载器的父类),而是当前的类加载器的 parent 属性被赋值另外一个类加载器实例,其含义更接近于“可以委派类加载工作的另一个类加载器(一个帮忙干活的上级)”。虽然绝大多数说法中,当一个类加载器的 parent 值为 null 时,它的父·类加载器是引导类加载器(bootstrap class loader),但是当看到 findBootstrapClassOrNull 方法时,我有点困惑,因为我以为会看到语义类似于 loadClassByBootstrapClassLoader 这样的方法名。从注释和代码的语义上看,bootstrap class loader 不像是任何一个类加载器的父·类加载器,但是从类加载的机制设计上说,它是,只是因为它并非由 Java 语言编写而成,不能实例化并赋值给 parent 属性。findBootstrapClassOrNull 方法的语义更接近于:当一个类加载器的父·类加载器为 null 时,将准备加载的目标类先当作启动类(Bootstrap Class)尝试查找,如果找不到就返回 null

怎么并行地加载类 getClassLoadingLock

需要加载的类可能很多很多,我们很容易想到如果可以并行地加载类就好了。显然,JDK 的编写者考虑到了这一点。
此方法返回类加载操作的锁对象。为了向后兼容,此方法的默认实现的行为如下。如果此 ClassLoader 对象注册为具备并行能力,则该方法返回与指定类名关联的专用对象。 否则,该方法返回此 ClassLoader 对象。
简单地说,如果 ClassLoader 对象注册为具备并行能力,那么一个 name 一个锁对象,已创建的锁对象保存在 ConcurrentHashMap 类型的 parallelLockMap 中,这样类加载工作可以并行;否则所有类加载工作共用一个锁对象,就是 ClassLoader 对象本身。
这个方案意味着非同名的目标类可以认为在加载时没有冲突?

1
2
3
4
5
6
7
8
9
10
11
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
什么是 “ClassLoader 对象注册为具有并行能力”呢?

AppClassLoader 中有一段 static 代码。事实上 java.lang.ClassLoader#registerAsParallelCapable 是将 ClassLoader 对象注册为具有并行能力唯一的入口。因此,所有想要注册为具有并行能力的 ClassLoader 都需要调用一次该方法。

1
2
3
static {
ClassLoader.registerAsParallelCapable();
}

java.lang.ClassLoader#registerAsParallelCapable 方法有一个注解 @CallerSensitive,这是因为它的代码中调用的 native 方法 sun.reflect.Reflection#getCallerClass() 方法。由注释可知,当且仅当以下所有条件全部满足时才注册成功:

  1. 尚未创建调用者的实例(类加载器尚未实例化)
  2. 调用者的所有超类(Object 类除外)都注册为具有并行能力。
怎么保证这两个条件成立呢?
  1. 对于第一个条件,可以通过将调用的代码写在 static 代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。
  2. 对于第二个条件,由于 Java 虚拟机加载类时,总是会先尝试加载其父类,又因为加载类时会先调用 static 代码块,因此父类的 static 代码块总是先于子类的 static 代码块。

你可以看到 AppClassLoader->URLClassLoader->SecureClassLoader->ClassLoader 均在 static 代码块实现注册,以保证满足以上两个条件。

注册工作做了什么?

简单地说就是保存了类加载器所属 ClassSet

1
2
3
4
5
6
7
8
@CallerSensitive
protected static boolean registerAsParallelCapable() {
// 获得此方法的调用者的 Class 实例,asSubClass 可以将 Class<?> 类型的 Class 转换为代表指定类的子类的 Class<? extends U> 类型的 Class。
Class<? extends ClassLoader> callerClass =
Reflection.getCallerClass().asSubclass(ClassLoader.class);
// 注册调用者的 Class 为具有并行能力
return ParallelLoaders.register(callerClass);
}

方法 java.lang.ClassLoader.ParallelLoaders#registerParallelLoaders 封装了一组具有并行能力的加载器类型。就是持有 ClassLoaderClass 实例的集合,并保证添加时加同步锁。

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
// private 修饰,只有其外部类 ClassLoader 才可以使用
// static 修饰,内部类如果需要定义 static 方法或者 static 变量,必须用 static 修饰
private static class ParallelLoaders {
// private 修饰构造器方法,不希望这个类被实例化,只想要使用它的静态变量和方法。
private ParallelLoaders() {}

// the set of parallel capable loader types
// 使用 loaderTypes 时通过 synchronized 加同步锁
private static final Set<Class<? extends ClassLoader>> loaderTypes =
Collections.newSetFromMap(
// todo: 为什么使用弱引用来实现?为了卸载类时的垃圾回收?
new WeakHashMap<Class<? extends ClassLoader>, Boolean>());
static {
// 将 ClassLoader 本身注册为具有并行能力
synchronized (loaderTypes) { loaderTypes.add(ClassLoader.class); }
}

/**
* Registers the given class loader type as parallel capabale.
* Returns {@code true} is successfully registered; {@code false} if
* loader's super class is not registered.
*/
static boolean register(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
if (loaderTypes.contains(c.getSuperclass())) {
// register the class loader as parallel capable
// if and only if all of its super classes are.
// Note: given current classloading sequence, if
// the immediate super class is parallel capable,
// all the super classes higher up must be too.
// 当且仅当其所有超类都具有并行能力时,才将类加载器注册为具有并行能力。
// 注意:给定当前的类加载顺序(加载类时,Java 虚拟机总是先尝试加载其父类),如果直接超类具有并行能力,则所有更高的超类也必然具有并行能力。
loaderTypes.add(c);
return true;
} else {
return false;
}
}
}

/**
* Returns {@code true} if the given class loader type is
* registered as parallel capable.
*/
static boolean isRegistered(Class<? extends ClassLoader> c) {
synchronized (loaderTypes) {
return loaderTypes.contains(c);
}
}
}
“注册”怎么和锁产生联系?

但是以上的注册过程只是起到一个“标记”作用,没有涉及和锁相关的代码,那么这个“标记”是怎么和真正的锁产生联系呢?ClassLoader 提供了三个构造器方法:

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
private ClassLoader(Void unused, ClassLoader parent) {
// 由 private 修饰,不允许子类重写
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
// 如果类加载器已经注册为具有并行能力,则做一些赋值操作
parallelLockMap = new ConcurrentHashMap<>();
// 保存 package->certs 的 map 映射,相关的工作也可以并行进行
package2certs = new ConcurrentHashMap<>();
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
assertionLock = this;
}
}

// 由 protect 修饰,允许子类重写,传递了父·类加载器。
protected ClassLoader(ClassLoader parent) {
this(checkCreateClassLoader(), parent);
}

// 由 protect 修饰,允许子类重写,父·类加载器使用 getSystemClassLoader 方法返回的系统类加载器。
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}

ClassLoader 的构造器方法最终都调用 private 修饰的 java.lang.ClassLoader#ClassLoader(java.lang.Void, java.lang.ClassLoader),又因为父类的构造器方法总是先于子类的构造器方法被执行,这样一来,所有继承 ClassLoader 的类加载器在创建的时候都会根据在创建实例之前是否注册为具有并行能力而做不同的操作。

为什么注册的代码不能写在构造器方法里?

使用“注册”的代码也解释了 java.lang.ClassLoader#registerAsParallelCapable 为了满足调用成功的第一个条件为什么不能写在构造器方法中,因为使用这个机制的代码先于你在子类构造器方法里编写的代码被执行。
同时,不论是 loadLoader 还是 getClassLoadingLock 都是由 protect 修饰,允许子类重写,来自定义并行加载类的能力。

todo: 讨论自定义类加载器的时候,印象里似乎对并行加载类的提及比较少,之后留意一下。

检查目标类是否已加载

加载类之前显然需要检查目标类是否已加载,这项工作最终是交给 native 方法,在虚拟机中执行,就像在黑盒中一样。
todo: 不同类加载器同一个类名会如何判定?

1
2
3
4
5
6
7
protected final Class<?> findLoadedClass(String name) {
if (!checkName(name))
return null;
return findLoadedClass0(name);
}

private native final Class<?> findLoadedClass0(String name);

保证核心类库的安全性:双亲委派模型

正如在代码和注释中所看到的,正常情况下,类的加载工作先委派给自己的父·类加载器,即 parent 属性的值——另一个类加载器实例。一层一层向上委派直到 parentnull,代表类加载工作会尝试先委派给虚拟机内建的 bootstrap class loader 处理,然后由 bootstrap class loader 首先尝试加载。如果被委派方加载失败,委派方会自己再尝试加载。
正常加载类的是应用类加载器 AppClassLoader,它的 parentExtClassLoaderExtClassLoaderparentnull

在网上也能看到有人提到以前大家称之为“父·类加载器委派机制”,“双亲”一词易引人误解。

为什么要用这套奇怪的机制

这样设计很明显的一个目的就是保证核心类库的类加载安全性。比如 Object 类,设计者不希望编写代码的人重新写一个 Object 类并加载到 Java 虚拟机中,但是加载类的本质就是读取字节数据传递给 Java 虚拟机创建一个 Class 实例,使用这套机制的目的之一就是为了让核心类库先加载,同时先加载的类不会再次被加载。

通常流程如下:

  1. AppClassLoader 调用 loadClass 方法,先委派给 ExtClassLoader
  2. ExtClassLoader 调用 loadClass 方法,先委派给 bootstrap class loader
  3. bootstrap class loader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
  4. ExtClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
  5. ExtClassLoader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
  6. AppClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。

注释中提到鼓励重写 findClass 方法而不是 loadClass,因为正是该方法实现了所谓的“双亲委派模型”,java.lang.ClassLoader#findClass 实现了如何查找加载类。如果不是专门为了破坏这个类加载模型,应该选择重写 findClass;其次是因为该方法中涉及并行加载类的机制。

查找类资源:findClass

默认情况下,类加载器在自己尝试进行加载时,会调用 java.lang.ClassLoader#findClass 方法,该方法由子类重写。AppClassLoaderExtClassLoader 都是继承 URLClassLoader,而 URLClassLoader 重写了 findClass 方法。根据注释可知,该方法会从 URL 搜索路径查找并加载具有指定名称的类。任何引用 Jar 文件的 URL 都会根据需要加载并打开,直到找到该类。

过程如下:

  1. name 转换为 path,比如 com.example.BananaTest 转换为 com/example/BananaTest.class
  2. 使用 URL 搜索路径 URLClassPathpath 中获取 Resource,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。
  3. 调用 URLClassLoader 的私有方法 defineClass,该方法调用父类 SecureClassLoaderdefineClass 方法。
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
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
// todo:
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
// 将 name 转换为 path
String path = name.replace('.', '/').concat(".class");
// 从 URLClassPath 中查找 Resource
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
} catch (ClassFormatError e2) {
if (res.getDataError() != null) {
e2.addSuppressed(res.getDataError());
}
throw e2;
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}

查找类的目录列表:URLClassPath

URLClassLoader 拥有一个 URLClassPath 类型的属性 ucp。由注释可知,URLClassPath 类用于维护一个 URL 的搜索路径,以便从 Jar 文件和目录中加载类和资源。
URLClassPath 的核心构造器方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public URLClassPath(URL[] urls,
URLStreamHandlerFactory factory,
AccessControlContext acc) {
// 将 urls 保存到 ArrayList 类型的属性 path 中,根据注释,path 的含义为 URL 的原始搜索路径。
for (int i = 0; i < urls.length; i++) {
path.add(urls[i]);
}
// 将 urls 保存到 Stack 类型的属性 urls 中,根据注释,urls 的含义为未打开的 URL 列表。
push(urls);
if (factory != null) {
// 如果 factory 不为 null,使用它创建一个 URLStreamHandler 实例处理 Jar 文件。
jarHandler = factory.createURLStreamHandler("jar");
}
if (DISABLE_ACC_CHECKING)
this.acc = null;
else
this.acc = acc;
}
URLClassPath#getResource

URLClassLoader 调用 sun.misc.URLClassPath#getResource(java.lang.String, boolean) 方法获取指定名称对应的资源。根据注释,该方法会查找 URL 搜索路径上的第一个资源,如果找不到资源,则返回 null
显然,这里的 Loader 不是我们前面提到的类加载器。LoaderURLClassPath 的内部类,用于表示根据一个基本 URL 创建的资源和类的加载器。也就是说一个基本 URL 对应一个 Loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Resource getResource(String name, boolean check) {
if (DEBUG) {
System.err.println("URLClassPath.getResource(\"" + name + "\")");
}

Loader loader;
// 获取缓存(默认没有用)
int[] cache = getLookupCache(name);
// 不断获取下一个 Loader 来获取 Resource,直到获取到或者没有下一个 Loader
for (int i = 0; (loader = getNextLoader(cache, i)) != null; i++) {
Resource res = loader.getResource(name, check);
if (res != null) {
return res;
}
}
return null;
}
URLClassPath#getNextLoader

获取下一个 Loader,其实根据 index 从一个存放已创建 LoaderArrayList 中获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private synchronized Loader getNextLoader(int[] cache, int index) {
if (closed) {
return null;
}
if (cache != null) {
if (index < cache.length) {
Loader loader = loaders.get(cache[index]);
if (DEBUG_LOOKUP_CACHE) {
System.out.println("HASCACHE: Loading from : " + cache[index]
+ " = " + loader.getBaseURL());
}
return loader;
} else {
return null; // finished iterating over cache[]
}
} else {
// 获取 Loader
return getLoader(index);
}
}
URLClassPath#getLoader(int)
  1. index 到存放已创建 Loader 的列表中去获取(调用方传入的 index0 开始不断递增直到超过范围)。
  2. 如果 index 超过范围,说明已有的 Loader 都找不到目标 Resource,需要到未打开的 URL 中查找。
  3. 从未打开的 URL 中取出(pop)一个来创建 Loader,如果 urls 已经为空,则返回 null
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
51
52
53
54
55
56
57
private synchronized Loader getLoader(int index) {
if (closed) {
return null;
}
// Expand URL search path until the request can be satisfied
// or the URL stack is empty.
while (loaders.size() < index + 1) {
// Pop the next URL from the URL stack
// 如果 index 超过数组范围,需要从未打开的 URL 中取出一个,创建 Loader 并返回
URL url;
synchronized (urls) {
if (urls.empty()) {
return null;
} else {
url = urls.pop();
}
}
// Skip this URL if it already has a Loader. (Loader
// may be null in the case where URL has not been opened
// but is referenced by a JAR index.)
String urlNoFragString = URLUtil.urlNoFragString(url);
if (lmap.containsKey(urlNoFragString)) {
continue;
}
// Otherwise, create a new Loader for the URL.
Loader loader;
try {
// 根据 URL 创建 Loader
loader = getLoader(url);
// If the loader defines a local class path then add the
// URLs to the list of URLs to be opened.
URL[] urls = loader.getClassPath();
if (urls != null) {
push(urls);
}
} catch (IOException e) {
// Silently ignore for now...
continue;
} catch (SecurityException se) {
// Always silently ignore. The context, if there is one, that
// this URLClassPath was given during construction will never
// have permission to access the URL.
if (DEBUG) {
System.err.println("Failed to access " + url + ", " + se );
}
continue;
}
// Finally, add the Loader to the search path.
validateLookupCache(loaders.size(), urlNoFragString);
loaders.add(loader);
lmap.put(urlNoFragString, loader);
}
if (DEBUG_LOOKUP_CACHE) {
System.out.println("NOCACHE: Loading from : " + index );
}
return loaders.get(index);
}
URLClassPath#getLoader(java.net.URL)

根据指定的 URL 创建 Loader,不同类型的 URL 会返回不同具体实现的 Loader

  1. 如果 URL 不是以 / 结尾,认为是 Jar 文件,则返回 JarLoader 类型,比如 file:/C:/Users/xxx/.jdks/corretto-1.8.0_342/jre/lib/rt.jar
  2. 如果 URL/ 结尾,且协议为 file,则返回 FileLoader 类型,比如 file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/
  3. 如果 URL/ 结尾,且协议不会 file,则返回 Loader 类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private Loader getLoader(final URL url) throws IOException {
try {
return java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Loader>() {
public Loader run() throws IOException {
String file = url.getFile();
if (file != null && file.endsWith("/")) {
if ("file".equals(url.getProtocol())) {
return new FileLoader(url);
} else {
return new Loader(url);
}
} else {
return new JarLoader(url, jarHandler, lmap, acc);
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (IOException)pae.getException();
}
}

URLClassPath.FileLoader#getResource

FileLoadergetResource 为例,如果文件找到了,就会将文件包装成一个 FileInputStream,再将 FileInputStream 包装成一个 Resource 返回。

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
Resource getResource(final String name, boolean check) {
final URL url;
try {
URL normalizedBase = new URL(getBaseURL(), ".");
url = new URL(getBaseURL(), ParseUtil.encodePath(name, false));

if (url.getFile().startsWith(normalizedBase.getFile()) == false) {
// requested resource had ../..'s in path
return null;
}

if (check)
URLClassPath.check(url);

final File file;
if (name.indexOf("..") != -1) {
file = (new File(dir, name.replace('/', File.separatorChar)))
.getCanonicalFile();
if ( !((file.getPath()).startsWith(dir.getPath())) ) {
/* outside of base dir */
return null;
}
} else {
file = new File(dir, name.replace('/', File.separatorChar));
}

if (file.exists()) {
return new Resource() {
public String getName() { return name; };
public URL getURL() { return url; };
public URL getCodeSourceURL() { return getBaseURL(); };
public InputStream getInputStream() throws IOException
{ return new FileInputStream(file); };
public int getContentLength() throws IOException
{ return (int)file.length(); };
};
}
} catch (Exception e) {
return null;
}
return null;
}

ClassLoader 的搜索路径

从上文可知,ClassLoader 调用 findClass 方法查找类的时候,并不是漫无目的地查找,而是根据设置的类路径进行查找,不同的 ClassLoader 有不同的类路径。

以下是通过 IDEA 启动 Java 程序时的命令,可以看到其中通过 -classpath 指定了应用·类加载器 AppClassLoader 的类路径,该类路径除了包含常规的 JRE 的文件路径外,还额外添加了当前 maven 工程编译生成的 target\classes 目录。

1
C:\Users\xxx\.jdks\corretto-1.8.0_342\bin\java.exe -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:52959,suspend=y,server=n -javaagent:C:\Users\xxx\AppData\Local\JetBrains\IntelliJIdea2022.3\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\charsets.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\access-bridge-64.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\cldrdata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\dnsns.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jaccess.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\jfxrt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\localedata.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\nashorn.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunec.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunjce_provider.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunmscapi.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\sunpkcs11.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\ext\zipfs.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jce.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfr.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jfxswt.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\jsse.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\management-agent.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\resources.jar;C:\Users\xxx\.jdks\corretto-1.8.0_342\jre\lib\rt.jar;C:\Users\xxx\IdeaProjects\java-test\target\classes;C:\Program Files\JetBrains\IntelliJ IDEA 2022.3.3\lib\idea_rt.jar" org.example.BananaTest
bootstrap class loader

启动·类加载器 bootstrap class loader,加载核心类库,即 <JRE_HOME>/lib 目录中的部分类库,如 rt.jar,只有名字符合要求的 jar 才能被识别。 启动 Java 虚拟机时可以通过选项 -Xbootclasspath 修改默认的类路径,有三种使用方式:

  • -Xbootclasspath::完全覆盖核心类库的类路径,不常用,除非重写核心类库。
  • -Xbootclasspath/a: 以后缀的方式拼接在原搜索路径后面,常用。
  • -Xbootclasspath/p: 以前缀的方式拼接再原搜索路径前面.不常用,避免引起不必要的冲突。

IDEA 中编辑启动配置,添加 VM 选项,-Xbootclasspath:C:\Software,里面没有类文件,启动虚拟机失败,提示:

1
2
3
4
Error occurred during initialization of VM
java/lang/NoClassDefFoundError: java/lang/Object

进程已结束,退出代码1
ExtClassLoader

扩展·类加载器 ExtClassLoader,加载 <JRE_HOME>/lib/ext/ 目录中的类库。启动 Java 虚拟机时可以通过选项 -Djava.ext.dirs 修改默认的类路径。显然修改不当同样可能会引起 Java 程序的异常。

AppClassLoader

应用·类加载器 AppClassLoader ,加载应用级别的搜索路径中的类库。可以使用系统的环境变量 CLASSPATH 的值,也可以在启动 Java 虚拟机时通过选项 -classpath 修改。

CLASSPATHWindows 中,多个文件路径使用分号 ; 分隔,而 Linux 中则使用冒号 : 分隔。以下例子表示当前目录和另一个文件路径拼接而成的类路径。

  • Windows:.;C:\path\to\classes
  • Linux:.:/path/to/classes

事实上,AppClassLoader 最终的类路径,不仅仅包含 -classpath 的值,还会包含 -javaagent 指定的值。

字节数据转换为 Class 实例:defineClass

方法 defineClass,顾名思义,就是定义类,将字节数据转换为 Class 实例。在 ClassLoader 以及其子类中有很多同名方法,方法内各种处理和包装,最终都是为了使用 name 和字节数据等参数,调用 native 方法获得一个 Class 实例。
以下是定义类时最终可能调用的 native 方法。

1
2
3
4
5
6
7
8
9
private native Class<?> defineClass0(String name, byte[] b, int off, int len,
ProtectionDomain pd);

private native Class<?> defineClass1(String name, byte[] b, int off, int len,
ProtectionDomain pd, String source);

private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
int off, int len, ProtectionDomain pd,
String source);

其方法参数有:

  • name,目标类的名称。
  • byte[]ByteBuffer 类型的字节数据,offlen 只是为了定位传入的字节数组中关于目标类的字节数据,通常分别是 0 和字节数组的长度,毕竟专门构造一个包含无关数据的字节数组很无聊。
  • ProtectionDomain,保护域,todo:
  • sourceCodeSource 的位置。

defineClass 方法的调用过程,其实就是从 URLClassLoader 开始,一层一层处理后再调用父类的 defineClass 方法,分别经过了 SecureClassLoaderClassLoader

URLClassLoader#defineClass

此方法是再 URLClassLoaderfindClass 方法中,获得正确的 Resource 之后调用的,由 private 修饰。根据注释,它使用从指定资源获取的类字节来定义类,生成的类必须先解析才能使用。

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
private Class<?> defineClass(String name, Resource res) throws IOException {
long t0 = System.nanoTime();
// 获取最后一个 . 的位置
int i = name.lastIndexOf('.');
// 返回资源的 CodeSourceURL
URL url = res.getCodeSourceURL();
if (i != -1) {
// 截取包名 com.example
String pkgname = name.substring(0, i);
// Check if package already loaded.
Manifest man = res.getManifest();
definePackageInternal(pkgname, man, url);
}
// Now read the class bytes and define the class
// 先尝试以 ByteBuffer 的形式返回字节数据,如果资源的输入流不是在 ByteBuffer 之上实现的,则返回 null
java.nio.ByteBuffer bb = res.getByteBuffer();
if (bb != null) {
// Use (direct) ByteBuffer:
// 不常用
CodeSigner[] signers = res.getCodeSigners();
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
// 调用 java.security.SecureClassLoader#defineClass(java.lang.String, java.nio.ByteBuffer, java.security.CodeSource)
return defineClass(name, bb, cs);
} else {
// 以字节数组的形式返回资源数据
byte[] b = res.getBytes();
// must read certificates AFTER reading bytes.
// 必须再读取字节数据后读取证书,todo:
CodeSigner[] signers = res.getCodeSigners();
// 根据 URL 和签名者创建 CodeSource
CodeSource cs = new CodeSource(url, signers);
sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
// 调用 java.security.SecureClassLoader#defineClass(java.lang.String, byte[], int, int, java.security.CodeSource)
return defineClass(name, b, 0, b.length, cs);
}
}

Resource 类提供了 getBytes 方法,此方法以字节数组的形式返回字节数据。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public byte[] getBytes() throws IOException {
byte[] b;
// Get stream before content length so that a FileNotFoundException
// can propagate upwards without being caught too early
// 在获取内容长度之前获取流,以便 FileNotFoundException 可以向上传播而不会过早被捕获(todo: 不理解)
// 获取缓存的 InputStream
InputStream in = cachedInputStream();

// This code has been uglified to protect against interrupts.
// Even if a thread has been interrupted when loading resources,
// the IO should not abort, so must carefully retry, failing only
// if the retry leads to some other IO exception.
// 该代码为了防止中断有点丑陋。即使线程在加载资源时被中断,IO 也不应该中止,因此必须小心重试,只有当重试导致其他 IO 异常时才会失败。
// 检测当前线程是否收到中断信号,收到的话则返回 true 且清除中断状态,重新变更为未中断状态。
boolean isInterrupted = Thread.interrupted();
int len;
for (;;) {
try {
// 获取内容长度,顺利的话就跳出循环
len = getContentLength();
break;
} catch (InterruptedIOException iioe) {
// 如果获取内容长度时,线程被中断抛出了异常,捕获后清除中断状态
Thread.interrupted();
isInterrupted = true;
}
}

try {
b = new byte[0];
if (len == -1) len = Integer.MAX_VALUE;
int pos = 0;
while (pos < len) {
int bytesToRead;
if (pos >= b.length) { // Only expand when there's no room
// 如果当前读取位置已经大于等于数组长度
// 本次待读取字节长度 = 剩余未读取长度和 1024 取较小值
bytesToRead = Math.min(len - pos, b.length + 1024);
if (b.length < pos + bytesToRead) {
// 如果当前读取位置 + 本次待读取字节长度 > 数组长度,则创建新数组并复制数据
b = Arrays.copyOf(b, pos + bytesToRead);
}
} else {
// 数组还有空间,待读取字节长度 = 数组剩余空间
bytesToRead = b.length - pos;
}
int cc = 0;
try {
// 读取数据
cc = in.read(b, pos, bytesToRead);
} catch (InterruptedIOException iioe) {
// 如果读取时,线程被中断抛出了异常,捕获后清除中断状态
Thread.interrupted();
isInterrupted = true;
}
if (cc < 0) {
// 如果读取返回值 < 0
if (len != Integer.MAX_VALUE) {
// 且长度并未无限,表示提前检测到 EOF,抛出异常
throw new EOFException("Detect premature EOF");
} else {
// 如果长度无限,表示读到了文件结尾,数组长度大于当前读取位置,创建新数组并复制长度
if (b.length != pos) {
b = Arrays.copyOf(b, pos);
}
break;
}
}
pos += cc;
}
} finally {
try {
in.close();
} catch (InterruptedIOException iioe) {
isInterrupted = true;
} catch (IOException ignore) {}

if (isInterrupted) {
// 如果 isInterrupted 为 true,代表中断过,重新将线程状态置为中断。
Thread.currentThread().interrupt();
}
}
return b;
}

getByteBuffer 之后会缓存 InputStream 以便调用 getBytes 时使用,方法由 synchronized 修饰。

1
2
3
4
5
6
private synchronized InputStream cachedInputStream() throws IOException {
if (cis == null) {
cis = getInputStream();
}
return cis;
}

在这个例子中,Resource 的实例是 URLClassPath 中的匿名类 FileLoaderResource 的匿名类的方式创建的。

1
2
3
4
5
6
7
8
9
10
11
public InputStream getInputStream() throws IOException
{
// 在该匿名类中,getInputStream 的实现就是简单地根据 FileLoader 中保存的 File 实例创建 FileInputStream 并返回。
return new FileInputStream(file);
}

public int getContentLength() throws IOException
{
// 在该匿名类中,getContentLength 的实现就是简单地根据 FileLoader 中保存的 File 实例获取长度。
return (int)file.length();
};

SecureClassLoader#defineClass

URLClassLoader 继承自 SecureClassLoaderSecureClassLoader 提供并重载了 defineClass 方法,两个方法的注释均比代码长得多。
由注释可知,方法的作用是将字节数据(byte[] 类型或者 ByteBuffer 类型)转换为 Class 类型的实例,有一个可选的 CodeSource 类型的参数。

1
2
3
4
5
6
7
8
9
10
11
12
protected final Class<?> defineClass(String name,
byte[] b, int off, int len,
CodeSource cs)
{
return defineClass(name, b, off, len, getProtectionDomain(cs));
}

protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
CodeSource cs)
{
return defineClass(name, b, getProtectionDomain(cs));
}

方法中只是简单地将 CodeSource 类型的参数转换成 ProtectionDomain 类型,就调用 ClassLoaderdefineClass 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private ProtectionDomain getProtectionDomain(CodeSource cs) {
// 如果 CodeSource 为 null,直接返回 null
if (cs == null)
return null;

ProtectionDomain pd = null;
synchronized (pdcache) {
// 先从 Map 缓存中获取 ProtectionDomain
pd = pdcache.get(cs);
if (pd == null) {
// 从 CodeSource 中获取 PermissionCollection
PermissionCollection perms = getPermissions(cs);
// 缓存中没有,则创建一个 ProtectionDomain 并放入缓存
pd = new ProtectionDomain(cs, perms, this, null);
pdcache.put(cs, pd);
if (debug != null) {
debug.println(" getPermissions "+ pd);
debug.println("");
}
}
}
return pd;
}
getPermissions

根据注释可知,此方法会返回给定 CodeSource 对象的权限。此方法由 protect 修饰,AppClassLoaderURLClassLoader 都有重写。当前 ClassLoaderAppClassLoader

AppClassLoader#getPermissions,添加允许从类路径加载的任何类退出 VM的权限。

1
2
3
4
5
6
7
8
9
protected PermissionCollection getPermissions(CodeSource codesource)
{
// 调用父类 URLClassLoader 的 getPermissions
PermissionCollection perms = super.getPermissions(codesource);
// 允许从类路径加载的任何类退出 VM的权限。
// todo: 这是否自定义的类加载器加载的类,可能不能退出 VM。
perms.add(new RuntimePermission("exitVM"));
return perms;
}

SecureClassLoader#getPermissions,添加一个读文件或读目录的权限。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
protected PermissionCollection getPermissions(CodeSource codesource)
{
// 调用父类 SecureClassLoader 的 getPermissions
PermissionCollection perms = super.getPermissions(codesource);

URL url = codesource.getLocation();

Permission p;
URLConnection urlConnection;

try {
// FileURLConnection 实例
urlConnection = url.openConnection();
// 允许 read 的 FilePermission 实例
p = urlConnection.getPermission();
} catch (java.io.IOException ioe) {
p = null;
urlConnection = null;
}

if (p instanceof FilePermission) {
// if the permission has a separator char on the end,
// it means the codebase is a directory, and we need
// to add an additional permission to read recursively
// 如果文件路径以文件分隔符结尾,表示目录,需要在末尾添加"-"改为递归读的权限
String path = p.getName();
if (path.endsWith(File.separator)) {
path += "-";
p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
}
} else if ((p == null) && (url.getProtocol().equals("file"))) {
String path = url.getFile().replace('/', File.separatorChar);
path = ParseUtil.decode(path);
if (path.endsWith(File.separator))
path += "-";
p = new FilePermission(path, SecurityConstants.FILE_READ_ACTION);
} else {
/**
* Not loading from a 'file:' URL so we want to give the class
* permission to connect to and accept from the remote host
* after we've made sure the host is the correct one and is valid.
*/
URL locUrl = url;
if (urlConnection instanceof JarURLConnection) {
locUrl = ((JarURLConnection)urlConnection).getJarFileURL();
}
String host = locUrl.getHost();
if (host != null && (host.length() > 0))
p = new SocketPermission(host,
SecurityConstants.SOCKET_CONNECT_ACCEPT_ACTION);
}

// make sure the person that created this class loader
// would have this permission

if (p != null) {
final SecurityManager sm = System.getSecurityManager();
if (sm != null) {
final Permission fp = p;
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() throws SecurityException {
sm.checkPermission(fp);
return null;
}
}, acc);
}
perms.add(p);
}
return perms;
}

SecureClassLoader#getPermissions,延迟设置权限,在创建 ProtectionDomain 时再设置。

1
2
3
4
5
6
7
8
protected PermissionCollection getPermissions(CodeSource codesource)
{
// 检查以确保类加载器已初始化。在 SecureClassLoader 构造器最后会用一个布尔变量表示加载器初始化成功。
// 从代码上看,似乎只能保证 SecureClassLoader 的构造器方法已执行完毕?
check();
// ProtectionDomain 延迟绑定,Permissions 继承 PermissionCollection 类。
return new Permissions(); // ProtectionDomain defers the binding
}
ProtectionDomain

ProtectionDomain 的相关构造器参数:

  • CodeSource
  • PermissionCollection,如果不为 null,会设置权限为只读,表示权限在使用过程中不再修改;同时检查是否需要设置拥有全部权限。
  • ClassLoader
  • Principal[]

这样看来,SecureClassLoader 为了定义类做的处理,就是简单地创建一些关于权限的对象,并保存了 CodeSource->ProtectionDomain 的映射作为缓存。

ClassLoader#defineClass

抽象类 ClassLoader 中最终用于定义类的 native 方法 define0define1define2 都是由 private 修饰的,ClassLoader 提供并重载了 defineClass 方法作为使用它们的入口,这些 defineClass 方法都由 protect final 修饰,这意味着这些方法只能被子类使用,并且不能被重写。

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
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
int len = b.remaining();

// Use byte[] if not a direct ByteBufer:
if (!b.isDirect()) {
if (b.hasArray()) {
return defineClass(name, b.array(),
b.position() + b.arrayOffset(), len,
protectionDomain);
} else {
// no array, or read-only array
byte[] tb = new byte[len];
b.get(tb); // get bytes out of byte buffer.
return defineClass(name, tb, 0, len, protectionDomain);
}
}

protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass2(name, b, b.position(), len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}

主要步骤:

  1. preDefineClass 前置处理
  2. defineClassX
  3. postDefineClass 后置处理
preDefineClass

确定保护域 ProtectionDomain,并检查:

  1. 未定义 java.*
  2. 该类的签名者与包(package)中其余类的签名者相匹配
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
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
// 检查 name 为 null 或者有可能是有效的二进制名称
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);

// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
// 如果 name 以 java. 开头,则抛出异常
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
// 如果未传入 ProtectionDomain,取默认的 ProtectionDomain
pd = defaultDomain;
}

// 存放了 package->certs 的 map 映射作为缓存,检查一个包内的 certs 都是一样的
// todo: certs
if (name != null) checkCerts(name, pd.getCodeSource());

return pd;
}
defineClassSourceLocation

确定 ClassCodeSource 位置。

1
2
3
4
5
6
7
8
9
private String defineClassSourceLocation(ProtectionDomain pd)
{
CodeSource cs = pd.getCodeSource();
String source = null;
if (cs != null && cs.getLocation() != null) {
source = cs.getLocation().toString();
}
return source;
}
defineClassX 方法

这些 native 方法使用了 name,字节数据,ProtectionDomainsource 等参数,像黑盒一样,在虚拟机中定义了一个类。

postDefineClass

在定义类后使用 ProtectionDomain 中的 certs 补充 Class 实例的 signer 信息,猜测在 native 方法 defineClassX 方法中,对 ProtectionDomain 做了一些修改。事实上,从代码上看,将 CodeSource 包装为 ProtectionDomain 传入后,除了 defineClassX 方法外,其他地方都是取出 CodeSource 使用。

1
2
3
4
5
6
7
8
9
private void postDefineClass(Class<?> c, ProtectionDomain pd)
{
if (pd.getCodeSource() != null) {
// 获取证书
Certificate certs[] = pd.getCodeSource().getCertificates();
if (certs != null)
setSigners(c, certs);
}
}

本文记录了 Tmux 常用命令和快捷键作为备忘笔记(好容易忘啊 Orz)。

阅读全文 »

本文介绍了如何使用 SSH 连接 Github 和免密登录服务器作为备忘笔记,主要在新建虚拟机或重装云服务器系统时使用。

阅读全文 »

环境搭建

安装 Ubuntu 20.04

使用 Vmware Workstation 通过 ubuntu-20.04.6-live-server-amd64.iso 安装。需要满足条件如下:

  • 2 个或更多 CPU(2 CPU)。
  • 2GB 可用内存(4GB)。
  • 20GB 可用磁盘空间(30GB)。
  • 网络连接
  • 容器或虚拟机管理器(Docker)。

安装 Docker

参考官方文档:Install Docker Engine on Ubuntu

安装 Minikube

参考官方文档:minikube start

安装 kubectl 并启动 kubectl 自动补全功能

参考官方文档:在 Linux 系统中安装并设置 kubectl

开始使用

创建集群:

1
minikube start

创建集群时的权限问题

不加 sudo

不加 sudo 的时候,创建集群失败,提示无法选择默认 driver。可能是 docker 处于不健康状态或者用户权限不足。

1
2
3
4
5
6
7
8
9
10
11
12
$ minikube start
* minikube v1.30.1 on Ubuntu 20.04
* Unable to pick a default driver. Here is what was considered, in preference order:
- docker: Not healthy: "docker version --format {{.Server.Os}}-{{.Server.Version}}:{{.Server.Platform.Name}}" exit status 1: permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.24/version": dial unix /var/run/docker.sock: connect: permission denied
- docker: Suggestion: Add your user to the 'docker' group: 'sudo usermod -aG docker $USER && newgrp docker' <https://docs.docker.com/engine/install/linux-postinstall/>
* Alternatively you could install one of these drivers:
- kvm2: Not installed: exec: "virsh": executable file not found in $PATH
- podman: Not installed: exec: "podman": executable file not found in $PATH
- qemu2: Not installed: exec: "qemu-system-x86_64": executable file not found in $PATH
- virtualbox: Not installed: unable to find VBoxManage in $PATH

X Exiting due to DRV_NOT_HEALTHY: Found driver(s) but none were healthy. See above for suggestions how to fix installed drivers.
使用 sudo

使用 sudo 的时候,会提示不建议通过 root 权限使用 docker,如果还是想要继续,可以使用选项 --force

1
2
3
4
5
6
7
8
$ sudo minikube start
* minikube v1.30.1 on Ubuntu 20.04
* Automatically selected the docker driver. Other choices: none, ssh
* The "docker" driver should not be used with root privileges. If you wish to continue as root, use --force.
* If you are running minikube within a VM, consider using --driver=none:
* https://minikube.sigs.k8s.io/docs/reference/drivers/none/

X Exiting due to DRV_AS_ROOT: The "docker" driver should not be used with root privileges.
使用选项 –force

考虑到仅用于测试,尝试通过 sudo minikube start --force 启动集群,成功启动集群但是提示使用该选项可能会引发未知行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ sudo minikube start --force
* minikube v1.30.1 on Ubuntu 20.04
! minikube skips various validations when --force is supplied; this may lead to unexpected behavior
* Automatically selected the docker driver. Other choices: ssh, none
* The "docker" driver should not be used with root privileges. If you wish to continue as root, use --force.
* If you are running minikube within a VM, consider using --driver=none:
* https://minikube.sigs.k8s.io/docs/reference/drivers/none/
* Using Docker driver with root privileges
* Starting control plane node minikube in cluster minikube
* Pulling base image ...
* Downloading Kubernetes v1.26.3 preload ...
> preloaded-images-k8s-v18-v1...: 397.02 MiB / 397.02 MiB 100.00% 25.89 M
> index.docker.io/kicbase/sta...: 373.53 MiB / 373.53 MiB 100.00% 7.17 Mi
! minikube was unable to download gcr.io/k8s-minikube/kicbase:v0.0.39, but successfully downloaded docker.io/kicbase/stable:v0.0.39 as a fallback image
* Creating docker container (CPUs=2, Memory=2200MB) ...
* Preparing Kubernetes v1.26.3 on Docker 23.0.2 ...
- Generating certificates and keys ...
- Booting up control plane ...
- Configuring RBAC rules ...
* Configuring bridge CNI (Container Networking Interface) ...
- Using image gcr.io/k8s-minikube/storage-provisioner:v5
* Verifying Kubernetes components...
* Enabled addons: default-storageclass, storage-provisioner
* Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

成功启动集群后,使用 kubectl get pod 测试,提示连接被拒绝。

1
2
3
4
5
6
7
$ kubectl get pod
E0622 21:55:27.400754 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.401000 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.410464 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.410951 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
E0622 21:55:27.412076 18561 memcache.go:265] couldn't get current server API group list: Get "http://localhost:8080/api?timeout=32s": dial tcp 127.0.0.1:8080: connect: connection refused
The connection to the server localhost:8080 was refused - did you specify the right host or port?

使用 minikube dashboard 启动控制台,在访问时同样提示连接被拒绝:dial tcp 127.0.0.1:8080: connect: connection refused

考虑到可能还有别的问题,决定采用官方建议将用户添加到 docker 用户组。

将用户添加到 docker 用户组

使用 sudo usermod -aG docker $USER && newgrp docker 将当前用户添加到 docker 用户组并切换当前用户组到 docker 用户组后,正常启动集群。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ minikube start
* minikube v1.30.1 on Ubuntu 20.04
* Automatically selected the docker driver. Other choices: ssh, none
* Using Docker driver with root privileges
* Starting control plane node minikube in cluster minikube
* Pulling base image ...
* Downloading Kubernetes v1.26.3 preload ...
> preloaded-images-k8s-v18-v1...: 393.36 MiB / 397.02 MiB 99.08% 19.35 Mi! minikube was unable to download gcr.io/k8s-minikube/kicbase:v0.0.39, but successfully downloaded docker.io/kicbase/stable:v0.0.39 as a fallback image
> preloaded-images-k8s-v18-v1...: 397.02 MiB / 397.02 MiB 100.00% 21.44 M
* Creating docker container (CPUs=2, Memory=2200MB) ...
* Preparing Kubernetes v1.26.3 on Docker 23.0.2 ...
- Generating certificates and keys ...
- Booting up control plane ...
- Configuring RBAC rules ...
* Configuring bridge CNI (Container Networking Interface) ...
- Using image gcr.io/k8s-minikube/storage-provisioner:v5
* Verifying Kubernetes components...
* Enabled addons: storage-provisioner, default-storageclass
* Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

创建集群时下载镜像失败

在解决问题的过程中,发现有人存在下载镜像失败的情况。从启动日志可以看到,由于 minikube 下载 gcr.io/k8s-minikube/kicbase:v0.0.39 镜像失败,自动下载 docker.io/kicbase/stable:v0.0.39 镜像作为备选。如果从 docker.io 下载镜像也很困难,还可以通过指定镜像仓库启动集群。可以通过查看帮助内关于仓库的信息,获取官方建议中国大陆用户使用的镜像仓库地址。

1
2
3
4
5
$ minikube start --help | grep repo
--image-repository='':
Alternative image repository to pull docker images from. This can be used when you have limited access to gcr.io. Set it to "auto" to let minikube decide one for you. For Chinese mainland users, you may use local gcr.io mirrors such as registry.cn-hangzhou.aliyuncs.com/google_containers

$ minikube start --image-repository='registry.cn-hangzhou.aliyuncs.com/google_containers'

如何通过宿主机进行访问 minikube 控制台

启动控制台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ minikube dashboard
* Enabling dashboard ...
- Using image docker.io/kubernetesui/dashboard:v2.7.0
- Using image docker.io/kubernetesui/metrics-scraper:v1.0.8
* Some dashboard features require the metrics-server addon. To enable all features please run:

minikube addons enable metrics-server


* Verifying dashboard health ...
* Launching proxy ...
* Verifying proxy health ...
* Opening http://127.0.0.1:35967/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/ in your default browser...
http://127.0.0.1:35967/api/v1/namespaces/kubernetes-dashboard/services/http:kubernetes-dashboard:/proxy/

如果虚拟机安装的 Ubuntu 是 Desktop 版本,那么你可以在 Ubuntu 里直接通过浏览器访问。但是如果你安装的 Ubuntu 是 server 版本,除了使用 curl 访问 url 外,你也许想要在宿主机的浏览器访问:

1
kubectl proxy --port=your-port --address='your-virtual-machine-ip' --accept-hosts='^.*' &

使用过程中下载镜像失败

从其他镜像仓库下载代替

一般是在需要从 gcr.io 镜像仓库下载时发生,比如官方教程中需要执行以下命令,会发现 deploymentpod 迟迟不能达到目标状态。

1
2
3
4
5
6
7
8
9
$ kubectl create deployment hello-node --image=registry.k8s.io/e2e-test-images/agnhost:2.39 -- /agnhost netexec --http-port=8080

$ kubectl get deployments
NAME READY UP-TO-DATE AVAILABLE AGE
hello-node 0/1 1 0 19s

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
hello-node-7b87cd5f68-zwd4g 0/1 ImagePullBackOff 0 38s

仅作为测试用途,可以从 Docker 官方仓库搜索镜像,找到排名靠前,版本相同或相近的可靠镜像代替。

配置代理

对于这类网络连接不通的情况,配置代理是通用的解决方案。开始使用后发现从其他镜像仓库下载代替的方法有点麻烦,于是决定设置代理。
刚开始我以为给 Ubuntu 上的 dockerd 配置代理帮助加速 docker pull 即可,后来发现仍然下载失败。即使我通过 docker pull 先下载镜像到本地,配置 imagePullPolicyNever 或者 IfNotPresent,minikube 还是不能识别到本地已有的镜像。猜测 minikube 的机制和我想象的是不同的,需要直接为 minikube 容器配置代理。搜索到以下命令满足需求:

1
minikube start --docker-env http_proxy=http://proxyAddress:port --docker-env https_proxy=http://proxyAddress:port --docker-env no_proxy=localhost,127.0.0.1,your-virtual-machine-ip/24

需要使用自定义镜像

测试过程中遇到需要使用自定义镜像的场景。在上一个问题中,我们已经发现 Minikube 不能直接使用本地已有的镜像,但是有两种方法可以解决该问题。

minikube image load
1
minikube image load <IMAGE_NAME>
minikube image build
1
minikube image build -t <IMAGE_NAME> .

参考链接

Install Docker Engine on Ubuntu
minikube start
k8s的迷你版——minikube+docker的安装
minikube - Why The “docker” driver should not be used with root privileges
安装Minikube无法访问k8s.gcr.io的简单解决办法
让其他电脑访问minikube dashboard
【问题解决】This container is having trouble accessing https://k8s.gcr.io | 如何解决从k8s.gcr.io拉取镜像失败问题?
K8S(kubernetes)镜像源
minikube 设置代理
两种在Minikube中运行本地Docker镜像的简单方式

Ubuntu 上安装 Clash 后,Clash 通过监听本地的 7890 端口,提供代理服务。但是不同程序设置代理的方式不尽相同,并不是启动了 Clash 以及在某一处设置后,整个系统发出的 HTTP 请求都能经过代理。本文将介绍如何为终端、docker 和容器添加代理。

为终端设置代理

有时候,我们需要在终端通过执行命令的方式访问网络和下载资源,比如使用 wgetcurl

设置 Shell 环境变量

这一类软件都是可以通过为 Shell 设置环境变量的方式来设置代理,涉及到的环境变量有 http_proxyhttps_proxyno_proxy
仅为当前会话设置,执行命令:

1
2
3
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1

永久设置代理,在设置 Shell 环境变量的脚本中(不同 Shell 的配置文件不同,比如 ~/.bashrc~/.zshrc)添加:

1
2
3
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1

重新启动一个会话或者执行命令 source ~/.bashrc 使其在当前会话立即生效。

修改 wget 配置文件

在搜索过程中发现还可以在 wget 的配置文件 ~/.wgetrc 中添加:

1
2
3
4
use_proxy = on

http_proxy = http://proxyAddress:port
https_proxy = http://proxyAddress:port

为 docker 设置代理

如果你以为为终端设置代理后 docker 就会使用代理,那你就错了。在从官方的镜像仓库 pull 镜像反复出错后并收到类似 Error response from daemon: Get "https://registry-1.docker.io/v2/": read tcp 192.168.3.140:59460->44.205.64.79:443: read: connection reset by peer 这样的报错信息后,我才开始怀疑我并没有真正给 docker 设置好代理。
在执行 docker pull 命令时,实际上命令是由守护进程 docker daemon 执行的。

通过 systemd 设置

如果你的 docker daemon 是通过 systemd 管理的,那么你可以通过设置 docker.service 服务的环境变量来设置代理。
执行命令查看 docker.service 信息,得知配置文件位置 /lib/systemd/system/docker.service

1
2
3
4
5
6
7
8
9
10
11
~$ systemctl status docker.service 
● docker.service - Docker Application Container Engine
Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
Active: active (running) since Tue 2023-06-13 00:52:54 CST; 22h ago
TriggeredBy: ● docker.socket
Docs: https://docs.docker.com
Main PID: 387690 (dockerd)
Tasks: 139
Memory: 89.6M
CPU: 1min 26.512s
CGroup: /system.slice/docker.service

docker.service[Service] 模块添加:

1
2
3
Environment=HTTP_PROXY=http://proxyAddress:port
Environment=HTTPS_PROXY=http://proxyAddress:port
Environment=NO_PROXY=localhost,127.0.0.1

重新加载配置文件并重启服务:

1
2
systemctl daemon-reload
systemctl restart docker.service

修改 dockerd 配置文件

还可以修改 dockerd 配置文件,添加:

1
export http_proxy="http://proxyAddress:port"

然后重启 docker daemon 即可。

国内的镜像仓库在绝大多数时候都可以满足条件,但是存在个别镜像同步不及时的情况,如果使用 latest 标签拉取到的镜像并非近期的镜像,因此有时候需要直接从官方镜像仓库拉取镜像。

为 docker 容器设置代理

docker daemon 进程设置代理和为 docker 容器设置代理是有区别的。比如使用 docker 启动媒体服务器 jellyfin 后,jellyfin 的刮削功能就需要代理才能正常使用,这时候不要因为在很多地方设置过代理就以为容器内部已经在使用代理了。

修改配置文件

创建或修改 ~/.docker/config.json,添加:

1
2
3
4
5
6
7
8
9
10
11
{
"proxies":
{
"default":
{
"httpProxy": "http://proxyAddress:port",
"httpsProxy": "http://proxyAddress:port",
"noProxy": "localhost,127.0.0.1"
}
}
}

此后创建的新容器,会自动设置环境变量来使用代理。

为指定容器添加环境变量

在启动容器时使用 -e 手动注入环境变量 http_proxy。这意味着进入容器使用 export 设置环境变量的方式也是可行的。

注意:如果代理是使用宿主机的代理,当网络为 bridge 模式,proxyAddress 需要填写宿主机的 IP;如果使用 host 模式,proxyAddress 可以填写 127.0.0.1。

总结

不要因为在很多地方设置过代理,就想当然地以为当前的访问也是经过代理的。每个软件设置代理的方式不尽相同,但是大体上可以归结为:

  1. 使用系统的环境变量
  2. 修改软件的配置文件
  3. 执行时注入参数

举一反三,像 aptgit 这类软件也是有其设置代理的方法。当你的代理稳定但是相应的访问失败时,大胆假设你的代理没有设置成功。要理清楚,当前的访问是谁发起的,才能正确地使用关键词搜索到正确的设置方式。

原本我在 docker 相关的使用中,有关代理的设置方式是通过修改配置文件,实现永久、全局的代理配置。但是在后续的使用中,发现代理在一些场景(比如使用 cloudflare tunnel)中会引起不易排查的问题,决定采用临时、局部的配置方式。

参考链接

Linux 让终端走代理的几种方法
Linux ❀ wget设置代理
配置Docker使用代理
Docker的三种网络代理配置
docker 设置代理,以及国内加速镜像设置

0%