Moralok's Blog

你在幼年时当快乐

本文介绍如何通过 Dockers Compose 安装 GrafanaPrometheus 在局域网中配合各类 exporter 为主机和诸多内部服务搭建监控。

阅读全文 »

在分布式应用中,并发访问资源需要谨慎考虑。比如读取和修改保存并不是一个原子操作,在并发时,就可能发生修改的结果被覆盖的问题。

很多人都了解在必要的时候需要使用分布式锁来限制程序的并发执行,但是在具体的细节上,往往并不正确。

基于 Redis 的分布式锁简单实现

本质上要实现的目标就是在 Redis 中占坑,告诉后来者资源已经被锁定,放弃或者稍后重试。Redis 原生支持 set if not exists 的语义。

1
2
3
4
5
6
7
> setnx lock:user1 true
OK

... do something

> del lock:user1
(integer) 1

死锁问题

问题一:异常引发死锁 1

如果在处理过程中,程序出现异常,将导致 del 指令没有执行成功。锁无法释放,其他线程将无法再获取锁。

改进一:设置超时时间

对 key 设置过期时间,如果在处理过程中,程序出现异常,导致 del 指令没有执行成功,设置的过期时间一到,key 将自动被删除,锁也就等于被释放了。

1
2
3
4
5
6
7
8
> setnx lock:user1 true
OK
> expire lock:user1 5

... do something

> del lock:user1
(integer) 1

问题二:异常引发死锁 2

事实上,上述措施并没有彻底解决问题。如果在设置 key 的超时时间之前,程序出现异常,一切仍旧会发生。

本质原因是 setnx 和 expire 两个指令不是一个原子操作。那么是否可以使用 Redis 的事务解决呢?不行。因为 expire 依赖于 setnx 的执行结果,如果 setnx 没有成功,expire 就不应该执行。

改进二:setnx + expire 的原子指令

如果 setnx 和 expire 可以用一个原子指令实现就好了。

基于原生指令的实现

在 Redis 2.8 版本中,Redis 的作者加入 set 指令扩展参数,允许 setnx 和 expire 组合成一个原子指令。

1
2
3
4
5
6
7
> set lock:user1 true ex 5 nx
OK

... do something

> del lock:user1
(integer) 1
基于 Lua 脚本的实现

除了使用原生的指令外,还可以使用 Lua 脚本,将多个 Redis 指令组合成一个原子指令。

1
2
3
4
5
6
if redis.call('setnx', KEYS[1], ARGV[1]) == 1 then
redis.call('expire', KEYS[1], ARGV[2])
return true
else
return false
end

超时问题

基于 Redis 的分布式锁还会面临超时问题。如果在加锁和释放之间的处理逻辑过于耗时,以至于超出了 key 的过期时间,锁将在处理结束前被释放,就可能发生问题。

问题一:其他线程提前进入临界区

如果第一个线程因为处理逻辑过于耗时导致在处理结束前锁已经被释放,其他线程将可以提前获得锁,临界区的代码将不能保证严格串行执行。

问题二:错误释放其他线程的锁

如果在第二个线程获得锁后,第一个线程刚好处理逻辑结束去释放锁,将导致第二个线程的锁提前被释放,引发连锁问题。

改进一:不要用于较长时间的任务

与其说是改进,不如说是注意事项。如果真的出现问题,造成的数据错误可能需要人工介入解决。

如果真的存在这样的业务场景,应考虑使用其他解决方案加以优化。

改进二:使用 watchdog 实现锁续期

为 Redis 的 key 设置过期时间,其实是为了解决死锁问题而做出的兜底措施。可以为获得的锁设置定时任务定期地为锁续期,以避免锁被提前释放。

1
2
3
4
5
6
7
private void scheduleRenewal() {
String value = lockValue.get();
ScheduledFuture<?> scheduledFuture = sScheduler.scheduleAtFixedRate(
() -> this.renewal(value), RENEWAL_INTERVAL, RENEWAL_INTERVAL, TimeUnit.MILLISECONDS
);
renewalTask.set(scheduledFuture);
}

但是这个方式仍然不能避免解锁失败时的其他线程的等待时间。

改进三:加锁时指定 tag

可以将 set 指令的 value 参数设置为一个随机数,释放锁时先匹配持有的 tag 是否和 value 一致,如果一致再删除 key,以此避免锁被其他线程错误释放。

基于原生指令的实现
1
2
3
4
tag = random.nextint()
if redis.set(key, tag, nx= True, ex=5):
do_something()
redis.delifequals(key, tag)

但是注意,Redis 并没有提供语义为 delete if equals 的原子指令,这样的话问题并不能被彻底解决。如果在第一个线程判断 tag 是否和 value 相等之后,第二个线程刚好获得了锁,然后第一个线程因为匹配成功执行删除 key 操作,仍然将导致第二个线程获得的锁被第一个线程错误释放。

基于 Lua 脚本的实现
1
2
3
4
5
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end

可重入性

可重入性是指线程在已经持有锁的情况下再次请求加锁,如果一个锁支持同一个线程多次加锁,那么就称这个锁是可重入的,类似 Java 的 ReentrantLock。

使用 ThreadLocal 实现锁计数

Redis 分布式锁如果要支持可重入,可以使用线程的 ThreadLocal 变量存储当前持有的锁计数。但是在多次获得锁后,过期时间并没有得到延长,后续获得锁后持有锁的时间其实比设置的时间更短。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);

public boolean tryLock() {
Integer count = lockCount.get();
if (count != null && count > 0) {
lockCount.set(count + 1);
return true;
}
String result = commands.set(lockKey, lockValue.get(), SetArgs.Builder.nx().px(RedisLockManager.LOCK_EXPIRE));
if ("OK".equals(result)) {
lockCount.set(1);
scheduleRenewal();
return true;
}
return false;
}

使用 Redis hash 实现锁计数

还可以使用 Redis 的 hash 数据结构实现锁计数,支持重新获取锁后重置过期时间。

1
2
3
4
5
6
7
8
9
10
if (redis.call('exists', KEYS[1]) == 0) then 
redis.call('hset', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
return redis.call('pttl', KEYS[1]);

书的作者不推荐使用可重入锁,他提出可重入锁会加重客户端的复杂度,如果在编写代码时注意在逻辑结构上进行调整,完全可以避免使用可重入锁。

代码实现

redis-lock

参考文章

  • 《Redis 深度历险,核心原理与应用实践》

演示字节码指令的执行

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

public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}

操作数栈和本地变量表的大小

在编译期间就可计算得到操作数栈和本地变量表的大小。

1
stack=2, locals=4, args_size=1

本地变量表

Slot,即槽位,可理解为索引。

1
2
3
4
5
Start  Length  Slot  Name   Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I

