diff --git a/2020/08/19/docker-frequently-used-commands/index.html b/2020/08/19/docker-frequently-used-commands/index.html index e17cb390..6499aed5 100644 --- a/2020/08/19/docker-frequently-used-commands/index.html +++ b/2020/08/19/docker-frequently-used-commands/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

更新于 - + @@ -768,13 +768,13 @@

- 37k + 38k
Hexo & NexT.Muse 强力驱动 diff --git a/2023/05/27/how-to-install-clash-on-ubuntu/index.html b/2023/05/27/how-to-install-clash-on-ubuntu/index.html index c8b93db0..cda299c7 100644 --- a/2023/05/27/how-to-install-clash-on-ubuntu/index.html +++ b/2023/05/27/how-to-install-clash-on-ubuntu/index.html @@ -27,7 +27,7 @@ - + @@ -157,13 +157,13 @@ @@ -238,7 +238,7 @@

- + @@ -365,13 +365,13 @@

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html b/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html index 02d48d8e..87af5a12 100644 --- a/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html +++ b/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -346,13 +346,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html b/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html index 971751f3..6e6986c4 100644 --- a/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html +++ b/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -351,13 +351,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html b/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html index e249b177..a1ecda64 100644 --- a/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html +++ b/2023/06/07/how-to-use-OpenVPN-to-access-home-network/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -378,13 +378,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html b/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html index e3697c24..5969d3f0 100644 --- a/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html +++ b/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/index.html @@ -27,10 +27,10 @@ - + - + @@ -157,13 +157,13 @@ @@ -238,7 +238,7 @@

- + @@ -325,8 +325,8 @@

@@ -372,13 +372,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html b/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html index c66230b0..b2cbbf99 100644 --- a/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html +++ b/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -380,13 +380,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html b/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html index 19cf7730..a6a51f6b 100644 --- a/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html +++ b/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -341,13 +341,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html b/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html index 9dc2a04d..61b4e5bd 100644 --- a/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html +++ b/2023/06/28/how-to-use-ssh-to-connect-github-and-server/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -364,13 +364,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/06/29/tmux-frequently-used-commands/index.html b/2023/06/29/tmux-frequently-used-commands/index.html index d6017c45..0f51191e 100644 --- a/2023/06/29/tmux-frequently-used-commands/index.html +++ b/2023/06/29/tmux-frequently-used-commands/index.html @@ -27,7 +27,7 @@ - + @@ -156,13 +156,13 @@ @@ -237,7 +237,7 @@

- + @@ -384,13 +384,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/07/13/Java-class-loader-source-code-analysis/index.html b/2023/07/13/Java-class-loader-source-code-analysis/index.html index b30ec650..07109f64 100644 --- a/2023/07/13/Java-class-loader-source-code-analysis/index.html +++ b/2023/07/13/Java-class-loader-source-code-analysis/index.html @@ -27,7 +27,7 @@ - + @@ -157,13 +157,13 @@ @@ -238,7 +238,7 @@

- + @@ -518,13 +518,13 @@

- 37k + 38k
Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/01/testing-and-analysis-of-jvm-gc/index.html b/2023/11/01/testing-and-analysis-of-jvm-gc/index.html index 96013f1e..4f1f2490 100644 --- a/2023/11/01/testing-and-analysis-of-jvm-gc/index.html +++ b/2023/11/01/testing-and-analysis-of-jvm-gc/index.html @@ -27,7 +27,7 @@ - + @@ -157,13 +157,13 @@ @@ -238,7 +238,7 @@

- + @@ -394,13 +394,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/03/testing-and-analysis-of-StringTable/index.html b/2023/11/03/testing-and-analysis-of-StringTable/index.html index e88a0d56..c92b5041 100644 --- a/2023/11/03/testing-and-analysis-of-StringTable/index.html +++ b/2023/11/03/testing-and-analysis-of-StringTable/index.html @@ -31,7 +31,7 @@ - + @@ -162,13 +162,13 @@ @@ -243,7 +243,7 @@

- + @@ -437,13 +437,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html b/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html index 5cd05f8c..51013234 100644 --- a/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html +++ b/2023/11/04/testing-and-analysis-of-jvm-memory-area/index.html @@ -32,7 +32,7 @@ - + @@ -163,13 +163,13 @@ @@ -244,7 +244,7 @@

- + @@ -471,13 +471,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/07/garbage-collection-in-Java/index.html b/2023/11/07/garbage-collection-in-Java/index.html index 54f9fb51..8b548ece 100644 --- a/2023/11/07/garbage-collection-in-Java/index.html +++ b/2023/11/07/garbage-collection-in-Java/index.html @@ -34,7 +34,7 @@ - + @@ -165,13 +165,13 @@ @@ -246,7 +246,7 @@

- + @@ -673,13 +673,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html b/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html index 86e17a67..85449d0a 100644 --- a/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html +++ b/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html @@ -28,7 +28,7 @@ - + @@ -159,13 +159,13 @@ @@ -240,7 +240,7 @@

- + @@ -368,6 +368,9 @@

+

@@ -399,13 +402,13 @@

- 37k + 38k
Hexo & NexT.Muse 强力驱动 diff --git a/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110180458.png b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110180458.png new file mode 100644 index 00000000..1b1268ba Binary files /dev/null and b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110180458.png differ diff --git a/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110182254.png b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110182254.png new file mode 100644 index 00000000..3c7b0b19 Binary files /dev/null and b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110182254.png differ diff --git a/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110190407.png b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110190407.png new file mode 100644 index 00000000..7613a04a Binary files /dev/null and b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110190407.png differ diff --git a/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110190442.png b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110190442.png new file mode 100644 index 00000000..0978f128 Binary files /dev/null and b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110190442.png differ diff --git a/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110191738.png b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110191738.png new file mode 100644 index 00000000..e76ce16f Binary files /dev/null and b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110191738.png differ diff --git a/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110193919.png b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110193919.png new file mode 100644 index 00000000..e911776c Binary files /dev/null and b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/Pasted image 20231110193919.png differ diff --git a/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/index.html b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/index.html new file mode 100644 index 00000000..d499bda8 --- /dev/null +++ b/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/index.html @@ -0,0 +1,415 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +基于 Redis 的分布式锁的简单实现 | Moralok + + + + + + + + + + + +
+ +
+
+
+ + + + + +
+ + + + + + + +
+ +
+ +
+ + + + + + + + +
+ + +
+ + 0% +
+ + + + +
+ + + + + +
+ + + +
+ + + + + + + +
+

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

+ + +
+ + + + +
+

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

+ + +

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

+

基于 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 的过期时间,锁将在处理结束前被释放,就可能发生问题。

+ + +

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

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

+

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

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

+

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

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

+

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

+

改进二:加锁时指定 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。

+

Redis 分布式锁如果要支持可重入,可以使用线程的 ThreadLocal 变量存储当前持有的锁计数。

+
1
private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);
+ +

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

+

参考文章

    +
  • 《Redis 深度历险,核心原理与应用实践》
  • +
+ +
+ + + + + + +
+
+ + + + + + +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + diff --git a/archives/2020/08/index.html b/archives/2020/08/index.html index 7485dfa0..da3e5551 100644 --- a/archives/2020/08/index.html +++ b/archives/2020/08/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 16 篇日志。 继续努力。 + 嗯..! 目前共计 17 篇日志。 继续努力。
@@ -250,13 +250,13 @@

Moralok

- 37k + 38k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2020/index.html b/archives/2020/index.html index f78a6a79..c081715f 100644 --- a/archives/2020/index.html +++ b/archives/2020/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 16 篇日志。 继续努力。 + 嗯..! 目前共计 17 篇日志。 继续努力。
@@ -250,13 +250,13 @@