运行时常量池

1
2
3
#3 = Integer            32768
#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V

字节码指令

1
2
3
4
5
6
7
8
9
10
11
12
 0: bipush        10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
  • bipush,将一个 byte,推入操作数栈。
    • short 范围内的数是和字节码指令一起存储的,范围外的数是存储在运行时常量池中的。
    • 操作数栈的宽度是 4 个字节,short 范围内的数在推入操作数栈前会经过符号扩展成为 int。
  • istore_1,将栈顶的 int,存入局部变量表,槽位 1。
  • ldc,从运行时常量池中将指定常量推入操作数栈。
  • istore_2,将栈顶的 int,存入局部变量表,槽位 2。
  • iload_1 iload_2,依次从局部变量表将两个 int 推入操作数栈,槽位分别是 1 和 2。
  • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
  • istore_3,将栈顶的 int,存入局部变量表,槽位 3。
  • getstatic,获取类的静态属性,推入操作数栈。
  • iload_3,从局部变量表将 int 推入操作数栈,槽位 3。
  • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
  • return,返回 void

分析 a++ 和 ++a

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

public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
}

字节码指令

1
2
3
4
5
6
7
8
9
10
11
 0: bipush        10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
  • a++ 和 ++a 的区别是先 load 还是先 iinc。
  • iinc,将局部变量表指定槽位的数加上一个常数。
  • 注意 a 只 load 到操作数栈并没有 store 回局部变量表。
  • b = 10 + 12 + 12 = 34
  • a = 10 + 1 + 1 - 1 = 11

分析判断条件

1
2
3
4
5
6
7
8
9
10
11
12
public class ByteCodeTest_4 {

public static void main(String[] args) {
int a = 0;
// ifeq, goto
if (a == 0) {
a = 10;
} else {
a = 20;
}
}
}

字节码指令

1
2
3
4
5
6
7
8
9
10
 0: iconst_0
1: istore_1
2: iload_1
3: ifne 12
6: bipush 10
8: istore_1
9: goto 15
12: bipush 20
14: istore_1
15: return
  • iconst,将一个 int 常量推入操作数栈。
  • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
  • goto,总是进入的分支,跳转到指定行号。

涉及的字节码指令

  • bipush,将一个 byte 符号扩展为一个 int,推入操作数栈。
  • istore,将栈顶的 int,存入局部变量表的指定槽位。
  • iload,将局部变量表指定槽位的 int,推入操作数栈。
  • ldc,从运行时常量池将指定常量推入操作数栈。
  • iadd,将栈顶的两个 int 弹出并相加,将结果推入操作数栈。
  • getstatic,获取类的静态属性,推入操作数栈。
  • invokevirtual,将栈顶的参数依次弹出,调用实例方法。
  • return,返回 void。
  • iinc,将局部变量表中指定槽位的数加一个常量。
  • if<cond>,一个 int 和 0 的比较成立时进入分支,跳转到指定行号。
    • ifeq,equals
    • ifne,not equals
    • iflt,less than
    • ifge,greater than or equals
    • ifgt,great than
    • ifle,less than or equals
  • goto,总是进入的分支,跳转到指定行号。

每一次重新阅读,都有新的收获,也将过去这段时间以来一些新的零散的知识点串联在一起。
沿着周志明老师的行文脉络,了解问题发生的背景,当时的人如何思考,提出了哪些方案,各自有什么优缺点,附带产生的问题如何解决,理论研究如何应用到工程实践中,就像真实地经历一段研发历史。这让人对垃圾收集的认识不再停留在记忆上,而是深入到理解中,相关的知识点不再是空中楼阁,无根之水,而是从一些事实基础和问题自然延申出来。
尽管在更底层的实现上仍然缺乏认识和想象力,以至于在一些细节上还是疑惑重重,但是仍然有豁然开朗的感觉呢。比如以前看不同垃圾收集器的过程示意图如鸡肋,如今看其中的停顿和并发,只觉充满智慧。

概述

垃圾收集(Garbage Collection,简称 GC)需要考虑什么?

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

为什么要去了解垃圾收集和内存分配?
当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们就需要对这些“自动化技术”实施必要的监控和调节。

在 Java 中,垃圾收集需要关注哪些内存区域?
程序计数器、虚拟机栈和本地方法栈,随线程而生,随线程而灭,栈中的栈帧随着方法的进入和退出有条不紊地执行着入栈和出栈操作,每个栈帧中分配多少内存可以认为是编译期可知的,因此这几个区域地内存分配和回收具备确定性。
但是 Java 堆和方法区这两个区域则有显著的不确定性,只有运行期间,我们才知道程序会创建哪些对象,创建多少个对象,这部分内存的分配和回收是动态的。

哪些内存需要回收?

哪些对象是还存活着,哪些已经死亡?

对象死亡即不可能再被任何途径使用。其实曾经的我会怀疑,遗落在内存中的对象,真的没有办法“魔法般地”获取其引用地址吗?引用变量的值不就是 64 位的数字吗?

引用计数算法(Reference Counting)

优点:

  • 原理简单
  • 判定效率高

缺点:

  • 例外情况多,需要额外处理(比如循环引用)

提及引用计数算法,人们好像认定它无法应对循环引用因而被抛弃。虽说 Java 虚拟机中没有选用它,但是在其他计算机领域有所运用。循环引用也并非它绕不过去的难题,事实上,跨代引用问题中,老年代引用新生代形成的引用链不是也可能是一个尚未回收的孤岛吗?

可达性分析算法(Reachability Analysis)

  • 选取一系列称为“GC Roots”的根对象作为起始节点集。
  • 根据引用关系向下搜索。
  • 如果某个对象到 GC Roots 间没有任何引用链相连,即该对象不可能再被使用。用图论的话说,就是 GC Roots 到该对象不可达。

那么可作为 GC Roots 的对象有哪些呢?
固定的 GC Roots,主要是在全局性引用和执行上下文中:

  1. 在虚拟机栈(栈帧中的本地变量表)中引用的对象,比如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。
  2. 在方法区中类静态属性引用的变量。
  3. 在方法区中类常量引用的对象,比如字符串常量池(String Table)里的引用。
  4. 在本地方法栈中 JNI,即 Native 方法引用的对象。
  5. Java 虚拟机内部的引用,如基本类型的 Class 对象,常驻的异常类型,还有系统类加载器。
  6. 所有被同步锁(synchronized)持有的对象
  7. 反映 Java 虚拟机内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等(不懂)。

临时性的GC Roots:
除了固定的 GC Roots 集合外,根据用户所选用的垃圾收集器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入。

比如,当针对新生代发起垃圾收集时,如果老年代对象引用了它,那么被引用的对象就不应该被回收,尽管老年代对象可能已经不可达。为此,老年代对象需要临时性加入 GC Roots 集合。
当然,为了避免将所有老年代对象加入 GC Roots 集合这样一看就很不合理的操作,会做一些优化处理。

“引用”的概念扩充

对于判断对象是否存活而言,“引用”的重要性不言而喻。但是如果对象只有“被引用”和“未被引用”两种状态,对于描述一些“内存足够就保留,内存不足就抛弃”的对象就显得无能为力。
缓存系统就是这样的一个典型应用场景。当内存充足时,就保留作为缓存;当内存不足时,就抛弃腾出空间给其他资源。

曾经有一位热衷实践技术的同事就和我介绍了他在项目中使用弱引用实现的缓存模块,当时我还不太理解他为何这样做。事实上,享受自动垃圾收集的我并不能在一开始就敏锐地把握到对象在应用程序中的创建、存活和消亡过程。
当然我们并不推荐自己实现基于 JVM 的缓存系统,事实上他之所以提及,正是因为出了 bug。

引用的分类

  • 强引用(Strongly Reference),只要强引用还在,绝不会回收。
  • 软引用(Soft Reference),只被软引用关联的对象,在系统发生 OOM 前,会被列入回收范围进行第二次回收。
  • 弱引用(Weak Reference),只被弱引用关联的对象,只能生存到下一次垃圾收集发生为止,无论内存是否 足够,都会回收。
  • 虚引用(Phantom Reference),一个对象是否有虚引用,不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。设置虚引用的唯一目的就是为了在对象被回收时收到一个系统通知。

虚引用的一个经典应用是是 ByteBuffer 对象被回收时自动释放直接内存。

弱引用的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ReferenceTest_3 {
private static final int _4MB = 4 * 1024 * 1024;

// -Xmx20m -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) throws IOException {
// list -> WeakReference -> byte[]
List<WeakReference<byte[]>> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
list.add(ref);
System.out.print(list.size() + " ");
for (WeakReference<byte[]> w : list) {

System.out.print(w.get() + " ");
}
System.out.println();
}
System.out.println("循环结束: " + list.size());
System.in.read();
}
}

在测试中,minor GC 并没有回收掉全部的只被弱引用关联的对象,full GC 才全部回收掉,我一度以为关于弱引用的表述不正确。后来进一步测试发现,是因为部分对象直接分配在老年代。因此更准确的表述是,每一次 GC 都会回收所在发生区域里只被弱引用关联的对象。
这是一个有趣的经验,让我对部分垃圾收集中的“部分”二字有更深刻的体会,原来非收集区域的对象真的对发生在其他区域的垃圾收集无感。

了解为什么扩充引用的概念,让人对引用的分类豁然开朗。我的脑海里情不自禁冒出了不太恰当的比喻:一个城市里的公民被区分了等级,一等公民(强)永远不会被强行驱逐;二等公民(软)在城市资源紧张时会被强行驱逐;三等公民(弱)被认为影响市容市貌,一旦有整顿就会被强行驱逐;一等公民里有一些需要被监视,一旦离开,会触发一个事件。

finalize 方法

有趣的知识点,无趣的面试考点。

方法区的垃圾回收是什么样的?

  • 《Java虚拟机规范》中提到可以不要求虚拟机在方法区实现垃圾收集
  • 确实有未实现或未完整实现方法区类型卸载的收集器
  • 原因是方法区垃圾收集的性价比通常比较低

方法区的垃圾收集主要回收两部分:

  • 废弃的常量
  • 不再使用的类型

如何判定一个常量是否废弃?
没有任何字符串对象引用常量池中的“java”常量,且虚拟机中也没有其他地方引用这个字面量。
如果这时发生垃圾回收,而且垃圾收集器判断确实有必要,才会将“java”常量清理出常量池。

“虚拟机中也没有其他地方引用这个字面量”怎么理解?

如何判定一个类型是否可卸载?

  • 该类的所有的实例都已经被回收
  • 加载该类的类加载器已经被回收
  • 该类对应的 Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

Java虚拟机被允许对满足上述三个条件的无用类进行回收,这里说的仅仅是“被允许”,而并不是
和对象一样,没有引用了就必然会回收。关于是否要对类型进行回收,HotSpot 虚拟机提供了 -Xnoclassgc 参数进行控制,还可以使用 -verbose:class 以及 -XX:+TraceClassLoading、-XX:+TraceClassUnLoading 查看类加载和卸载信息。

条件二如此苛刻,系统类加载器不会被回收,是否意味着正常的应用程序,类一旦加载就不会卸载?
“无法在任何地方通过反射访问该类的方法”是否多余,Method 对象不是引用了 Class 对象吗?
Class 对象没有被引用时,会被回收吗?
卸载类是指回收 Class 对象加上清理方法区中的类的信息(怎么样的存储结构呢)吗?

如何回收:垃圾收集算法

分类:

  • 引用计数式垃圾收集(Reference Counting GC)
  • 追踪式垃圾收集(Tracing GC)

分代收集理论