Moralok

- 37k + 38k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/05/index.html b/archives/2023/05/index.html index f1bf52c2..b9ee8216 100644 --- a/archives/2023/05/index.html +++ b/archives/2023/05/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 16 篇日志。 继续努力。 + 嗯..! 目前共计 17 篇日志。 继续努力。
@@ -250,13 +250,13 @@

Moralok

- 37k + 38k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/06/index.html b/archives/2023/06/index.html index 488fd702..899cc22b 100644 --- a/archives/2023/06/index.html +++ b/archives/2023/06/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 16 篇日志。 继续努力。 + 嗯..! 目前共计 17 篇日志。 继续努力。
@@ -390,13 +390,13 @@

Moralok

- 37k + 38k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/07/index.html b/archives/2023/07/index.html index 95008009..8ed22336 100644 --- a/archives/2023/07/index.html +++ b/archives/2023/07/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 16 篇日志。 继续努力。 + 嗯..! 目前共计 17 篇日志。 继续努力。
@@ -250,13 +250,13 @@

Moralok

- 37k + 38k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html index 29e915a6..de2834af 100644 --- a/archives/2023/11/index.html +++ b/archives/2023/11/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 16 篇日志。 继续努力。 + 嗯..! 目前共计 17 篇日志。 继续努力。
@@ -203,6 +203,26 @@

Moralok

2023
+ +
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/index.html b/archives/2023/index.html index 5da1ccd5..eb71a887 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 16 篇日志。 继续努力。 + 嗯..! 目前共计 17 篇日志。 继续努力。
@@ -203,6 +203,26 @@

Moralok

2023
+ +
- -
@@ -433,13 +433,13 @@

Moralok

- 37k + 38k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/page/2/index.html b/archives/2023/page/2/index.html index 6494ffb2..12bae7f3 100644 --- a/archives/2023/page/2/index.html +++ b/archives/2023/page/2/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 16 篇日志。 继续努力。 + 嗯..! 目前共计 17 篇日志。 继续努力。
@@ -203,6 +203,26 @@

Moralok

2023
+ +
Hexo & NexT.Muse 强力驱动 diff --git a/archives/index.html b/archives/index.html index b8eb8694..3a1d7a11 100644 --- a/archives/index.html +++ b/archives/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 16 篇日志。 继续努力。 + 嗯..! 目前共计 17 篇日志。 继续努力。
@@ -203,6 +203,26 @@

Moralok

2023
+ +
- -
@@ -433,13 +433,13 @@

Moralok

- 37k + 38k
Hexo & NexT.Muse 强力驱动 diff --git a/archives/page/2/index.html b/archives/page/2/index.html index 6ca7b3cb..ebd1541c 100644 --- a/archives/page/2/index.html +++ b/archives/page/2/index.html @@ -152,13 +152,13 @@

Moralok

@@ -195,7 +195,7 @@

Moralok

- 嗯..! 目前共计 16 篇日志。 继续努力。 + 嗯..! 目前共计 17 篇日志。 继续努力。
@@ -203,6 +203,26 @@

Moralok

2023
+ +
Hexo & NexT.Muse 强力驱动 diff --git a/css/main.css b/css/main.css index 09842eae..6f33d61e 100644 --- a/css/main.css +++ b/css/main.css @@ -2641,7 +2641,7 @@ mark.search-keyword { vertical-align: middle; } .links-of-author a::before { - background: #36ffff; + background: #cdfff0; display: inline-block; margin-right: 3px; transform: translateY(-2px); diff --git a/index.html b/index.html index 4861a5a1..64cb15ab 100644 --- a/index.html +++ b/index.html @@ -152,13 +152,13 @@

Moralok

@@ -191,6 +191,146 @@

Moralok

+
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

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

+ + +

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

+

基于 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 的过期时间,锁将在处理结束前被释放,就可能发生问题。

+ + +

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

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

+

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

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

+

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

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

+

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

+

改进二:加锁时指定 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。

+

Redis 分布式锁如果要支持可重入,可以使用线程的 ThreadLocal 变量存储当前持有的锁计数。

+
1
private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);
+ +

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

+

参考文章

    +
  • 《Redis 深度历险,核心原理与应用实践》
  • +
+ + +
+ + + + + +
+
+ +
+
+
+ + + + + + +
@@ -232,7 +372,7 @@

- + @@ -400,7 +540,7 @@

- + @@ -833,7 +973,7 @@

- + @@ -1066,7 +1206,7 @@

- + @@ -1266,7 +1406,7 @@

- + @@ -1428,7 +1568,7 @@

- + @@ -1714,7 +1854,7 @@

- + @@ -1868,7 +2008,7 @@

- + @@ -2002,7 +2142,7 @@

- + @@ -2069,156 +2209,6 @@

- - - -
- - - - - - - -
-

- -

- - -
- - - - -
-

环境搭建

安装 Ubuntu 20.04

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

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

安装 Docker

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

-

安装 Minikube

参考官方文档:minikube start

-

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

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

-

开始使用

创建集群:

-
1
minikube start
- -

创建集群时的权限问题

不加 sudo

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

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

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

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

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

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

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

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

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

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

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

-

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

-
将用户添加到 docker 用户组

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

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

创建集群时下载镜像失败

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

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

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

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

启动控制台:

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

minikube addons enable metrics-server


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

- -

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

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

使用过程中下载镜像失败

从其他镜像仓库下载代替

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

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

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

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

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

-
配置代理

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

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

需要使用自定义镜像

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

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

参考链接

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

- - -
- - - - - -
-
- -
-
-

- - - - @@ -2243,13 +2233,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json index 9e6b6763..d6e29221 100644 --- a/leancloud_counter_security_urls.json +++ b/leancloud_counter_security_urls.json @@ -1 +1 @@ -[{"title":"在 Ubuntu 上安装 Clash","url":"/2023/05/27/how-to-install-clash-on-ubuntu/"},{"title":"Docker 常用命令列表","url":"/2020/08/19/docker-frequently-used-commands/"},{"title":"在 Windows 10 上安装 OpenVPN 服务器","url":"/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/"},{"title":"在 iOS 和 macOS 上安装 OpenVPN 客户端","url":"/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/"},{"title":"使用 OpenVPN 访问家庭内网","url":"/2023/06/07/how-to-use-OpenVPN-to-access-home-network/"},{"title":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"title":"如何为终端、docker 和容器设置代理","url":"/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/"},{"title":"Ubuntu server 20.04 安装后没有分配全部磁盘空间","url":"/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/"},{"title":"如何使用 SSH 连接 Github 和服务器","url":"/2023/06/28/how-to-use-ssh-to-connect-github-and-server/"},{"title":"Tmux 常用命令和快捷键","url":"/2023/06/29/tmux-frequently-used-commands/"},{"title":"JVM GC 的测试和分析","url":"/2023/11/01/testing-and-analysis-of-jvm-gc/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"title":"Java 类加载器源码分析","url":"/2023/07/13/Java-class-loader-source-code-analysis/"},{"title":"关于 Java 字节码指令的一些例子分析","url":"/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/"},{"title":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"},{"title":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"}] \ No newline at end of file +[{"title":"Docker 常用命令列表","url":"/2020/08/19/docker-frequently-used-commands/"},{"title":"在 iOS 和 macOS 上安装 OpenVPN 客户端","url":"/2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/"},{"title":"在 Windows 10 上安装 OpenVPN 服务器","url":"/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/"},{"title":"在 Ubuntu 上安装 Clash","url":"/2023/05/27/how-to-install-clash-on-ubuntu/"},{"title":"使用 OpenVPN 访问家庭内网","url":"/2023/06/07/how-to-use-OpenVPN-to-access-home-network/"},{"title":"如何为终端、docker 和容器设置代理","url":"/2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/"},{"title":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"title":"Ubuntu server 20.04 安装后没有分配全部磁盘空间","url":"/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/"},{"title":"如何使用 SSH 连接 Github 和服务器","url":"/2023/06/28/how-to-use-ssh-to-connect-github-and-server/"},{"title":"Tmux 常用命令和快捷键","url":"/2023/06/29/tmux-frequently-used-commands/"},{"title":"JVM GC 的测试和分析","url":"/2023/11/01/testing-and-analysis-of-jvm-gc/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"title":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"},{"title":"关于 Java 字节码指令的一些例子分析","url":"/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/"},{"title":"基于 Redis 的分布式锁的简单实现","url":"/2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/"},{"title":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"},{"title":"Java 类加载器源码分析","url":"/2023/07/13/Java-class-loader-source-code-analysis/"}] \ No newline at end of file diff --git a/page/2/index.html b/page/2/index.html index a7ed857d..d850f838 100644 --- a/page/2/index.html +++ b/page/2/index.html @@ -152,13 +152,13 @@