分代收集名为理论,实质是一套符合大多数程序运行实际情况的经验法则,它建立在两个分代假说之上:

  1. 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
  2. 强分代假说(String Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。

这两个分代假说共同奠定了多款常用垃圾收集器的一致的设计原则:收集器应该将 Java 堆划分出不同的区域,然后将回收对象依据其年龄分配到不同的区域之中存储。
正因为有了区域划分,垃圾收集器才可以每次只回收一个或某些部分的区域,因而才有了“Minor GC”、“Major GC”和“Full GC”等回收类型划分;针对不同区域安排与里面存储对象存亡特征相匹配的垃圾收集算法,因而才发展出“标记-复制”、“标记-清除”和“标记-整理”等垃圾收集算法。

了解分代收集理论,分代收集算法更显得有理有据。

一般把 Java 堆划分为新生代(Young Generation)和老年代(Old Generation)两个区域。

针对不同分代的垃圾收集分类:

  • 部分收集(Partial GC):目标不是完整收集整个堆、
    • 新生代收集(Minor GC/Young GC)
    • 老年代收集(Major GC/Old GC):只有 CMS。Major GC有些混淆,应按上下文注意是老年代收集还是整堆收集。
    • 混合收集(Mixed GC):只有 G1
  • 整堆收集(Full GC)

区域划分引起另一个问题,跨代引用。这个问题在前文的 GC Roots 选择时也提到过。
根据前两条假说可逻辑推理得出隐含推论:存在相互引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。

  1. 跨代引用假说(Integenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。

依据这条假说,我们不应再为少量的跨代引用而去扫描整个老年代。那么怎么处理跨代引用呢?在后面 HotSpot 的实现细节中我们再提。

标记-清除算法(Mark Sweep)

在 1960 年由 Lisp 之父 John McCarthy 提出,分为标记和清除两个阶段。
缺点:

  • 执行效率不稳定,对象越多,且需要回收的对象越多,效率会降低。
  • 内存空间碎片化,长时间后没有足够的连续内存分配大对象。

标记-复制算法

为解决标记-清除算法面对大量可回收对象时执行效率低的问题,1969 年 Fenichel 提出“半区复制”(Semispace Copying)。将内存划分为大小相等的两块,每次只使用一块,用完的时候,复制存活的对象到另一块。

优点:

  • 存活对象少时,仅需复制少量对象。
  • 不存在内存空间碎片

缺点:

  • 空间浪费

适用场景:

  • 新生代。

怎么减少空间的浪费呢?
IBM 公司研究表明新生代中 98% 的对象熬不过第一轮收集。在 1989 年,Andrew Appel 针对具备“朝生夕灭”特点的对象,提出了一种更优化的半区复制分代策略,现在称为“Appel 式回收”。具体做法是把新生代分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次分配内存只使用 Eden 和其中一块 Survivor。
HotSpot 虚拟机默认 Eden:Suvivor 为 8:1。

在测试中,尽管 survivor 有空间,仍然只在 Eden 空间进行分配。

如果 Survivor 空间不足以容纳一次 Minor GC 之后存活的对象时怎么办?
罕见情况的“逃生门”——使用其他区域(通常是老年代)进行分配担保(Handle Promotion)。

标记-整理算法(Mark Compact)

标记-复制算法面对对象存活率较高的情况,效率会降低;更关键的是,它需要额外空间进行分配担保。
针对老年代对象的存亡特征,1974 年 Edward Lueders 提出了另外一种有针对性的“标记-整理”算法,让所有存活对象都向内存空间一端移动。

移动和复制的开销有什么差距吗?撇开分配担保问题,大量复制和大量移动是类似的把?

移动对象的弊端

  • 大量对象存活的话需要大量移动对象,负担重。在以前,对象移动操作必须全程暂停用户应用程序才能进行(STW,Stop The World)。
  • 不整理内存碎片的话,需要依赖更复杂的内存分配器和内存访问器解决。内存访问最频繁,反而影响吞吐量。

不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算。在这点上的区别分别演变出两种发展方向,低延迟和高吞吐量。

书中提到的内存分配器和内存访问器解决的是在碎片化的内存空间中进行内存分配,还是可以将大对象分散地存储到碎片化的空间中呢?还提到硬盘存储大文件不要求物理连续的磁盘空间以及内存访问环节的额外负担,应该是指一种分散存储的实现方案吧?

对于 CMS 的“和稀泥”解决方案,暂时容忍内存碎片,直到影响对象分配时再采用标记-整理算法收集一次,如果抛开备用的 Serial Old 单线程的效率问题不谈,除非在碎片化的内存空间中分配和访问内存在效率上低于在整理后的内存空间中,要不然“和稀泥”这种懒惰式的处理方案理论上效率更高吧?

HotSpot 的算法细节实现

根节点枚举

对象浩如烟海,怎么实现高效查找根节点的呢?

迄今为止,所有收集器在根节点枚举这一步骤时都是必须暂停用户线程的。这是因为根节点枚举如果不在一个一致性快照中进行,准确性无法保证。

Exact VM 类型的虚拟机使用的是准确式垃圾回收,当用户线程停顿后,不需要一个不漏地检查完所有执行上下文和全局的引用位置,就有办法直接得到哪些地方存放着对象引用。HotSpot 的具体解决方案是使用一组称为 OopMap(Ordinary Object Pointer Map) 的数据结构。

  • 一旦类加载动作完成,HotSpot 会把对象内什么偏移量上是什么类型的数据计算出来。
  • 在即时编译过程中,也会在特定位置记录下栈和寄存器里什么位置是引用。

看书时,在这个地方其实有很多困惑。对于 OopMap,就像知道了一个不太懂的东西,了解了它能做什么,最明确的是,在即时编译中,会在安全点位置生成 OopMap。

如何识别数据类型

首先,我困惑的是,非准确式垃圾回收是什么东西,要找到对象引用得怎么做?
书中提到,准确式内存管理是指虚拟机可以知道内存中某个位置的数据具体是什么类型。比如内存中有一个 32 位的整数 123456,虚拟机将有能力分辨出它到底是一个指向了 123456 的内存地址的引用类型还是一个数值为 123456 的整数。
Exact VM 抛弃掉以前 Classic VM 基于句柄(Handle)的对象查找方式,因为在垃圾收集后对象可能被移动,如果地址改变(123456->654321),在没有明确信息表明内存中哪些数值式引用类型的情况下,虚拟机肯定不能把所有123456的值改为654321,所以要使用句柄来保持引用值得稳定。

看到这个举例之后容易理解多了,可能是没有手动管理内存的经验,对这方面体会不深刻。

以栈为例,栈帧里装有 int、double 等类型的数值,也有引用类型变量的地址,这些值在保守式 GC 看无法直接分辨是数值还是引用。但保守式 GC 其实还是有一定的分辨能力:

  1. 是不是正确的对齐值(可以理解为是一个正确的指针的值)
  2. 是否指向堆内的地址
  3. 是否指向对象的头(从这点上看,虚拟机是会尝试获取对象头校验一下吗?)

但是根据这些规则进行检测还是不够的,比如某一个 int 的值刚好指向某个对象的起始地址,就恰好满足上述的所有条件,这样就可能造成该对象被错误识别为存活对象,这个现象称为对堆的压迫。
保守式 GC 的“保守”二字体现在它将可疑的根看作是指针进行保守地处理。

死亡的对象被错误保留下来,挤占了堆空间,称其为“压迫”还听形象的。这个“保守”在理解以后才不感觉违和。

OopMap 的类型

  • 类型信息里记录了自己的 OopMap,这应该对应的是“一旦类加载动作完成,HotSpot 会把对象内什么偏移量上是什么类型的数据计算出来”,从对象向它引用的对象查找时使用。
  • 被 JIT 编译后的指令流,也会在特定的位置(安全点)记录下 OopMap。
  • 奇怪的是,暂时没有找到明确指出在 Java 字节码的特定位置(安全点)也记录着 OopMap。

方法区的类静态属性和类常量也是根据类型信息里的 OopMap 查找的吗?

安全点

为什么引入安全点,它解决了什么问题?

在 OopMap 的协助下,HotSpot 可以快速准确地完成 GC Roots。但是非常多的指令可能导致 OopMap 的内容发生变化。

书中“可能导致引用关系变化……的指令非常多”这句话让人困惑,OopMap 不是为了定位哪里是引用而非引用指向哪吧?如果只看“导致 OopMap 内容变化的指令非常多”更易理解。

在哪里生成 OopMap 呢?
既然指令会导致 OopMap 内容发生变化,最简单粗暴的就是为每一条指令生成 OopMap,但是这样会空间成本会急剧上升。
HotSpot 只在特定的位置生成 OopMap,这些位置被称为“安全点”(Safepoint)。
安全点的设定决定了并非在代码指令流的任意位置都能够停顿下来开始垃圾收集,而是要求必须到达安全点才能够暂停。

安全点怎么选择呢?

  • 不能太少以至于让收集器等太久
  • 不能太频繁以至于过分增大运行时的内存负担

安全点的位置的选取基本上是以“是否具有让程序长时间执行的特征”为标准进行选定的:

  • 所有的非计数循环的末尾
  • 方法返回之前/调用方法的 call 指令后
  • 每条 Java 编译后的字节码的边界(不理解)
  • 可能抛异常的地方

有没有权威的标准描述啊。

如何在垃圾收集发生时,让所有线程(不包括 JNI 调用的线程)都跑到最近的安全点,然后停顿下来呢?

  • 抢先式中断(Preemptive Suspension),系统先中断全部用户线程,如果有用户线程中断的地方不是安全点,就恢复该线程执行,再中断,直到跑到安全点。(没人选)
  • 主动式中断(Voluntary Suspenstion),设置一个标志位,线程执行时不停主动轮询,发现中断标志为真就在最近的安全点主动挂起。

这个问题和如何通知多线程一起做一件事很像,设置标志位,再让线程运行过程中轮询。

什么时候进行轮询?
轮询标志的地方与安全点是重合的,另外还要加上创建对象和其他需要在堆上分配内存的地方,这样可以检查是否即将发生垃圾收集,避免没有足够内存分配给新对象。

仔细一想,与安全点重合似乎是理所当然,额外选取的地方也有恰当的理由。

轮询在印象里都意味着额外开销,虚拟机如何应对?
HotSpot 使用内存保护陷阱的方式,把轮询操作精简至只有一条汇编指令的程度。

很遗憾,看懂又不懂,总之,轮询操作优化得很高效。

安全区域

为什么引入安全区域,它解决了什么问题?

安全点机制保证了程序执行时,在不太长的时间内,就会遇到可进入垃圾收集过程的安全点,但是如果程序“不执行”的时候,比如线程处于 Sleep 或者 Blocked 时,就无法通过轮询获知中断标识。对于这种情况,引入了安全区域(Safe Region)来解决。

安全区域是指能够确保在某一段代码片段之中,引用关系不会发生变化。安全区域可以看作是扩展拉伸了的安全点。

  1. 当用户线程执行到安全区域里的代码,首先会标识自己进入了安全区域
  2. 虚拟机发起垃圾回收时,不去管这些写线程
  3. 当线程要离开安全区域,会检查虚拟机是否已完成根节点枚举(或者其他需要暂定用户线程的阶段),看起来是否处于用户线程暂停是可以检测的
  4. 如果已完成,继续;如果未完成,等待至收到可以离开安全区域的信号

不去管处于安全区域的线程的意思是什么,不是线程主动轮询是否中断的标志吗?这个标志不是全局共享的吗,是虚拟机为各个线程单独设置的?

记忆集和卡表

为什么引入记忆集,它解决了什么问题?

为了解决对象跨代引用所带来的问题,比如在标记新生代时,避免将整个老年代的对象都加入 GC Roots 进行扫描。

这个问题起初让我很迷惑,直到我认识到“老年代里有一些已经不可达但还没回收的对象,它们可能引用了新生代里的对象”这个现象,我在想我可能理解了跨代问题。但是此时,我又迟疑了,如果引用新生代对象的老年代对象已经不可达,回收掉就好了,如果老年代对象可达,那么从固定的 GC Roots 一定可以找到老年代对象,再找到新生代对象。

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的抽象数据结构。
如果不考虑效率和成本,最简单的方法可以用非收集区域中所有含跨代引用的对象数组来实现。

设计者通常选择更为粗犷的记录粒度来节省记忆集的存储和维护成本,比如:

  • 字长精度:每个记录精确到一个机器字长,该字包含跨代指针。
  • 对象精度:每个记录精确到一个对象,该对象里包含跨代指针。
  • 卡精度:每个记录精确到一块内存区域,该区域内有对象含有跨代指针。

卡表(Card Table)是记忆集的一种具体实现。
卡表最简单的形式可以只是一个字节数组,HotSpot 虚拟机也是这样做的。

1
CARD_TABLE[this address >> 9] = 0;

一个卡页的内存中通常包含不止一个对象,只要卡页内有一个或更多对象的字段存在跨代引用指针,就将卡表的数组元素的值标识为 1,成该元素变脏(Dirty)。

这是否以为着卡表长度为 (老年代容量 / 512),这内存占用还是不低欸。

区域内怎么判断出对象的开始和结束位置呢?虚拟机可以通过一个地址,知道该地址是一个对象的起始地址吗?

写屏障

卡表元素如何维护?它们何时变脏,谁来把它们变脏?

卡表元素何时变脏的答案是明确的——有其他分代区域中对象引用本区域对象时,其对应的卡表元素就应该变脏。

问题是如何在对象赋值的那一刻去更新维护卡表呢?
在解释执行字节码的情况中,虚拟机有充分的介入空间;但是在编译执行的场景中,即时编译后的代码已经是纯粹的机器指令流了。
HotSpot 虚拟机是通过写屏障(Write Barrier)技术维护卡表状态,在机器码层面介入每一个赋值操作。

又是一个看懂又不懂的事情,很多资料里尝试用伪代码表达,但是我很想知道它是怎么实现在机器码层面介入每一个赋值操作,是在 JVM 源码中用 C++ 实现的吗?是在解释器和 JIT 编译器共同支持下,在机器码层面加入了代码片段吗?
总之,理解为引用字段赋值的 AOP 切面吧。

  • 写前屏障(Pre-Write Barrier),直到 G1 收集器才使用到。
  • 写后屏障(Post-Write Barrier)。
1
2
3
4
5
6
void oop_field_store(oop* field, oop new_value) {
// 引用字段赋值操作
*field = new_value;
// 写后屏障,在这里完成卡表状态更新
post_write_barrier(field, new_value);
}

这应该只是为了表达思路的伪代码吧?

应用写屏障后,每次只要对引用更新,都会产生额外的开销,但是和 Minor GC 时扫描整个老年代的代价相比还是低很多的。

除了写屏障的开销外,卡表在高并发场景下还面临着“伪共享”(False Sharing)问题。

现代中央处理器的缓存系统中是以缓存行(Cache Line)为单位存储的,当多线程修改互相独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响而导致性能降低。
假设 64 个卡表元素共享一个缓存行,对应的卡页总内存为 32 KB,如果不同线程更新的对象都在这 32 KB 的内存区域内,就会导致更新卡表时发生伪共享问题。
一种简单的解决方案是不采用无条件的写屏障,而是先检查卡表标记,只有未被标记才标记为变脏。

在 JDK 7 后,新增 -XX:+UseCondCardMark 参数决定是否开启卡表更新的条件判断。开启后增加一次额外的判断开销,但能够避免伪共享问题。

并发的可达性分析

从 GC Roots 再继续向下遍历对象图,停顿时间必然与堆容量成正比。如果能够削减这部分的停顿时间,对于所有使用追踪式垃圾收集算法的收集器而言,“标记”阶段都能收益匪浅。

为什么必须在一个能保障一致性的快照上才能进行对象图的遍历?
三色标记(Tri-color Marking),扫描过程就想灰色波峰从黑向白推进。
白色:表示对象尚未被垃圾收集器访问到。
黑色:表示对象已经被垃圾收集器访问到,且这个对象的所有引用都已经扫描过了。
灰色:表示对象已经被垃圾收集器访问到,但这个对象上至少有一个引用还没有被扫描过。

如果用户线程是冻结的,那么不会有问题。如果用户线程在收集过程中修改了引用关系,就会出现两种后果:

  • 把原本消亡的对象,错误标记为存活。(尚可以容忍,只是产生了浮动垃圾,下次收集即可)
  • 把原本存活的对象,错误标记为死亡。这是不允许发生的。

Wilson 于 1994 年在理论上证明,当且仅当以下两个条件同时满足,会产生“对象消失”的问题:

  • 赋值器插入了一条或多条从黑色对象到白色对象的新引用。
  • 赋值器删除了全部从灰色到白色对象的直接或间接引用。

第二个条件的描述需要很精确,不能忽略关键字眼。

要解决并发扫描时的对象消亡问题,只需要破坏这两个条件的任意一个即可。由此分别产生了两种解决方案:

  • 增量更新(Incremental Update),当黑色对象插入新的指向白色对象的引用时,记录,在并发扫描结束后,以这些黑色对象为根,重新扫描。
  • 原始快照(Snapshot At The Beginning,SATB),当灰色对象要删除指向白色对象的引用关系时,记录,在并发扫描结束后,以这些灰色对象为根,重新扫描。

和破坏死锁的思路类似。

不太理解“以这些灰色对象为根,重新扫描”,反而是“按照扫描的那一刻的对象图进行扫描”更容易理解。
应该是以这些灰色对象为根,根据记录的被删除引用,继续扫描。把继续扫描到的对象,统统视为存活对象。
理解起来有点绕,因为第一次扫描是沿着引用链进行,删除引用后,确实没办法继续往下。但是引用关系的变化通过写屏障记录了下来。第二次扫描需要 STW。
如果没有理解错的话,这时候会产生一种错误将白标记为黑的浮动垃圾;第一种方案不会出现该情况;但两种方案都不能避免已标记为黑的对象在并发时成为浮动垃圾。

理解了这部分,再看垃圾收集器的过程示意图中的停顿和并发,不再枯燥反觉有趣了。

其实我不太理解

参考文章

  1. 《深入理解Java虚拟机》
  2. JVM中的OopMap
  3. 图解 OopMap、Safe Point、Safe Region
  4. 详解GC(一)理论篇

内存区域

JVM 内存区域划分为:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
  • 方法区

程序计数器

虚拟机栈

Java 虚拟机栈(Java Virtual Machine Stack),线程私有,生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的线程内存模型。每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口等信息。

可以使用 -Xss1024k 设置虚拟机栈的大小。默认情况下都是 1024k,只有 Windows 中取决于虚拟内存。

栈内存溢出

  1. 栈帧过多导致栈内存溢出
  2. 栈帧过大导致栈内存溢出(难复现)

不正确的递归调用

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

private static int count = 0;

// 改变栈的大小限制 -Xss256k,观察调用次数的变化
public static void main(String[] args) {
try {
method1();
} catch (Throwable t) {
t.printStackTrace();
} finally {
// 默认情况下经过 20000+ 次,改变参数后 3000+ 次
System.out.println(count);
}
}

private static void method1() {
count++;
method1();
}
}

循环引用导致 JSON 解析无限循环

并非只有自己写的递归方法可能引发栈内存溢出,有可能第三方库也会引发栈内存溢出。

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

public static void main(String[] args) throws JsonProcessingException {
Department department = new Department();
department.setName("Tech");

Employee employee1 = new Employee();
employee1.setName("Tom");
employee1.setDepartment(department);

Employee employee2 = new Employee();
employee2.setName("Tim");
employee2.setDepartment(department);

department.setEmployees(Arrays.asList(employee1, employee2));

ObjectMapper objectMapper = new ObjectMapper();
System.out.println(objectMapper.writeValueAsString(department));
}

static class Department {
private String name;
private List<Employee> employees;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public List<Employee> getEmployees() {
return employees;
}

public void setEmployees(List<Employee> employees) {
this.employees = employees;
}
}

static class Employee {
private String name;
private Department department;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Department getDepartment() {
return department;
}

public void setDepartment(Department department) {
this.department = department;
}
}
}

局部变量的线程安全问题

  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
28
29
30
31
32
public class StackTest_3 {

public static void main(String[] args) {
method1();
}

// 线程安全
private static void method1() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb);
}

// 线程不安全
private static void method2(StringBuilder sb) {
sb.append(1);
sb.append(2);
sb.append(3);
System.out.println(sb);
}

// 线程不安全,看到一个说法:发生指令重排,sb 的 append 操作发生在返回之后(有待确认)
private static StringBuilder method3() {
StringBuilder sb = new StringBuilder();
sb.append(1);
sb.append(2);
sb.append(3);
return sb;
}
}

线程问题排查

CPU 占用率居高不下

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 ThreadTest_1 {

public static void main(String[] args) {
new Thread(null, () -> {
System.out.println("t1...");
while (true) {

}
}, "thread1").start();

new Thread(null, () -> {
System.out.println("t2...");
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread2").start();

new Thread(null, () -> {
System.out.println("t3...");
try {
TimeUnit.SECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "thread3").start();
}
}

当发现 CPU 占用率居高不下时,可以尝试以下步骤:

  1. top,定位 cpu 占用高的进程 id。
  2. ps H -eo pid,tid,%cpu | grep pid,进一步定位引起 cpu 占用高的线程 id。
  3. jstack pid,根据线程 id 换算成 16进制的 nid 找到对应线程,进一步定位到问题的源码行号。
1
2
3
4
5
"thread1" #8 prio=5 os_prio=0 tid=0x00007f9bd0162800 nid=0x1061ad runnable [0x00007f9bd56eb000]
java.lang.Thread.State: RUNNABLE
at com.moralok.jvm.thread.ThreadTest_1.lambda$main$0(ThreadTest_1.java:10)
at com.moralok.jvm.thread.ThreadTest_1$$Lambda$1/250421012.run(Unknown Source)
at java.lang.Thread.run(Thread.java:750)

死锁,迟迟未返回结果

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

private static final Object A = new Object();
private static final Object B = new Object();

public static void main(String[] args) {
new Thread(null, () -> {
System.out.println("t1...");
synchronized (A) {
System.out.println(Thread.currentThread().getName() + " get A");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (B) {
System.out.println(Thread.currentThread().getName() + " get B");
}
}
}, "thread1").start();

new Thread(null, () -> {
System.out.println("t2...");
synchronized (B) {
System.out.println(Thread.currentThread().getName() + " get B");
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
synchronized (A) {
System.out.println(Thread.currentThread().getName() + " get A");
}
}
}, "thread2").start();
}
}
  1. jstack pid,会显示找到死锁,以及死锁涉及的线程,,并各自持有的锁还有等待的锁。
  2. 其他工具如 jconsole 也具有检测死锁的功能。

本地方法栈

堆(Heap)的特点:

  1. 线程共享,需要考虑线程安全问题。
  2. 存在垃圾回收机制。
  3. 使用 -Xmx8m 设置大小。

堆内存溢出

既然堆有垃圾回收机制,为什么还会发生内存溢出呢?最开始的时候,我也有这样的困惑。
后来我才认识到,还在使用中的对象是不能被强制回收的,不再使用的对象不是立刻回收的。当创建对象却没有足够的内存空间时,如果清理掉那些不再使用的对象就有足够的内存空间,就不会发生内存溢出,程序只是表现为卡顿。

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

// -Xmx8m
// 不设置可能不提示 Java heap space,出错地方不同,报错信息不同
public static void main(String[] args) {
int i = 0;
try {
List<String> list = new ArrayList<>();
String s = "hello";
while (true) {
list.add(s);
s = s + s;
i++;
}
} catch (Throwable t) {
t.printStackTrace();
} finally {
System.out.println("运行次数 " + i);
}
}
}
1
2
3
4
5
6
7
java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
at java.lang.StringBuilder.append(StringBuilder.java:141)
at com.moralok.jvm.memory.heap.HeapTest_1.main(HeapTest_1.java:21)
运行次数 17

堆内存溢出的发生往往需要长时间的运行,因此在排查相关问题时,可以适当调小堆内存。

监测堆内存

  1. 使用 jps 查看 Java 进程列表
  2. 使用 jmap -heap pid 查看堆内存信息
  3. 还可以使用 jconsole 观察堆内存变化曲线
  4. 还可以使用 VisualVM 查看堆信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HeapTest_2 {

public static void main(String[] args) throws InterruptedException {
System.out.println("1...");
TimeUnit.SECONDS.sleep(30);
// 堆空间占用上升 10MB
byte[] bytes = new byte[1024 * 1024 * 10];
System.out.println("2...");
TimeUnit.SECONDS.sleep(30);
bytes = null;
// 堆空间占用下降
System.gc();
System.out.println("3...");
TimeUnit.SECONDS.sleep(3000);
}
}

使用 jmap -heap pid 查看堆内存信息:

1
2
3
4
5
6
7
Eden Space:
capacity = 268435456 (256.0MB)
used = 32212360 (30.72010040283203MB)

used = 42698136 (40.720115661621094MB)

used = 5368728 (5.120018005371094MB)

使用 jconsole 查看堆内存信息:

堆内存占用居高不下

当你发现堆内存占用居高不下,经过 GC,下降也不明显,如果你想查看一下堆内的具体情况,可以将其 dump 查看。

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

// jps 查进程,jmap 看堆内存,jconsole 执行GC,堆内存占用没有明显下降
// 使用 VisualVM 的堆 dump 功能,观察大对象
public static void main(String[] args) throws IOException {
List<Student> students = new ArrayList<>();
for (int i = 0; i < 200; i++) {
students.add(new Student());
}
System.in.read();
}

static class Student {
private byte[] score = new byte[1024 * 1024];
}
}

可使用 VisualVM 的 Heap Dump 功能:

也可使用 jmap -dump:format=b,file=filename.hprof pid,需要其他分析工具搭配。

方法区

根据《Java虚拟机规范》,方法区在逻辑上是堆的一部分,但是在具体实现上,各个虚拟机厂商并不相同。对于 Hotspot 而言:

  • JDK 8 之前,方法区的具体实现为永久代,使用堆内存,使用 -XX:MaxPermSize=10m 设置大小。
  • JDK 8 开始,方法区的具体实现为元空间,使用直接内存,使用 -XX:MaxMetaspaceSize=10m 设置大小。

方法区溢出

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
public class MethodAreaTest_1 extends ClassLoader {

// -XX:MaxMetaspaceSize=8m MaxMetaspaceSize is too small.
// -XX:MaxMetaspaceSize=10m java.lang.OutOfMemoryError: Compressed class space
// 不是 Metaspace 应该是某个参数设置的问题
// JDK 6: -XX:MaxPermSize=8m PermGen space
public static void main(String[] args) {
int j = 0;
try {
MethodAreaTest_1 methodAreaTest1 = new MethodAreaTest_1();
for (int i = 0; i < 20000; i++, j++) {
ClassWriter classWriter = new ClassWriter(0);
// 版本号,public,类名,包名,父类,接口
classWriter.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, "Class" + i, null, "java/lang/Object", null);
// 返回二进制字节码
byte[] code = classWriter.toByteArray();
// 加载类
methodAreaTest1.defineClass("Class" + i, code, 0, code.length);
}
} catch (ClassFormatError e) {
e.printStackTrace();
} finally {
System.out.println("次数 " + j);
}
}
}
  1. 当设置的值太小时 -XX:MaxMetaspaceSize=8m,提示 MaxMetaspaceSize is too small。
  2. 实验中抛出 java.lang.OutOfMemoryError: Compressed class space。
  3. 添加参数 -XX:-UseCompressedClassPointers 后,抛出 java.lang.OutOfMemoryError: Metaspace。
  4. JDK 6 设置 -XX:MaxPermSize=8m,抛出 java.lang.OutOfMemoryError: PermGen space。