Moralok

@@ -191,6 +191,156 @@

Moralok

+
+ + + +
+ + + + + + + +
+

+ +

+ + +
+ + + + +
+

环境搭建

安装 Ubuntu 20.04

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

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

安装 Docker

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

+

安装 Minikube

参考官方文档:minikube start

+

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

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

+

开始使用

创建集群:

+
1
minikube start
+ +

创建集群时的权限问题

不加 sudo

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

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

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

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

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

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

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

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

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

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

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

+

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

+
将用户添加到 docker 用户组

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

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

创建集群时下载镜像失败

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

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

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

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

启动控制台:

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

minikube addons enable metrics-server


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

+ +

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

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

使用过程中下载镜像失败

从其他镜像仓库下载代替

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

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

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

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

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

+
配置代理

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

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

需要使用自定义镜像

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

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

参考链接

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

+ + +
+ + + + + +
+
+ +
+
+
+ + + + + + +
@@ -232,7 +382,7 @@

- + @@ -372,7 +522,7 @@

- + @@ -520,7 +670,7 @@

- + @@ -636,7 +786,7 @@

- + @@ -757,7 +907,7 @@

- + @@ -890,7 +1040,7 @@

- + @@ -1411,13 +1561,13 @@

- 37k + 38k

Hexo & NexT.Muse 强力驱动 diff --git a/search.xml b/search.xml index 32e07e32..2bc170d1 100644 --- a/search.xml +++ b/search.xml @@ -1,50 +1,5 @@ - - 在 Ubuntu 上安装 Clash - /2023/05/27/how-to-install-clash-on-ubuntu/ - 安装 Clash

GitHub 下载预构建的二进制文件。

-
:/mnt/hgfs/share$ wget https://github.com/Dreamacro/clash/releases/download/v1.16.0/clash-linux-amd64-v1.16.0.gz
- -

使用 gzip 解压压缩包 clash-linux-amd64-v1.16.0.gz 得到 clash-linux-amd64-v1.16.0。

-
:/mnt/hgfs/share$ gzip -d clash-linux-amd64-v1.16.0.gz 
gzip: clash-linux-amd64-v1.16.0: Value too large for defined data type
-

忽略提示。

-

移动二进制文件到目录 /usr/local/bin 并且重命名为 clash。

-
:/mnt/hgfs/share$ sudo mv clash-linux-amd64-v1.16.0 /usr/local/bin/clash
[sudo] password for wrmao:
- -

现在可以通过 clash -v 查看 Clash 的版本信息。

-
:~$ clash -v
Clash v1.16.0 linux amd64 with go1.20.4 Fri May 19 13:57:32 UTC 2023
- -

使用 Clash

使用命令 clash 启动,可以看到日志。

-
:~$ clash
INFO[0000] Can't find config, create a initial config file
INFO[0000] Can't find MMDB, start download
INFO[0003] Mixed(http+socks) proxy listening at: 127.0.0.1:7890
- -

在第一次启动 Clash 时,Clash 会在 ~/.config 下创建目录 clash,并在其中创建 3 个文件。

-
:~/.config/clash$ ls
cache.db config.yaml Country.mmdb
-

其中 config.yaml 是 Clash 的配置文件,Country.mmdb 是全球 IP 库,可以实现各个国家的 IP 信息解析和地理定位。
config.yaml 的内容直接从已有的配置文件复制过来。

-

可以浏览器访问 http://clash.razord.top/#/proxies 选择代理服务器。

-

设置 Clash 为后台服务

参考官方文档 Clash as a service

-

拷贝配置文件到 /etc/clash

-
:~/.config/clash$ sudo cp config.yaml /etc/clash/
:~/.config/clash$ sudo cp Country.mmdb /etc/clash/
- -

/etc/systemd/system/clash.service 创建 systemd 配置文件:

-
[Unit]
Description=Clash daemon, A rule-based proxy in Go.
After=network-online.target

[Service]
Type=simple
Restart=always
ExecStart=/usr/local/bin/clash -d /etc/clash

[Install]
WantedBy=multi-user.target
- -

之后重载 systemd:

-
systemctl daemon-reload
- -

设置系统开机时启动 clashd:

-
systemctl enable clash
- -

马上启动 clashd:

-
systemctl start clash
- -

检查 Clash 的健康状态和日志:

-
systemctl status clash
journalctl -xe
]]> - - clash - proxy - - Docker 常用命令列表 /2020/08/19/docker-frequently-used-commands/ @@ -497,6 +452,33 @@ docker + + 在 iOS 和 macOS 上安装 OpenVPN 客户端 + /2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/ + 安装 OpenVPN Connect

macOS 访问官网下载
iOS 访问 AppStore,需要登录外区 Apple ID。

+

配置 OpenVPN Connect

客户端提供了两种方式导入配置文件,一是通过 URL,建议 URL 仅限在私有网络内访问,二是通过其他方式例如邮件,下载为本地文件再导入。

+

配置文件的组织方式又分为两种形式,一种是将 CA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 的内容复制粘贴到 client.ovpn 中,形成一个联合配置文件;另一种是使用 openssl 将 CA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 转换为 PKCS#12 文件,先后导入 client.ovpn12 和 client.ovpn。

+

单一 client.ovpn

从目录 C:\Program Files\OpenVPN\sample-config 复制客户端配置文件模板 client.ovpn,修改以下配置:

+
remote your-server 1194

;ca ca.crt
;cert client.crt
;key client.key

;tls-auth ta.key 1

<ca>
-----BEGIN CERTIFICATE-----
paste contents of ca.crt
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
paste contents of client.crt
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
paste contents of client.key
-----END PRIVATE KEY-----
</key>
+

remote your-server 1194 中的地址和端口替换成你的 OpenVPN server 的地址和端口。将 ca ca.crtcert client.crtkey client.keytls-auth ta.key 1 注释掉,将各自文件中的内容以上述类 XML 的形式粘贴到 client.ovpn 中。

+

将修改好的客户端配置文件导入到客户端中即可。

+

client.ovpn + client.opvn12

使用 openssl 命令将客户端的证书和私钥文件转换为 PKCS#12 形式的文件。该命令会提示 Enter Export Password,可以为空,但为了安全建议设置密码。

+
openssl pkcs12 -export -in cert -inkey key -certfile ca -name MyClient -out client.ovpn12
+