不要认为自己不会写动态生成字节码相关的代码就忽略这方面的问题,如今很多框架使用字节码技术大量地动态生成类。

运行时常量池

二进制字节码文件主要包含三类信息:

  1. 类的基本信息
  2. 类的常量池(Constant Pool)
  3. 类的方法信息

使用 javap 反编译

1
2
3
4
5
6
public class MethodAreaTest_2 {  

public static void main(String[] args) {
System.out.println("hello world");
}
}
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
Classfile /C:/Users/username/Documents/github/jvm-study/target/classes/com/moralok/jvm/memory/methodarea/MethodAreaTest_2.class
Last modified 2023-11-4; size 619 bytes
MD5 checksum 0ed10a8f0a03be54fd4159958ee7446c
Compiled from "MethodAreaTest_2.java"
public class com.moralok.jvm.memory.methodarea.MethodAreaTest_2
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #23 // hello world
#4 = Methodref #24.#25 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #26 // com/moralok/jvm/memory/methodarea/MethodAreaTest_2
#6 = Class #27 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/moralok/jvm/memory/methodarea/MethodAreaTest_2;
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 args
#17 = Utf8 [Ljava/lang/String;
#18 = Utf8 SourceFile
#19 = Utf8 MethodAreaTest_2.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = Class #28 // java/lang/System
#22 = NameAndType #29:#30 // out:Ljava/io/PrintStream;
#23 = Utf8 hello world
#24 = Class #31 // java/io/PrintStream
#25 = NameAndType #32:#33 // println:(Ljava/lang/String;)V
#26 = Utf8 com/moralok/jvm/memory/methodarea/MethodAreaTest_2
#27 = Utf8 java/lang/Object
#28 = Utf8 java/lang/System
#29 = Utf8 out
#30 = Utf8 Ljava/io/PrintStream;
#31 = Utf8 java/io/PrintStream
#32 = Utf8 println
#33 = Utf8 (Ljava/lang/String;)V
{
public com.moralok.jvm.memory.methodarea.MethodAreaTest_2();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/moralok/jvm/memory/methodarea/MethodAreaTest_2;

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 6: 0
line 7: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
}
SourceFile: "MethodAreaTest_2.java"
  1. Class 文件的常量池就是一张表,虚拟机根据索引去查找类名、字段名及其类型,方法名及其参数类型和字面量等。
  2. 当类被加载到虚拟机之后,Class 文件中的常量池中的信息就进入到了运行时常量池。
  3. 这个过程其实就是信息从文件进入了内存。