由于在 iOS 中导入 PKCS#12 文件到 Keychain 中时只导入了客户端证书和密钥,CA 根证书并没有导入,client.ovpn 文件中必须要保留 CA 根证书的配置。
既可以用传统的引用文件的方式:

+
ca ca.crt
+

也可以用类 XML 的形式粘贴 ca.crt 内容到 client.ovpn 中:

+
<ca>
paste contents of ca.crt here
</ca>
+

先导入 client.ovpn12(需要输入转换时的密码),再导入 client.ovpn。

+
+

但是我失败了……导入 client.ovpn12 时密码一直错误,搜索到类似的案例,但是没有找到解决方案。不确定是不是 openssl 版本引起的。

+
+

路由器 NAT

在路由器管理后台的 NAT 设置功能里,配置好对外端口号和 Windows 10 主机上 OpenVPN 端口号的映射关系。

+

参考链接

iOS 使用 OpenVPN 的 FAQ
如何通过 iOS Keychain 使用客户端证书和密钥
如何配置 iOS OpenVPN 客户端的证书认证

+]]>
+ + OpenVPN + +
在 Windows 10 上安装 OpenVPN 服务器 /2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/ @@ -530,30 +512,48 @@ - 在 iOS 和 macOS 上安装 OpenVPN 客户端 - /2023/06/07/how-to-setup-OpenVPN-connect-client-on-iOS-and-macOS/ - 安装 OpenVPN Connect

macOS 访问官网下载
iOS 访问 AppStore,需要登录外区 Apple ID。

-

配置 OpenVPN Connect

客户端提供了两种方式导入配置文件,一是通过 URL,建议 URL 仅限在私有网络内访问,二是通过其他方式例如邮件,下载为本地文件再导入。

-

配置文件的组织方式又分为两种形式,一种是将 CA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 的内容复制粘贴到 client.ovpn 中,形成一个联合配置文件;另一种是使用 openssl 将 CA 根证书 ca.crt,客户端证书 client.crt,客户端密钥 client.key 转换为 PKCS#12 文件,先后导入 client.ovpn12 和 client.ovpn。

-

单一 client.ovpn

从目录 C:\Program Files\OpenVPN\sample-config 复制客户端配置文件模板 client.ovpn,修改以下配置:

-
remote your-server 1194

;ca ca.crt
;cert client.crt
;key client.key

;tls-auth ta.key 1

<ca>
-----BEGIN CERTIFICATE-----
paste contents of ca.crt
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
paste contents of client.crt
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
paste contents of client.key
-----END PRIVATE KEY-----
</key>
-

remote your-server 1194 中的地址和端口替换成你的 OpenVPN server 的地址和端口。将 ca ca.crtcert client.crtkey client.keytls-auth ta.key 1 注释掉,将各自文件中的内容以上述类 XML 的形式粘贴到 client.ovpn 中。

-

将修改好的客户端配置文件导入到客户端中即可。

-

client.ovpn + client.opvn12

使用 openssl 命令将客户端的证书和私钥文件转换为 PKCS#12 形式的文件。该命令会提示 Enter Export Password,可以为空,但为了安全建议设置密码。

-
openssl pkcs12 -export -in cert -inkey key -certfile ca -name MyClient -out client.ovpn12
-

由于在 iOS 中导入 PKCS#12 文件到 Keychain 中时只导入了客户端证书和密钥,CA 根证书并没有导入,client.ovpn 文件中必须要保留 CA 根证书的配置。
既可以用传统的引用文件的方式:

-
ca ca.crt
-

也可以用类 XML 的形式粘贴 ca.crt 内容到 client.ovpn 中:

-
<ca>
paste contents of ca.crt here
</ca>
-

先导入 client.ovpn12(需要输入转换时的密码),再导入 client.ovpn。

-
-

但是我失败了……导入 client.ovpn12 时密码一直错误,搜索到类似的案例,但是没有找到解决方案。不确定是不是 openssl 版本引起的。

-
-

路由器 NAT

在路由器管理后台的 NAT 设置功能里,配置好对外端口号和 Windows 10 主机上 OpenVPN 端口号的映射关系。

-

参考链接

iOS 使用 OpenVPN 的 FAQ
如何通过 iOS Keychain 使用客户端证书和密钥
如何配置 iOS OpenVPN 客户端的证书认证

-]]>
+ 在 Ubuntu 上安装 Clash + /2023/05/27/how-to-install-clash-on-ubuntu/ + 安装 Clash

GitHub 下载预构建的二进制文件。

+
:/mnt/hgfs/share$ wget https://github.com/Dreamacro/clash/releases/download/v1.16.0/clash-linux-amd64-v1.16.0.gz
+ +

使用 gzip 解压压缩包 clash-linux-amd64-v1.16.0.gz 得到 clash-linux-amd64-v1.16.0。

+
:/mnt/hgfs/share$ gzip -d clash-linux-amd64-v1.16.0.gz 
gzip: clash-linux-amd64-v1.16.0: Value too large for defined data type
+

忽略提示。

+

移动二进制文件到目录 /usr/local/bin 并且重命名为 clash。

+
:/mnt/hgfs/share$ sudo mv clash-linux-amd64-v1.16.0 /usr/local/bin/clash
[sudo] password for wrmao:
+ +

现在可以通过 clash -v 查看 Clash 的版本信息。

+
:~$ clash -v
Clash v1.16.0 linux amd64 with go1.20.4 Fri May 19 13:57:32 UTC 2023
+ +

使用 Clash

使用命令 clash 启动,可以看到日志。

+
:~$ clash
INFO[0000] Can't find config, create a initial config file
INFO[0000] Can't find MMDB, start download
INFO[0003] Mixed(http+socks) proxy listening at: 127.0.0.1:7890
+ +

在第一次启动 Clash 时,Clash 会在 ~/.config 下创建目录 clash,并在其中创建 3 个文件。

+
:~/.config/clash$ ls
cache.db config.yaml Country.mmdb
+

其中 config.yaml 是 Clash 的配置文件,Country.mmdb 是全球 IP 库,可以实现各个国家的 IP 信息解析和地理定位。
config.yaml 的内容直接从已有的配置文件复制过来。

+

可以浏览器访问 http://clash.razord.top/#/proxies 选择代理服务器。

+

设置 Clash 为后台服务

参考官方文档 Clash as a service

+

拷贝配置文件到 /etc/clash

+
:~/.config/clash$ sudo cp config.yaml /etc/clash/
:~/.config/clash$ sudo cp Country.mmdb /etc/clash/
+ +

/etc/systemd/system/clash.service 创建 systemd 配置文件:

+
[Unit]
Description=Clash daemon, A rule-based proxy in Go.
After=network-online.target

[Service]
Type=simple
Restart=always
ExecStart=/usr/local/bin/clash -d /etc/clash

[Install]
WantedBy=multi-user.target
+ +

之后重载 systemd:

+
systemctl daemon-reload
+ +

设置系统开机时启动 clashd:

+
systemctl enable clash
+ +

马上启动 clashd:

+
systemctl start clash
+ +

检查 Clash 的健康状态和日志:

+
systemctl status clash
journalctl -xe
]]>
- OpenVPN + clash + proxy
@@ -615,6 +615,58 @@ OpenVPN + + 如何为终端、docker 和容器设置代理 + /2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/ + Ubuntu 上安装 Clash 后,Clash 通过监听本地的 7890 端口,提供代理服务。但是不同程序设置代理的方式不尽相同,并不是启动了 Clash 以及在某一处设置后,整个系统发出的 HTTP 请求都能经过代理。本文将介绍如何为终端、docker 和容器添加代理。

+

为终端设置代理

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

+