虚拟机解释器(interpreter)需要解释的字节码指令如下:

1
2
3
0: getstatic     #2
3: ldc #3
5: invokevirtual #4

索引 #2 的意思就是去常量表里查找对应项代表的事物。

直接内存

  • 常见于 NIO 操作中的数据缓冲区。
  • 分配和回收的成本较高,但读写性能更高。
  • 不由 JVM 进行内存释放

NIO 和 IO 的拷贝性能

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

private static final String FROM = "C:\\Users\\username\\Videos\\jellyfin\\media\\movies\\Harry Potter and the Chamber of Secrets (2002) [1080p]\\Harry.Potter.and.the.Chamber.of.Secrets.2002.1080p.BrRip.x264.YIFY.mp4";
private static final String TO = "C:\\Users\\username\\Videos\\jellyfin\\media\\movies\\Harry Potter and the Chamber of Secrets (2002) [1080p]\\Harry.Potter.and.the.Chamber.of.Secrets.2002.1080p.BrRip.x264.YIFY-copy.mp4";
private static final int _1Mb = 1024 * 1024;

public static void main(String[] args) {
io();
directBuffer();
}

private static void directBuffer() {
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();
FileChannel to = new FileOutputStream(TO).getChannel()) {
ByteBuffer buffer = ByteBuffer.allocateDirect(_1Mb);
while (true) {
int len = from.read(buffer);
if (len == -1) {
break;
}
buffer.flip();
to.write(buffer);
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("directBuffer 用时 " + (end - start) / 1000_000.0);
}

private static void io() {
long start = System.nanoTime();
try (FileInputStream from = new FileInputStream(FROM);
FileOutputStream to = new FileOutputStream(TO)) {
byte[] buffer = new byte[_1Mb];
while (true) {
int len = from.read(buffer);
if (len == -1) {
break;
}
to.write(buffer);
}
} catch (IOException e) {
e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("io 用时 " + (end - start) / 1000_000.0);
}
}
1
2
io 用时 1676.9797
directBuffer 用时 836.4796

直接内存溢出

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

private static final int _100Mb = 1024 * 1024 * 100;

public static void main(String[] args) {
List<ByteBuffer> list = new ArrayList<>();
int i = 0;
try {
while (true) {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_100Mb);
list.add(byteBuffer);
i++;
}
} catch (Throwable t) {
t.printStackTrace();
} System.out.println(i);
}
}
1
2
3
4
5
6
java.lang.OutOfMemoryError: Direct buffer memory
at java.nio.Bits.reserveMemory(Bits.java:695)
at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
at com.moralok.jvm.memory.direct.DirectMemoryTest_2.main(DirectMemoryTest_2.java:16)
145