设置 Shell 环境变量

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

+
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
+

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

+
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
+

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

+

修改 wget 配置文件

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

+
use_proxy = on

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

为 docker 设置代理

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

+

通过 systemd 设置

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

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

docker.service[Service] 模块添加:

+
Environment=HTTP_PROXY=http://proxyAddress:port
Environment=HTTPS_PROXY=http://proxyAddress:port
Environment=NO_PROXY=localhost,127.0.0.1
+

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

+
systemctl daemon-reload
systemctl restart docker.service
+ +

修改 dockerd 配置文件

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

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

然后重启 docker daemon 即可。

+
+

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

+
+

为 docker 容器设置代理

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

+

修改配置文件

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

+
{
"proxies":
{
"default":
{
"httpProxy": "http://proxyAddress:port",
"httpsProxy": "http://proxyAddress:port",
"noProxy": "localhost,127.0.0.1"
}
}
}
+

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

+

为指定容器添加环境变量

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

+
+

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

+
+

总结

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

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

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

+
+

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

+
+

参考链接

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

+]]>
+ + docker + proxy + +
如何在 Ubuntu 20.04 上安装 Minikube /2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/ @@ -676,58 +728,6 @@ minikube - - 如何为终端、docker 和容器设置代理 - /2023/06/13/how-to-configure-proxy-for-terminal-docker-and-container/ - Ubuntu 上安装 Clash 后,Clash 通过监听本地的 7890 端口,提供代理服务。但是不同程序设置代理的方式不尽相同,并不是启动了 Clash 以及在某一处设置后,整个系统发出的 HTTP 请求都能经过代理。本文将介绍如何为终端、docker 和容器添加代理。

-

为终端设置代理

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

-

设置 Shell 环境变量

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

-
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
-

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

-
export http_proxy=http://proxyAddress:port
export https_proxy=http://proxyAddress:port
export no_proxy=localhost,127.0.0.1
-

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

-

修改 wget 配置文件

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

-
use_proxy = on

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

为 docker 设置代理

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

-

通过 systemd 设置

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

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

docker.service[Service] 模块添加:

-
Environment=HTTP_PROXY=http://proxyAddress:port
Environment=HTTPS_PROXY=http://proxyAddress:port
Environment=NO_PROXY=localhost,127.0.0.1
-

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

-
systemctl daemon-reload
systemctl restart docker.service
- -

修改 dockerd 配置文件

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

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

然后重启 docker daemon 即可。

-
-

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

-
-

为 docker 容器设置代理

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

-

修改配置文件

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

-
{
"proxies":
{
"default":
{
"httpProxy": "http://proxyAddress:port",
"httpsProxy": "http://proxyAddress:port",
"noProxy": "localhost,127.0.0.1"
}
}
}
-

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

-

为指定容器添加环境变量

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

-
-

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

-
-

总结

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

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

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

-
-

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

-
-

参考链接

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

-]]>
- - proxy - docker - -
Ubuntu server 20.04 安装后没有分配全部磁盘空间 /2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/ @@ -1047,201 +1047,148 @@ - Java 类加载器源码分析 - /2023/07/13/Java-class-loader-source-code-analysis/ - 组织类加载工作:loadClass

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

-
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
+ JVM 内存区域的测试和分析 + /2023/11/04/testing-and-analysis-of-jvm-memory-area/ + 内存区域

JVM 内存区域划分为:

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

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

-
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. -
  3. 在父·类加载器上调用 loadClass 方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。
  4. -
  5. 调用 findClass(String) 方法来查找该类。
  6. +

    程序计数器

    虚拟机栈

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

    +

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

    +

    栈内存溢出

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

    如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 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 对象本身。
    这个方案意味着非同名的目标类可以认为在加载时没有冲突?

    -
    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;
    }
    +

    不正确的递归调用

    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();
    }
    }
    -
    什么是 “ClassLoader 对象注册为具有并行能力”呢?

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

    -
    static {
    ClassLoader.registerAsParallelCapable();
    }
    -

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

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

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

    -
    注册工作做了什么?

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

    -
    @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);
    }
    +

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

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

    +
    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;
    }
    }
    }
    -

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

    -
    // 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);
    }
    }
    }
    +

    局部变量的线程安全问题

      +
    1. 局部变量如果未逃离方法的作用范围,就是线程安全的。
    2. +
    3. 局部变量如果是引用类型且逃离了方法的作用范围,就是线程不安全的。
    4. +
    +
    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;
    }
    }
    -
    “注册”怎么和锁产生联系?

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

    -
    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());
    }
    +

    线程问题排查

    CPU 占用率居高不下

    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. +
    3. ps H -eo pid,tid,%cpu | grep pid,进一步定位引起 cpu 占用高的线程 id。
    4. +
    5. jstack pid,根据线程 id 换算成 16进制的 nid 找到对应线程,进一步定位到问题的源码行号。
    6. +
    +
    "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)
    -

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

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

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

    -
    -

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

    -
    -

    检查目标类是否已加载

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

    -
    protected final Class<?> findLoadedClass(String name) {
    if (!checkName(name))
    return null;
    return findLoadedClass0(name);
    }

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

    死锁,迟迟未返回结果

    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();
    }
    }
    -

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

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

    -
    -

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

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

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

    -

    通常流程如下:

      -
    1. AppClassLoader 调用 loadClass 方法,先委派给 ExtClassLoader
    2. -
    3. ExtClassLoader 调用 loadClass 方法,先委派给 bootstrap class loader
    4. -
    5. bootstrap class loader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
    6. -
    7. ExtClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
    8. -
    9. ExtClassLoader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
    10. -
    11. AppClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
    12. +
    13. jstack pid,会显示找到死锁,以及死锁涉及的线程,,并各自持有的锁还有等待的锁。
    14. +
    15. 其他工具如 jconsole 也具有检测死锁的功能。
    -

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

    -

    查找类资源:findClass

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

    -

    过程如下:

    +

    本地方法栈

    堆(Heap)的特点:

      -
    1. name 转换为 path,比如 com.example.BananaTest 转换为 com/example/BananaTest.class
    2. -
    3. 使用 URL 搜索路径 URLClassPathpath 中获取 Resource,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。
    4. -
    5. 调用 URLClassLoader 的私有方法 defineClass,该方法调用父类 SecureClassLoaderdefineClass 方法。
    6. +
    7. 线程共享,需要考虑线程安全问题。
    8. +
    9. 存在垃圾回收机制。
    10. +
    11. 使用 -Xmx8m 设置大小。
    -
    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 的核心构造器方法:

    -
    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

    -
    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;
    }
    +

    堆内存溢出

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

    +
    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);
    }
    }
    }
    -
    URLClassPath#getNextLoader

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

    -
    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);
    }
    }
    +
    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
    -
    URLClassPath#getLoader(int)
      -
    1. index 到存放已创建 Loader 的列表中去获取(调用方传入的 index0 开始不断递增直到超过范围)。
    2. -
    3. 如果 index 超过范围,说明已有的 Loader 都找不到目标 Resource,需要到未打开的 URL 中查找。
    4. -
    5. 从未打开的 URL 中取出(pop)一个来创建 Loader,如果 urls 已经为空,则返回 null
    6. +

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

      +

      监测堆内存

        +
      1. 使用 jps 查看 Java 进程列表
      2. +
      3. 使用 jmap -heap pid 查看堆内存信息
      4. +
      5. 还可以使用 jconsole 观察堆内存变化曲线
      6. +
      7. 还可以使用 VisualVM 查看堆信息
      -
      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);
      }
      +
      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);
      }
      }
      -
      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. -
      3. 如果 URL/ 结尾,且协议为 file,则返回 FileLoader 类型,比如 file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/
      4. -
      5. 如果 URL/ 结尾,且协议不会 file,则返回 Loader 类型。
      6. -
      -
      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();
      }
      }
      +

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

      +
      Eden Space:
      capacity = 268435456 (256.0MB)
      used = 32212360 (30.72010040283203MB)

      used = 42698136 (40.720115661621094MB)

      used = 5368728 (5.120018005371094MB)
      -

      URLClassPath.FileLoader#getResource

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

      -
      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;
      }
      +

      使用 jconsole 查看堆内存信息:

      + -

      ClassLoader 的搜索路径

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

      -

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

      -
      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
      +

      堆内存占用居高不下

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

      +
      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 功能:

      + -
      bootstrap class loader

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

      +

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

      +

      方法区

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

        -
      • -Xbootclasspath::完全覆盖核心类库的类路径,不常用,除非重写核心类库。
      • -
      • -Xbootclasspath/a: 以后缀的方式拼接在原搜索路径后面,常用。
      • -
      • -Xbootclasspath/p: 以前缀的方式拼接再原搜索路径前面.不常用,避免引起不必要的冲突。
      • +
      • JDK 8 之前,方法区的具体实现为永久代,使用堆内存,使用 -XX:MaxPermSize=10m 设置大小。
      • +
      • JDK 8 开始,方法区的具体实现为元空间,使用直接内存,使用 -XX:MaxMetaspaceSize=10m 设置大小。
      -

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

      -
      Error occurred during initialization of VM
      java/lang/NoClassDefFoundError: java/lang/Object

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

      方法区溢出

      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. +
      3. 实验中抛出 java.lang.OutOfMemoryError: Compressed class space。
      4. +
      5. 添加参数 -XX:-UseCompressedClassPointers 后,抛出 java.lang.OutOfMemoryError: Metaspace。
      6. +
      7. JDK 6 设置 -XX:MaxPermSize=8m,抛出 java.lang.OutOfMemoryError: PermGen space。
      8. +
      +

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

      +

      运行时常量池

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

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

      使用 javap 反编译

      public class MethodAreaTest_2 {  

      public static void main(String[] args) {
      System.out.println("hello world");
      }
      }
      -
      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 方法。

      -
      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);
      +
      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"
      -

      其方法参数有:

      -
        -
      • name,目标类的名称。
      • -
      • byte[]ByteBuffer 类型的字节数据,offlen 只是为了定位传入的字节数组中关于目标类的字节数据,通常分别是 0 和字节数组的长度,毕竟专门构造一个包含无关数据的字节数组很无聊。
      • -
      • ProtectionDomain,保护域,todo:
      • -
      • sourceCodeSource 的位置。
      • +
          +
        1. Class 文件的常量池就是一张表,虚拟机根据索引去查找类名、字段名及其类型,方法名及其参数类型和字面量等。
        2. +
        3. 当类被加载到虚拟机之后,Class 文件中的常量池中的信息就进入到了运行时常量池。
        4. +
        5. 这个过程其实就是信息从文件进入了内存。
        6. +
        +

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

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

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

        +

        直接内存

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

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

        -

        URLClassLoader#defineClass

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

        -
        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 方法,此方法以字节数组的形式返回字节数据。

        -
        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 修饰。

        -
        private synchronized InputStream cachedInputStream() throws IOException {
        if (cis == null) {
        cis = getInputStream();
        }
        return cis;
        }
        +

        NIO 和 IO 的拷贝性能

        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);
        }
        }
        -

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

        -
        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();
        };
        +
        io 用时 1676.9797
        directBuffer 用时 836.4796
        -

        SecureClassLoader#defineClass

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

        -
        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 方法。

        -
        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的权限。

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

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

        -
        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;
        }
        +

        直接内存溢出

        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);
        }
        }
        -

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

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

        ProtectionDomain 的相关构造器参数:

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

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

        -

        ClassLoader#defineClass

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

        -
        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;
        }
        +

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

        +

        直接内存释放的原理

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

        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. preDefineClass 前置处理
        2. -
        3. defineClassX
        4. -
        5. postDefineClass 后置处理
        6. -
        -
        preDefineClass

        确定保护域 ProtectionDomain,并检查:

        -
          -
        1. 未定义 java.*
        2. -
        3. 该类的签名者与包(package)中其余类的签名者相匹配
        4. -
        -
        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;
        }
        +

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

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

        +
        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);
        }
        }
        }
        -
        defineClassSourceLocation

        确定 ClassCodeSource 位置。

        -
        private String defineClassSourceLocation(ProtectionDomain pd)
        {
        CodeSource cs = pd.getCodeSource();
        String source = null;
        if (cs != null && cs.getLocation() != null) {
        source = cs.getLocation().toString();
        }
        return source;
        }
        +

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

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

        +

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

        +
        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;
        }
        -
        defineClassX 方法

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

        -
        postDefineClass

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

        -
        private void postDefineClass(Class<?> c, ProtectionDomain pd)
        {
        if (pd.getCodeSource() != null) {
        // 获取证书
        Certificate certs[] = pd.getCodeSource().getCertificates();
        if (certs != null)
        setSigners(c, certs);
        }
        }
        ]]> +

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

        +
        public void run() {
        if (address == 0) {
        // Paranoia
        return;
        }
        unsafe.freeMemory(address);
        address = 0;
        Bits.unreserveMemory(size, capacity);
        }
        +]]> Java - ClassLoader + jvm @@ -1325,13 +1272,71 @@ - Java 垃圾收集 - /2023/11/07/garbage-collection-in-Java/ - -

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

        - -

        概述

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

        -
          + 基于 Redis 的分布式锁的简单实现 + /2023/11/13/simple-implementation-of-distributed-lock-based-on-Redis/ + 在分布式应用中,并发访问资源需要谨慎考虑。比如读取和修改保存并不是一个原子操作,在并发时,就可能发生修改的结果被覆盖的问题。

          + + +

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

          +

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

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

          +
          > setnx lock:user1 true
          OK

          ... do something

          > del lock:user1
          (integer) 1
          + +

          死锁问题

          问题一:异常引发死锁 1

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

          + + +

          改进一:设置超时时间

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

          +
          > 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 组合成一个原子指令。

          +
          > set lock:user1 true ex 5 nx
          OK

          ... do something

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

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

          +
          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 的过期时间,锁将在处理结束前被释放,就可能发生问题。

          + + +

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

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

          +

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

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

          +

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

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

          +

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

          +

          改进二:加锁时指定 tag

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

          +
          基于原生指令的实现
          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 脚本的实现
          if redis.call("get", KEYS[1]) == ARGV[1] then
          return redis.call("del", KEYS[1])
          else
          return 0
          end
          + +

          可重入性

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

          +

          Redis 分布式锁如果要支持可重入,可以使用线程的 ThreadLocal 变量存储当前持有的锁计数。

          +
          private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);
          + +

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

          +

          参考文章

            +
          • 《Redis 深度历险,核心原理与应用实践》
          • +
          +]]>
          + + distributed lock + redis + + + + Java 垃圾收集 + /2023/11/07/garbage-collection-in-Java/ + +

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

          + +

          概述

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

          +
          • 哪些内存需要回收?
          • 什么时候回收?
          • 如何回收?
          • @@ -1670,148 +1675,201 @@ - JVM 内存区域的测试和分析 - /2023/11/04/testing-and-analysis-of-jvm-memory-area/ - 内存区域

            JVM 内存区域划分为:

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

            程序计数器

            虚拟机栈

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

            -

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

            -

            栈内存溢出

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

            不正确的递归调用

            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();
            }
            }
            - + Java 类加载器源码分析 + /2023/07/13/Java-class-loader-source-code-analysis/ + 组织类加载工作:loadClass

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

            +
            public Class<?> loadClass(String name) throws ClassNotFoundException {
            return loadClass(name, false);
            }
            -

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

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

            -
            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;
            }
            }
            }
            +

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

            +
            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;
            }
            }
            -

            局部变量的线程安全问题

              -
            1. 局部变量如果未逃离方法的作用范围,就是线程安全的。
            2. -
            3. 局部变量如果是引用类型且逃离了方法的作用范围,就是线程不安全的。
            4. +

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

              +
                +
              1. 调用 findLoadedClass(String) 以检查该类是否已加载。
              2. +
              3. 在父·类加载器上调用 loadClass 方法。如果父·类加载器为空,则使用虚拟机内置的类加载器。
              4. +
              5. 调用 findClass(String) 方法来查找该类。
              -
              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;
              }
              }
              +

              如果使用上述步骤找到了该类(找到并定义类),并且解析标志为 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 对象本身。
              这个方案意味着非同名的目标类可以认为在加载时没有冲突?

              +
              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;
              }
              -

              线程问题排查

              CPU 占用率居高不下

              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 占用率居高不下时,可以尝试以下步骤:

              +
              什么是 “ClassLoader 对象注册为具有并行能力”呢?

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

              +
              static {
              ClassLoader.registerAsParallelCapable();
              }
              + +

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

                -
              1. top,定位 cpu 占用高的进程 id。
              2. -
              3. ps H -eo pid,tid,%cpu | grep pid,进一步定位引起 cpu 占用高的线程 id。
              4. -
              5. jstack pid,根据线程 id 换算成 16进制的 nid 找到对应线程,进一步定位到问题的源码行号。
              6. +
              7. 尚未创建调用者的实例(类加载器尚未实例化)
              8. +
              9. 调用者的所有超类(Object 类除外)都注册为具有并行能力。
              -
              "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. 对于第一个条件,可以通过将调用的代码写在 static 代码块中来实现。如果写在构造器方法里,并且通过单例模式保证只实例化一次可以吗?答案是不行的,后续会解释这个“注册”行为在构造器方法中是如何被使用以及为何不能写在构造器方法里。
              2. +
              3. 对于第二个条件,由于 Java 虚拟机加载类时,总是会先尝试加载其父类,又因为加载类时会先调用 static 代码块,因此父类的 static 代码块总是先于子类的 static 代码块。
              4. +
              +

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

              +
              注册工作做了什么?

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

              +
              @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);
              }
              -

              死锁,迟迟未返回结果

              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();
              }
              }
              +

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

              +
              // 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 提供了三个构造器方法:

              +
              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: 不同类加载器同一个类名会如何判定?

              +
              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. jstack pid,会显示找到死锁,以及死锁涉及的线程,,并各自持有的锁还有等待的锁。
              2. -
              3. 其他工具如 jconsole 也具有检测死锁的功能。
              4. +
              5. AppClassLoader 调用 loadClass 方法,先委派给 ExtClassLoader
              6. +
              7. ExtClassLoader 调用 loadClass 方法,先委派给 bootstrap class loader
              8. +
              9. bootstrap class loader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
              10. +
              11. ExtClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
              12. +
              13. ExtClassLoader 在其设置的类路径中无法找到 BananaTest 类,抛出 ClassNotFoundException 异常。
              14. +
              15. AppClassLoader 捕获异常,然后自己调用 findClass 方法尝试进行加载。
              -

              本地方法栈

              堆(Heap)的特点:

              +

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

              +

              查找类资源:findClass

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

              +

              过程如下:

                -
              1. 线程共享,需要考虑线程安全问题。
              2. -
              3. 存在垃圾回收机制。
              4. -
              5. 使用 -Xmx8m 设置大小。
              6. +
              7. name 转换为 path,比如 com.example.BananaTest 转换为 com/example/BananaTest.class
              8. +
              9. 使用 URL 搜索路径 URLClassPathpath 中获取 Resource,本质上就是轮流将可能存放的目录列表拼接上文件路径进行查找。
              10. +
              11. 调用 URLClassLoader 的私有方法 defineClass,该方法调用父类 SecureClassLoaderdefineClass 方法。
              -

              堆内存溢出

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

              -
              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);
              }
              }
              }
              +
              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;
              }
              -
              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
              +

              查找类的目录列表:URLClassPath

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

              +
              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;
              }
              -

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

              -

              监测堆内存

                -
              1. 使用 jps 查看 Java 进程列表
              2. -
              3. 使用 jmap -heap pid 查看堆内存信息
              4. -
              5. 还可以使用 jconsole 观察堆内存变化曲线
              6. -
              7. 还可以使用 VisualVM 查看堆信息
              8. +
                URLClassPath#getResource

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

                +
                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 中获取。

                +
                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. +
                3. 如果 index 超过范围,说明已有的 Loader 都找不到目标 Resource,需要到未打开的 URL 中查找。
                4. +
                5. 从未打开的 URL 中取出(pop)一个来创建 Loader,如果 urls 已经为空,则返回 null
                -
                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);
                }
                }
                +
                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);
                }
                -

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

                -
                Eden Space:
                capacity = 268435456 (256.0MB)
                used = 32212360 (30.72010040283203MB)

                used = 42698136 (40.720115661621094MB)

                used = 5368728 (5.120018005371094MB)
                +
                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. +
                3. 如果 URL/ 结尾,且协议为 file,则返回 FileLoader 类型,比如 file:/C:/Users/xxx/IdeaProjects/java-test/target/classes/
                4. +
                5. 如果 URL/ 结尾,且协议不会 file,则返回 Loader 类型。
                6. +
                +
                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();
                }
                }
                -

                使用 jconsole 查看堆内存信息:

                - +

                URLClassPath.FileLoader#getResource

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

                +
                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;
                }
                -

                堆内存占用居高不下

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

                -
                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 功能:

                - +

                ClassLoader 的搜索路径

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

                +

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

                +
                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
                -

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

                -

                方法区

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

                +
                bootstrap class loader

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

                  -
                • JDK 8 之前,方法区的具体实现为永久代,使用堆内存,使用 -XX:MaxPermSize=10m 设置大小。
                • -
                • JDK 8 开始,方法区的具体实现为元空间,使用直接内存,使用 -XX:MaxMetaspaceSize=10m 设置大小。
                • +
                • -Xbootclasspath::完全覆盖核心类库的类路径,不常用,除非重写核心类库。
                • +
                • -Xbootclasspath/a: 以后缀的方式拼接在原搜索路径后面,常用。
                • +
                • -Xbootclasspath/p: 以前缀的方式拼接再原搜索路径前面.不常用,避免引起不必要的冲突。
                -

                方法区溢出

                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. -
                3. 实验中抛出 java.lang.OutOfMemoryError: Compressed class space。
                4. -
                5. 添加参数 -XX:-UseCompressedClassPointers 后,抛出 java.lang.OutOfMemoryError: Metaspace。
                6. -
                7. JDK 6 设置 -XX:MaxPermSize=8m,抛出 java.lang.OutOfMemoryError: PermGen space。
                8. -
                -

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

                -

                运行时常量池

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

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

                使用 javap 反编译

                public class MethodAreaTest_2 {  

                public static void main(String[] args) {
                System.out.println("hello world");
                }
                }
                +

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

                +
                Error occurred during initialization of VM
                java/lang/NoClassDefFoundError: java/lang/Object

                进程已结束,退出代码1
                -
                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"
                +
                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 方法。

                +
                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);
                -
                  -
                1. Class 文件的常量池就是一张表,虚拟机根据索引去查找类名、字段名及其类型,方法名及其参数类型和字面量等。
                2. -
                3. 当类被加载到虚拟机之后,Class 文件中的常量池中的信息就进入到了运行时常量池。
                4. -
                5. 这个过程其实就是信息从文件进入了内存。
                6. -
                -

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

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

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

                -

                直接内存

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

                  其方法参数有:

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

                  NIO 和 IO 的拷贝性能

                  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);
                  }
                  }
                  +

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

                  +

                  URLClassLoader#defineClass

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

                  +
                  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);
                  }
                  }
                  -
                  io 用时 1676.9797
                  directBuffer 用时 836.4796
                  +

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

                  +
                  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 修饰。

                  +
                  private synchronized InputStream cachedInputStream() throws IOException {
                  if (cis == null) {
                  cis = getInputStream();
                  }
                  return cis;
                  }
                  - +

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

                  +
                  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 类型的参数。

                  +
                  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));
                  }
                  -

                  直接内存溢出

                  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);
                  }
                  }
                  +

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

                  +
                  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;
                  }
                  -
                  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
                  +
                  getPermissions

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

                  +

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

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

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

                  -

                  直接内存释放的原理

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

                  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();
                  }
                  }
                  +

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

                  +
                  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;
                  }
                  -

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

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

                  -
                  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);
                  }
                  }
                  }
                  +

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

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

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

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

                  -

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

                  -
                  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;
                  }
                  +
                  ProtectionDomain

                  ProtectionDomain 的相关构造器参数:

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

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

                  +

                  ClassLoader#defineClass

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

                  +
                  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;
                  }
                  -

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

                  -
                  public void run() {
                  if (address == 0) {
                  // Paranoia
                  return;
                  }
                  unsafe.freeMemory(address);
                  address = 0;
                  Bits.unreserveMemory(size, capacity);
                  }
                  -]]> +

                  主要步骤:

                  +
                    +
                  1. preDefineClass 前置处理
                  2. +
                  3. defineClassX
                  4. +
                  5. postDefineClass 后置处理
                  6. +
                  +
                  preDefineClass

                  确定保护域 ProtectionDomain,并检查:

                  +
                    +
                  1. 未定义 java.*
                  2. +
                  3. 该类的签名者与包(package)中其余类的签名者相匹配
                  4. +
                  +
                  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 位置。

                  +
                  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 使用。

                  +
                  private void postDefineClass(Class<?> c, ProtectionDomain pd)
                  {
                  if (pd.getCodeSource() != null) {
                  // 获取证书
                  Certificate certs[] = pd.getCodeSource().getCertificates();
                  if (certs != null)
                  setSigners(c, certs);
                  }
                  }
                  ]]> Java - jvm + ClassLoader diff --git a/tags/ClassLoader/index.html b/tags/ClassLoader/index.html index 8ef5aca0..e4caf2bb 100644 --- a/tags/ClassLoader/index.html +++ b/tags/ClassLoader/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

                  ClassLoader - 37k + 38k
                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/Java/index.html b/tags/Java/index.html index 4e9054b0..802aa46b 100644 --- a/tags/Java/index.html +++ b/tags/Java/index.html @@ -152,13 +152,13 @@ @@ -351,13 +351,13 @@

                  Java - 37k + 38k

                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/OpenVPN/index.html b/tags/OpenVPN/index.html index f1a741aa..6ece3d0a 100644 --- a/tags/OpenVPN/index.html +++ b/tags/OpenVPN/index.html @@ -152,13 +152,13 @@ @@ -291,13 +291,13 @@

                  OpenVPN - 37k + 38k

                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/Ubuntu/index.html b/tags/Ubuntu/index.html index db899760..718ab612 100644 --- a/tags/Ubuntu/index.html +++ b/tags/Ubuntu/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

                  Ubuntu - 37k + 38k

                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/bytecode/index.html b/tags/bytecode/index.html index 6e9beee4..1197af34 100644 --- a/tags/bytecode/index.html +++ b/tags/bytecode/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

                  bytecode - 37k + 38k

                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/clash/index.html b/tags/clash/index.html index c544310b..0d32eff2 100644 --- a/tags/clash/index.html +++ b/tags/clash/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

                  clash - 37k + 38k

                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/distributed-lock/index.html b/tags/distributed-lock/index.html new file mode 100644 index 00000000..b5d87b97 --- /dev/null +++ b/tags/distributed-lock/index.html @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +标签: distributed lock | Moralok + + + + + + + + + + + +
                  + +
                  +
                  +
                  + + + + + +
                  + + + + + + + +
                  + +
                  + +
                  + + + + + + + + +
                  + + +
                  + + 0% +
                  + + + + +
                  + + + + + +
                  +
                  +
                  +

                  distributed lock + 标签 +

                  +
                  + + +
                  + 2023 +
                  + + + +
                  +
                  + + + + +
                  +
                  + +
                  + +
                  + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/docker/index.html b/tags/docker/index.html index 1a288e61..a489e2f0 100644 --- a/tags/docker/index.html +++ b/tags/docker/index.html @@ -152,13 +152,13 @@ @@ -274,13 +274,13 @@

                  docker - 37k + 38k

                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/index.html b/tags/index.html index 9e91ed92..b88574dc 100644 --- a/tags/index.html +++ b/tags/index.html @@ -27,7 +27,7 @@ - + @@ -155,13 +155,13 @@ @@ -209,10 +209,10 @@

                  tags
                  @@ -246,13 +246,13 @@

                  tags - 37k + 38k

                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/jvm/index.html b/tags/jvm/index.html index 3a8746e2..b0974a06 100644 --- a/tags/jvm/index.html +++ b/tags/jvm/index.html @@ -152,13 +152,13 @@ @@ -311,13 +311,13 @@

                  jvm - 37k + 38k

                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/minikube/index.html b/tags/minikube/index.html index a2307342..7e0a6919 100644 --- a/tags/minikube/index.html +++ b/tags/minikube/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

                  minikube - 37k + 38k

                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/proxy/index.html b/tags/proxy/index.html index 9c04fad4..fa89f944 100644 --- a/tags/proxy/index.html +++ b/tags/proxy/index.html @@ -152,13 +152,13 @@ @@ -271,13 +271,13 @@

                  proxy - 37k + 38k

                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/redis/index.html b/tags/redis/index.html new file mode 100644 index 00000000..62a6f7ad --- /dev/null +++ b/tags/redis/index.html @@ -0,0 +1,290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +标签: redis | Moralok + + + + + + + + + + + +
                  + +
                  +
                  +
                  + + + + + +
                  + + + + + + + +
                  + +
                  + +
                  + + + + + + + + +
                  + + +
                  + + 0% +
                  + + + + +
                  + + + + + +
                  +
                  +
                  +

                  redis + 标签 +

                  +
                  + + +
                  + 2023 +
                  + + + +
                  +
                  + + + + +
                  +
                  + +
                  + +
                  + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tags/ssh/index.html b/tags/ssh/index.html index aabd2686..0f742d21 100644 --- a/tags/ssh/index.html +++ b/tags/ssh/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

                  ssh - 37k + 38k

                  Hexo & NexT.Muse 强力驱动 diff --git a/tags/tmux/index.html b/tags/tmux/index.html index 6124e322..275d6600 100644 --- a/tags/tmux/index.html +++ b/tags/tmux/index.html @@ -152,13 +152,13 @@ @@ -251,13 +251,13 @@

                  tmux - 37k + 38k

                  Hexo & NexT.Muse 强力驱动