这似乎是代码中抛出的异常,而不是真正的直接内存溢出?

直接内存释放的原理

演示直接内存的释放受 GC 影响

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

private static final int _1GB = 1024 * 1024 * 1024;

public static void main(String[] args) throws IOException {
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1GB);
System.out.println("分配完毕");
System.in.read();
System.out.println("开始释放");
byteBuffer = null;
// 随着 ByteBuffer 的释放,从任务管理器界面看到程序的内存的占用迅速下降 1GB。
System.gc();
System.in.read();
}
}

手动进行直接内存的分配和释放

在代码中实现手动进行直接内存的分配和释放。

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

private static final int _1GB = 1024 * 1024 * 1024;

public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();

// 分配内存
long base = unsafe.allocateMemory(_1GB);
unsafe.setMemory(base, _1GB, (byte) 0);
System.in.read();

// 释放内存
unsafe.freeMemory(base);
System.in.read();
}

private static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}

如何将 GC 和直接内存的分配和释放关联

本质上,直接内存的自动释放是利用了虚引用的机制,间接调用了 unsafe 的分配和释放直接内存的方法。

DirectByteBuffer 就是使用 unsafe.allocateMemory(size) 分配直接内存。DirectByteBuffer 对象以及一个 Deallocator 对象(Runnable 类型)一起用于创建了一个虚引用类型的 Cleaner 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
DirectByteBuffer(int cap) {

// 省略
try {
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 省略
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}

根据虚引用的机制,如果 DirectByteBuffer 对象被回收,虚引用对象会被加入到 Cleanner 的引用队列,ReferenceHandler 线程会处理引用队列中的 Cleaner 对象,进而调用 Deallocator 对象的 run 方法。

1
2
3
4
5
6
7
8
9
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}

如果你准备过 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);
}
}
0%