diff --git a/2020/08/19/docker-frequently-used-commands/index.html b/2020/08/19/docker-frequently-used-commands/index.html index a787afa5..d9251165 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 @@ - + @@ -237,7 +237,7 @@

更新于 - + @@ -774,7 +774,7 @@

- 1:30 + 1:31
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 6ed316c3..230a8f7b 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 @@ - + @@ -238,7 +238,7 @@

- + @@ -371,7 +371,7 @@

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 93b09946..a7c31781 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 @@ - + @@ -237,7 +237,7 @@

- + @@ -352,7 +352,7 @@

- 1:30 + 1:31

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 11d37ba7..dd16fbf3 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 @@ - + @@ -237,7 +237,7 @@

- + @@ -357,7 +357,7 @@

- 1:30 + 1:31

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 1ac5b395..582576c5 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 @@ - + @@ -237,7 +237,7 @@

- + @@ -384,7 +384,7 @@

- 1:30 + 1:31

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 8d1d5668..49ef00e6 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,7 +27,7 @@ - + @@ -238,7 +238,7 @@

- + @@ -378,7 +378,7 @@

- 1:30 + 1:31

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 7064e0a6..1c1cc48d 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 @@ - + @@ -237,7 +237,7 @@

- + @@ -386,7 +386,7 @@

- 1:30 + 1:31

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 0d734a40..c68bafcf 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 @@ - + @@ -237,7 +237,7 @@

- + @@ -347,7 +347,7 @@

- 1:30 + 1:31

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 8f1fd8cf..fb2608e0 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 @@ - + @@ -237,7 +237,7 @@

- + @@ -370,7 +370,7 @@

- 1:30 + 1:31

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 29fdbb57..d06ed98e 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 @@ - + @@ -237,7 +237,7 @@

- + @@ -390,7 +390,7 @@

- 1:30 + 1:31

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 528900c6..8c2699ea 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 @@ - + @@ -238,7 +238,7 @@

- + @@ -524,7 +524,7 @@

- 1:30 + 1:31
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 23046451..e1a50224 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 @@ -19,15 +19,15 @@ - + - + - + @@ -144,7 +144,7 @@ @@ -273,68 +273,71 @@

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

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

// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {

}
}
+

堆的组成

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

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

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

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

堆的组成

    +

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

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

    空间占比

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

    -

    从地址指针分析空间

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

    +

    堆空间的比例

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

    +

    从内存地址分析堆空间

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

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

    -
    1
    2
    3
    4
    public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
    }
    +

    堆的垃圾回收

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

    GC 类型

      +

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

      +

      GC 类型

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

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

      -

      from 和 to 的角色变换

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

      -
      1
      2
      3
      4
      5
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_7MB]);
      list.add(new byte[_512KB]);
      }
      +

      from 和 to 的角色变换

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

      +

      eden 空间足够时不发生 GC

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

      eden 空间足够时不发生 GC

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

      -
      1
      2
      3
      4
      5
      6
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_7MB]);
      list.add(new byte[_512KB]);
      list.add(new byte[_512KB]);
      }
      +

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

      +

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

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

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

      在第三次添加时,由于 eden 空间不足,因此又发生了第二次垃圾回收。
      [DefNew: 8565K->512K(9216K), 0.0046378 secs],新生代的空间占用下降到了 512K。
      在第二次添加完成后,eden [0x00000000fec00000, 0x00000000ff3bd8d0)from [0x00000000ff500000, 0x00000000ff5b45f0) 占用的空间为 8116432 + 738800 = 8855232 约 8647.7K,略大于 8565K,在垃圾回收后新生代剩下 512K,应该是在 from 中留下了第二次添加时的 512K。
      8565K->8396K(19456K), 0.0046540 secs,堆的占用空间并未发生明显下降,部分对象是因为新生代空间不足,提前晋升到了老年代中。
      优先将大对象先晋升到老年代应该是一种晋升策略。
      eden 中加入了第三次添加时的对象,但远大于 512K。此时 edenfromtenured 中均有不好确认成分的空间占用。

      -
      1
      2
      3
      4
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB]);
      }
      +

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

      +

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

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

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

      +

      内存不足 OOM

      1
      2
      3
      4
      5
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB]);
      list.add(new byte[_8MB]);
      }
      -

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

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

      -
      1
      2
      3
      4
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB - 16]);
      }
      - -
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      [GC (Allocation Failure) [DefNew: 2013K->693K(9216K), 0.0015517 secs] 2013K->693K(19456K), 0.0015828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
      [GC (Allocation Failure) [DefNew: 8885K->0K(9216K), 0.0048110 secs] 8885K->8885K(19456K), 0.0048264 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
      Heap
      def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66958, 0x00000000ff400000)
      from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      tenured generation total 10240K, used 8885K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffead580, 0x00000000ffead600, 0x0000000100000000)
      Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 354K, capacity 388K, committed 512K, reserved 1048576K
      - +
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      waiting...
      [GC (Allocation Failure) [DefNew: 4711K->928K(9216K), 0.0017245 secs][Tenured: 8192K->9117K(10240K), 0.0021690 secs] 12903K->9117K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0039336 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      [Full GC (Allocation Failure) [Tenured: 9117K->9063K(10240K), 0.0014352 secs] 9117K->9063K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0014614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
      at com.moralok.jvm.gc.JvmGcTest.lambda$main$0(JvmGcTest.java:27)
      at com.moralok.jvm.gc.JvmGcTest$$Lambda$1/2003749087.run(Unknown Source)
      at java.lang.Thread.run(Thread.java:750)

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

      大对象的划分指标

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

      -
      1
      2
      3
      4
      5
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB]);
      list.add(new byte[_8MB]);
      }
      +

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

      +

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

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

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

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

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

      +

      大对象的划分指标

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

      +
      1
      2
      3
      4
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB - 16]);
      }
      -

      内存不足

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

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

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

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

      Heap
      def new generation total 9216K, used 1502K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 18% used [0x00000000fec00000, 0x00000000fed77a00, 0x00000000ff400000)
      from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      tenured generation total 10240K, used 9063K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 88% used [0x00000000ff600000, 0x00000000ffed9c50, 0x00000000ffed9e00, 0x0000000100000000)
      Metaspace used 4787K, capacity 4884K, committed 4992K, reserved 1056768K
      class space used 522K, capacity 558K, committed 640K, reserved 1048576K
      +
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      [GC (Allocation Failure) [DefNew: 2013K->693K(9216K), 0.0015517 secs] 2013K->693K(19456K), 0.0015828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
      [GC (Allocation Failure) [DefNew: 8885K->0K(9216K), 0.0048110 secs] 8885K->8885K(19456K), 0.0048264 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
      Heap
      def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66958, 0x00000000ff400000)
      from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      tenured generation total 10240K, used 8885K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffead580, 0x00000000ffead600, 0x0000000100000000)
      Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 354K, capacity 388K, committed 512K, reserved 1048576K
      -

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

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

      +

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

      +

      注意事项

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

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

@@ -397,7 +400,7 @@

- 1:30 + 1:31

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 047dc3fa..7cad828c 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 @@ - + @@ -243,7 +243,7 @@

- + @@ -443,7 +443,7 @@

- 1:30 + 1:31

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 43f40783..ec1c414c 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 @@ - + @@ -244,7 +244,7 @@

- + @@ -474,7 +474,7 @@

- 1:30 + 1:31

Hexo & NexT.Muse 强力驱动 diff --git a/archives/2020/08/index.html b/archives/2020/08/index.html index 950ac5d8..be2f5012 100644 --- a/archives/2020/08/index.html +++ b/archives/2020/08/index.html @@ -256,7 +256,7 @@

Moralok

- 1:30 + 1:31
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2020/index.html b/archives/2020/index.html index 487ae582..fbc987c2 100644 --- a/archives/2020/index.html +++ b/archives/2020/index.html @@ -256,7 +256,7 @@

Moralok

- 1:30 + 1:31
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/05/index.html b/archives/2023/05/index.html index e5f86300..572baeea 100644 --- a/archives/2023/05/index.html +++ b/archives/2023/05/index.html @@ -256,7 +256,7 @@

Moralok

- 1:30 + 1:31
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/06/index.html b/archives/2023/06/index.html index 692fc1fe..b6362592 100644 --- a/archives/2023/06/index.html +++ b/archives/2023/06/index.html @@ -396,7 +396,7 @@

Moralok

- 1:30 + 1:31
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/07/index.html b/archives/2023/07/index.html index 0a8ff067..14c46519 100644 --- a/archives/2023/07/index.html +++ b/archives/2023/07/index.html @@ -256,7 +256,7 @@

Moralok

- 1:30 + 1:31
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/11/index.html b/archives/2023/11/index.html index b4078531..f04230e0 100644 --- a/archives/2023/11/index.html +++ b/archives/2023/11/index.html @@ -296,7 +296,7 @@

Moralok

- 1:30 + 1:31
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/index.html b/archives/2023/index.html index 7170ff67..a50d1dec 100644 --- a/archives/2023/index.html +++ b/archives/2023/index.html @@ -439,7 +439,7 @@

Moralok

- 1:30 + 1:31
Hexo & NexT.Muse 强力驱动 diff --git a/archives/2023/page/2/index.html b/archives/2023/page/2/index.html index 174e74cb..98953bd0 100644 --- a/archives/2023/page/2/index.html +++ b/archives/2023/page/2/index.html @@ -299,7 +299,7 @@

Moralok

- 1:30 + 1:31
Hexo & NexT.Muse 强力驱动 diff --git a/archives/index.html b/archives/index.html index b615b921..8c16fb90 100644 --- a/archives/index.html +++ b/archives/index.html @@ -439,7 +439,7 @@

Moralok

- 1:30 + 1:31
Hexo & NexT.Muse 强力驱动 diff --git a/archives/page/2/index.html b/archives/page/2/index.html index 76fe1678..f7065314 100644 --- a/archives/page/2/index.html +++ b/archives/page/2/index.html @@ -322,7 +322,7 @@

Moralok

- 1:30 + 1:31
Hexo & NexT.Muse 强力驱动 diff --git a/css/main.css b/css/main.css index a3fa7e1c..d1900a92 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: #5b572b; + background: #78dae7; display: inline-block; margin-right: 3px; transform: translateY(-2px); diff --git a/index.html b/index.html index 2b894ace..2fa5ba70 100644 --- a/index.html +++ b/index.html @@ -232,7 +232,7 @@

- + @@ -465,7 +465,7 @@

- + @@ -665,7 +665,7 @@

- + @@ -682,14 +682,14 @@

- 2.2k + 2.5k

@@ -700,68 +700,71 @@

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

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

// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {

}
}
+

堆的组成

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

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

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

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

堆的组成

@@ -824,7 +827,7 @@

更新于 - + @@ -1110,7 +1113,7 @@

更新于 - + @@ -1264,7 +1267,7 @@

更新于 - + @@ -1398,7 +1401,7 @@

更新于 - + @@ -1509,7 +1512,7 @@

更新于 - + @@ -1659,7 +1662,7 @@

更新于 - + @@ -1799,7 +1802,7 @@

更新于 - + @@ -1933,7 +1936,7 @@

- 1:30 + 1:31
Hexo & NexT.Muse 强力驱动 diff --git a/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json index 807a5625..fbe80099 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":"在 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":"使用 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 server 20.04 安装后没有分配全部磁盘空间","url":"/2023/06/24/Ubuntu-server-20-04-not-all-disk-space-was-allocated-after-installation/"},{"title":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"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":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"}] \ No newline at end of file +[{"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":"Docker 常用命令列表","url":"/2020/08/19/docker-frequently-used-commands/"},{"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 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":"如何在 Ubuntu 20.04 上安装 Minikube","url":"/2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/"},{"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/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 a5de92fb..d200b96b 100644 --- a/page/2/index.html +++ b/page/2/index.html @@ -232,7 +232,7 @@

- + @@ -348,7 +348,7 @@

- + @@ -469,7 +469,7 @@

- + @@ -602,7 +602,7 @@

- + @@ -1129,7 +1129,7 @@

- 1:30 + 1:31

Hexo & NexT.Muse 强力驱动 diff --git a/search.xml b/search.xml index f34756d6..864c9921 100644 --- a/search.xml +++ b/search.xml @@ -1,5 +1,64 @@ + + 在 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/ + 安装 OpenVPN server

OpenVPN 社区 下载 Windows 64-bit MSI installer。本次安装的版本为 OpenVPN 2.6.4。

+
+

注意事项:在选择安装类型时选择 Customize 而不要选择 Install Now。额外勾选 OpenVPN -> OpenVPN Service -> Entire feature will be installed on local hard drive 和 OpenSSL Utilities -> EasyRSA 3 Certificate Management Scripts -> Entire feature will be installed on local hard drive。

+
+

安装完毕后,会弹出一条消息提示未找到可读的连接配置文件,暂时忽略。
此时在 控制面板\网络和 Internet\网络连接 中可以看到创建了两个新的网络适配器 OpenVPN TAP-Windows6 和 OpenVPN Wintun。

+

配置 OpenVPN server

打开 Windows 10 终端程序。
进入 OpenVPN 默认安装目录中的 easy-rsa 目录。

+
cd 'C:\Program Files\OpenVPN\easy-rsa'
+

执行命令进入 Easy-RSA 3 Shell

+
.\EasyRSA-Start.bat
+

初始化公钥基础设施目录 pki

+
./easyrsa init-pki
+

构建证书颁发机构(CA)密钥,CA 根证书文件将在后续用于对其他证书和密钥进行签名。该命令要求输入 Common Name,输入主机名即可。创建的 ca.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki 中,ca.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

+
./easyrsa build-ca nopass
+

构建服务器证书和密钥。创建的 server.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,server.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

+
./easyrsa build-server-full server nopass
+

构建客户端证书和密钥。创建的 client.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,client.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

+
./easyrsa build-client-full client nopass
+

生成 Diffie-Hellman 参数

+
./easyrsa gen-dh
+

从目录 C:\Program Files\OpenVPN\sample-config 复制服务端配置文件模板 server.ovpn 到目录 C:\Program Files\OpenVPN\config 中,修改以下配置:

+
port 1194
dh dh.pem
duplicate-cn
;tls-auth ta.key 0
+

端口号按需修改,默认为1194,需要保证 OpenVPN 的网络流量可以通过防火墙,设置 Windows 10 Defender 允许 OpenVPN 通过即可。dh2048.pem 修改为生成的文件名 dh.pem。取消注释 duplicate-cn,让多个客户端使用同一个客户端证书。注释掉 tls-auth ta.key 0。复制 ca.crt,dh.pem,server.crt 和 server.key 到目录 C:\Program Files\OpenVPN\config 中。

+

启动与连接

启动 OpenVPN,点击连接,系统提示分配 IP 10.8.0.1。按配置,每次 OpenVPN server 都将为自己分配 10.8.0.1。

+

参考链接

openvpn安装配置说明(windows系统)
如何在Windows 10上安装和配置OpenVPN

+]]>
+ + OpenVPN + +
在 Ubuntu 上安装 Clash /2023/05/27/how-to-install-clash-on-ubuntu/ @@ -497,65 +556,6 @@ 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/ - 安装 OpenVPN server

OpenVPN 社区 下载 Windows 64-bit MSI installer。本次安装的版本为 OpenVPN 2.6.4。

-
-

注意事项:在选择安装类型时选择 Customize 而不要选择 Install Now。额外勾选 OpenVPN -> OpenVPN Service -> Entire feature will be installed on local hard drive 和 OpenSSL Utilities -> EasyRSA 3 Certificate Management Scripts -> Entire feature will be installed on local hard drive。

-
-

安装完毕后,会弹出一条消息提示未找到可读的连接配置文件,暂时忽略。
此时在 控制面板\网络和 Internet\网络连接 中可以看到创建了两个新的网络适配器 OpenVPN TAP-Windows6 和 OpenVPN Wintun。

-

配置 OpenVPN server

打开 Windows 10 终端程序。
进入 OpenVPN 默认安装目录中的 easy-rsa 目录。

-
cd 'C:\Program Files\OpenVPN\easy-rsa'
-

执行命令进入 Easy-RSA 3 Shell

-
.\EasyRSA-Start.bat
-

初始化公钥基础设施目录 pki

-
./easyrsa init-pki
-

构建证书颁发机构(CA)密钥,CA 根证书文件将在后续用于对其他证书和密钥进行签名。该命令要求输入 Common Name,输入主机名即可。创建的 ca.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki 中,ca.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

-
./easyrsa build-ca nopass
-

构建服务器证书和密钥。创建的 server.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,server.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

-
./easyrsa build-server-full server nopass
-

构建客户端证书和密钥。创建的 client.crt 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\issued 中,client.key 保存在目录 C:\Program Files\OpenVPN\easy-rsa\pki\private 中。

-
./easyrsa build-client-full client nopass
-

生成 Diffie-Hellman 参数

-
./easyrsa gen-dh
-

从目录 C:\Program Files\OpenVPN\sample-config 复制服务端配置文件模板 server.ovpn 到目录 C:\Program Files\OpenVPN\config 中,修改以下配置:

-
port 1194
dh dh.pem
duplicate-cn
;tls-auth ta.key 0
-

端口号按需修改,默认为1194,需要保证 OpenVPN 的网络流量可以通过防火墙,设置 Windows 10 Defender 允许 OpenVPN 通过即可。dh2048.pem 修改为生成的文件名 dh.pem。取消注释 duplicate-cn,让多个客户端使用同一个客户端证书。注释掉 tls-auth ta.key 0。复制 ca.crt,dh.pem,server.crt 和 server.key 到目录 C:\Program Files\OpenVPN\config 中。

-

启动与连接

启动 OpenVPN,点击连接,系统提示分配 IP 10.8.0.1。按配置,每次 OpenVPN server 都将为自己分配 10.8.0.1。

-

参考链接

openvpn安装配置说明(windows系统)
如何在Windows 10上安装和配置OpenVPN

-]]>
- - OpenVPN - -
使用 OpenVPN 访问家庭内网 /2023/06/07/how-to-use-OpenVPN-to-access-home-network/ @@ -689,67 +689,6 @@ Ubuntu - - 如何在 Ubuntu 20.04 上安装 Minikube - /2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/ - 环境搭建

安装 Ubuntu 20.04

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

- -

安装 Docker

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

-

安装 Minikube

参考官方文档:minikube start

-

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

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

-

开始使用

创建集群:

-
minikube start
- -

创建集群时的权限问题

不加 sudo

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

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

-
$ 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 启动集群,成功启动集群但是提示使用该选项可能会引发未知行为。

-
$ 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 测试,提示连接被拒绝。

-
$ 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 用户组后,正常启动集群。

-
$ 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 下载镜像也很困难,还可以通过指定镜像仓库启动集群。可以通过查看帮助内关于仓库的信息,获取官方建议中国大陆用户使用的镜像仓库地址。

-
$ 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 控制台

启动控制台:

-
$ 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 外,你也许想要在宿主机的浏览器访问:

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

使用过程中下载镜像失败

从其他镜像仓库下载代替

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

-
$ 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 容器配置代理。搜索到以下命令满足需求:

-
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
minikube image load <IMAGE_NAME>
- -
minikube image build
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镜像的简单方式

-]]>
- - minikube - -
如何使用 SSH 连接 Github 和服务器 /2023/06/28/how-to-use-ssh-to-connect-github-and-server/ @@ -860,71 +799,135 @@ tmux + + 如何在 Ubuntu 20.04 上安装 Minikube + /2023/06/23/how-to-install-Minikube-on-Ubuntu-20-04/ + 环境搭建

安装 Ubuntu 20.04

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

+ +

安装 Docker

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

+

安装 Minikube

参考官方文档:minikube start

+

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

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

+

开始使用

创建集群:

+
minikube start
+ +

创建集群时的权限问题

不加 sudo

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

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

+
$ 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 启动集群,成功启动集群但是提示使用该选项可能会引发未知行为。

+
$ 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 测试,提示连接被拒绝。

+
$ 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 用户组后,正常启动集群。

+
$ 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 下载镜像也很困难,还可以通过指定镜像仓库启动集群。可以通过查看帮助内关于仓库的信息,获取官方建议中国大陆用户使用的镜像仓库地址。

+
$ 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 控制台

启动控制台:

+
$ 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 外,你也许想要在宿主机的浏览器访问:

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

使用过程中下载镜像失败

从其他镜像仓库下载代替

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

+
$ 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 容器配置代理。搜索到以下命令满足需求:

+
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
minikube image load <IMAGE_NAME>
+ +
minikube image build
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镜像的简单方式

+]]>
+ + minikube + +
JVM GC 的测试和分析 /2023/11/01/testing-and-analysis-of-jvm-gc/ -
public class JvmGcTest {

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

// -Xms20M -Xmx20M -Xmn10M -XX:+UseSerialGC -XX:+PrintGCDetails -verbose:gc
public static void main(String[] args) {

}
}
+ 堆的组成
public class JvmGcTest_1 {

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

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

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

堆的组成

    +

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

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

    空间占比

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

    -

    从地址指针分析空间

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

    +

    堆空间的比例

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

    +

    从内存地址分析堆空间

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

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

    -
    public static void main(String[] args) {
    List<byte[]> list = new ArrayList<>();
    list.add(new byte[_7MB]);
    }
    +

    堆的垃圾回收

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

    GC 类型

      +

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

      +

      GC 类型

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

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

      -

      from 和 to 的角色变换

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

      -
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_7MB]);
      list.add(new byte[_512KB]);
      }
      +

      from 和 to 的角色变换

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

      +

      eden 空间足够时不发生 GC

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

      eden 空间足够时不发生 GC

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

      -
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_7MB]);
      list.add(new byte[_512KB]);
      list.add(new byte[_512KB]);
      }
      +

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

      +

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

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

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

      在第三次添加时,由于 eden 空间不足,因此又发生了第二次垃圾回收。
      [DefNew: 8565K->512K(9216K), 0.0046378 secs],新生代的空间占用下降到了 512K。
      在第二次添加完成后,eden [0x00000000fec00000, 0x00000000ff3bd8d0)from [0x00000000ff500000, 0x00000000ff5b45f0) 占用的空间为 8116432 + 738800 = 8855232 约 8647.7K,略大于 8565K,在垃圾回收后新生代剩下 512K,应该是在 from 中留下了第二次添加时的 512K。
      8565K->8396K(19456K), 0.0046540 secs,堆的占用空间并未发生明显下降,部分对象是因为新生代空间不足,提前晋升到了老年代中。
      优先将大对象先晋升到老年代应该是一种晋升策略。
      eden 中加入了第三次添加时的对象,但远大于 512K。此时 edenfromtenured 中均有不好确认成分的空间占用。

      -
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB]);
      }
      +

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

      +

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

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

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

      +

      内存不足 OOM

      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB]);
      list.add(new byte[_8MB]);
      }
      -

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

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

      -
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB - 16]);
      }
      - -
      [GC (Allocation Failure) [DefNew: 2013K->693K(9216K), 0.0015517 secs] 2013K->693K(19456K), 0.0015828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
      [GC (Allocation Failure) [DefNew: 8885K->0K(9216K), 0.0048110 secs] 8885K->8885K(19456K), 0.0048264 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
      Heap
      def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66958, 0x00000000ff400000)
      from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      tenured generation total 10240K, used 8885K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffead580, 0x00000000ffead600, 0x0000000100000000)
      Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 354K, capacity 388K, committed 512K, reserved 1048576K
      - +
      waiting...
      [GC (Allocation Failure) [DefNew: 4711K->928K(9216K), 0.0017245 secs][Tenured: 8192K->9117K(10240K), 0.0021690 secs] 12903K->9117K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0039336 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      [Full GC (Allocation Failure) [Tenured: 9117K->9063K(10240K), 0.0014352 secs] 9117K->9063K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0014614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
      at com.moralok.jvm.gc.JvmGcTest.lambda$main$0(JvmGcTest.java:27)
      at com.moralok.jvm.gc.JvmGcTest$$Lambda$1/2003749087.run(Unknown Source)
      at java.lang.Thread.run(Thread.java:750)

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

      大对象的划分指标

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

      -
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB]);
      list.add(new byte[_8MB]);
      }
      +

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

      +

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

      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();

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

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

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

      +

      大对象的划分指标

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

      +
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();
      list.add(new byte[_8MB - 16]);
      }
      -

      内存不足

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

      -
      public static void main(String[] args) {
      List<byte[]> list = new ArrayList<>();

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

      System.out.println("waiting...");
      try {
      System.in.read();
      } catch (IOException e) {
      throw new RuntimeException(e);
      }
      }
      - -
      waiting...
      [GC (Allocation Failure) [DefNew: 4711K->928K(9216K), 0.0017245 secs][Tenured: 8192K->9117K(10240K), 0.0021690 secs] 12903K->9117K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0039336 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      [Full GC (Allocation Failure) [Tenured: 9117K->9063K(10240K), 0.0014352 secs] 9117K->9063K(19456K), [Metaspace: 4267K->4267K(1056768K)], 0.0014614 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
      Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
      at com.moralok.jvm.gc.JvmGcTest.lambda$main$0(JvmGcTest.java:27)
      at com.moralok.jvm.gc.JvmGcTest$$Lambda$1/2003749087.run(Unknown Source)
      at java.lang.Thread.run(Thread.java:750)

      Heap
      def new generation total 9216K, used 1502K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 18% used [0x00000000fec00000, 0x00000000fed77a00, 0x00000000ff400000)
      from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      tenured generation total 10240K, used 9063K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 88% used [0x00000000ff600000, 0x00000000ffed9c50, 0x00000000ffed9e00, 0x0000000100000000)
      Metaspace used 4787K, capacity 4884K, committed 4992K, reserved 1056768K
      class space used 522K, capacity 558K, committed 640K, reserved 1048576K
      +
      [GC (Allocation Failure) [DefNew: 2013K->693K(9216K), 0.0015517 secs] 2013K->693K(19456K), 0.0015828 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
      [GC (Allocation Failure) [DefNew: 8885K->0K(9216K), 0.0048110 secs] 8885K->8885K(19456K), 0.0048264 secs] [Times: user=0.00 sys=0.02, real=0.00 secs]
      Heap
      def new generation total 9216K, used 410K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
      eden space 8192K, 5% used [0x00000000fec00000, 0x00000000fec66958, 0x00000000ff400000)
      from space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
      to space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
      tenured generation total 10240K, used 8885K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
      the space 10240K, 86% used [0x00000000ff600000, 0x00000000ffead580, 0x00000000ffead600, 0x0000000100000000)
      Metaspace used 3321K, capacity 4496K, committed 4864K, reserved 1056768K
      class space used 354K, capacity 388K, committed 512K, reserved 1048576K
      -

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

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

      +

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

      +

      注意事项

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

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

      ]]> Java @@ -1037,6 +1040,151 @@
    • 请别再拿“String s = new String(“xyz”);创建了多少个String实例”来面试了吧
    • JVM中字符串常量池StringTable在内存中形式分析
    • +]]> + + Java + jvm + + + + 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();
      }
      }
      + + +

      循环引用导致 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;
      }
      }
      }
      + +

      局部变量的线程安全问题

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

      线程问题排查

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

      死锁,迟迟未返回结果

      public class ThreadTest_2 {

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

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

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

      本地方法栈

      堆(Heap)的特点:

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

      堆内存溢出

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

      +
      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);
      }
      }
      }
      + +
      java.lang.OutOfMemoryError: Java heap space
      at java.util.Arrays.copyOf(Arrays.java:3332)
      at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
      at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
      at java.lang.StringBuilder.append(StringBuilder.java:141)
      at com.moralok.jvm.memory.heap.HeapTest_1.main(HeapTest_1.java:21)
      运行次数 17
      + +

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

      +

      监测堆内存

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

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

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

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

      used = 42698136 (40.720115661621094MB)

      used = 5368728 (5.120018005371094MB)
      + +

      使用 jconsole 查看堆内存信息:

      + + +

      堆内存占用居高不下

      当你发现堆内存占用居高不下,经过 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 功能:

      + + +

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

      +

      方法区

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

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

      方法区溢出

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

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

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

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

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

      +

      直接内存

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

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

      直接内存溢出

      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);
      }
      }
      + +
      java.lang.OutOfMemoryError: Direct buffer memory
      at java.nio.Bits.reserveMemory(Bits.java:695)
      at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
      at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
      at com.moralok.jvm.memory.direct.DirectMemoryTest_2.main(DirectMemoryTest_2.java:16)
      145
      + +

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

      +

      直接内存释放的原理

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

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

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

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

      +
      public class DirectMemoryTest_4 {

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

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

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

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

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

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

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

      +

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

      +
      DirectByteBuffer(int cap) {

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

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

      +
      public void run() {
      if (address == 0) {
      // Paranoia
      return;
      }
      unsafe.freeMemory(address);
      address = 0;
      Bits.unreserveMemory(size, capacity);
      }
      ]]>
      Java @@ -1241,149 +1389,4 @@ ClassLoader
      - - 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();
      }
      }
      - - -

      循环引用导致 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;
      }
      }
      }
      - -

      局部变量的线程安全问题

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

      线程问题排查

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

      死锁,迟迟未返回结果

      public class ThreadTest_2 {

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

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

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

      本地方法栈

      堆(Heap)的特点:

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

      堆内存溢出

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

      -
      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);
      }
      }
      }
      - -
      java.lang.OutOfMemoryError: Java heap space
      at java.util.Arrays.copyOf(Arrays.java:3332)
      at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
      at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:448)
      at java.lang.StringBuilder.append(StringBuilder.java:141)
      at com.moralok.jvm.memory.heap.HeapTest_1.main(HeapTest_1.java:21)
      运行次数 17
      - -

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

      -

      监测堆内存

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

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

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

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

      used = 42698136 (40.720115661621094MB)

      used = 5368728 (5.120018005371094MB)
      - -

      使用 jconsole 查看堆内存信息:

      - - -

      堆内存占用居高不下

      当你发现堆内存占用居高不下,经过 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 功能:

      - - -

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

      -

      方法区

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

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

      方法区溢出

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

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

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

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

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

      -

      直接内存

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

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

      直接内存溢出

      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);
      }
      }
      - -
      java.lang.OutOfMemoryError: Direct buffer memory
      at java.nio.Bits.reserveMemory(Bits.java:695)
      at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)
      at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:311)
      at com.moralok.jvm.memory.direct.DirectMemoryTest_2.main(DirectMemoryTest_2.java:16)
      145
      - -

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

      -

      直接内存释放的原理

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

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

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

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

      -
      public class DirectMemoryTest_4 {

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

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

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

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

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

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

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

      -

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

      -
      DirectByteBuffer(int cap) {

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

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

      -
      public void run() {
      if (address == 0) {
      // Paranoia
      return;
      }
      unsafe.freeMemory(address);
      address = 0;
      Bits.unreserveMemory(size, capacity);
      }
      -]]>
      - - Java - jvm - -
      diff --git a/tags/ClassLoader/index.html b/tags/ClassLoader/index.html index c4c84bef..a0fb635a 100644 --- a/tags/ClassLoader/index.html +++ b/tags/ClassLoader/index.html @@ -257,7 +257,7 @@

      ClassLoader - 1:30 + 1:31
      Hexo & NexT.Muse 强力驱动 diff --git a/tags/Java/index.html b/tags/Java/index.html index 9de6cdfa..5f19daa0 100644 --- a/tags/Java/index.html +++ b/tags/Java/index.html @@ -317,7 +317,7 @@

      Java - 1:30 + 1:31

      Hexo & NexT.Muse 强力驱动 diff --git a/tags/OpenVPN/index.html b/tags/OpenVPN/index.html index 5d79eda1..4d611d78 100644 --- a/tags/OpenVPN/index.html +++ b/tags/OpenVPN/index.html @@ -297,7 +297,7 @@

      OpenVPN - 1:30 + 1:31

      Hexo & NexT.Muse 强力驱动 diff --git a/tags/Ubuntu/index.html b/tags/Ubuntu/index.html index e1d6644b..b08e06a0 100644 --- a/tags/Ubuntu/index.html +++ b/tags/Ubuntu/index.html @@ -257,7 +257,7 @@

      Ubuntu - 1:30 + 1:31

      Hexo & NexT.Muse 强力驱动 diff --git a/tags/clash/index.html b/tags/clash/index.html index 6d009ba0..e9e9a6d5 100644 --- a/tags/clash/index.html +++ b/tags/clash/index.html @@ -257,7 +257,7 @@

      clash - 1:30 + 1:31

      Hexo & NexT.Muse 强力驱动 diff --git a/tags/docker/index.html b/tags/docker/index.html index 664a831b..b7e17388 100644 --- a/tags/docker/index.html +++ b/tags/docker/index.html @@ -280,7 +280,7 @@

      docker - 1:30 + 1:31

      Hexo & NexT.Muse 强力驱动 diff --git a/tags/index.html b/tags/index.html index 006bac5b..059bf186 100644 --- a/tags/index.html +++ b/tags/index.html @@ -27,7 +27,7 @@ - + @@ -252,7 +252,7 @@

      tags - 1:30 + 1:31

      Hexo & NexT.Muse 强力驱动 diff --git a/tags/jvm/index.html b/tags/jvm/index.html index dbc3ee4b..b87779dc 100644 --- a/tags/jvm/index.html +++ b/tags/jvm/index.html @@ -297,7 +297,7 @@

      jvm - 1:30 + 1:31

      Hexo & NexT.Muse 强力驱动 diff --git a/tags/minikube/index.html b/tags/minikube/index.html index 9220f101..4e825842 100644 --- a/tags/minikube/index.html +++ b/tags/minikube/index.html @@ -257,7 +257,7 @@

      minikube - 1:30 + 1:31

      Hexo & NexT.Muse 强力驱动 diff --git a/tags/proxy/index.html b/tags/proxy/index.html index 2be7e341..3436af80 100644 --- a/tags/proxy/index.html +++ b/tags/proxy/index.html @@ -277,7 +277,7 @@

      proxy - 1:30 + 1:31

      Hexo & NexT.Muse 强力驱动 diff --git a/tags/ssh/index.html b/tags/ssh/index.html index a7552da1..9ab64984 100644 --- a/tags/ssh/index.html +++ b/tags/ssh/index.html @@ -257,7 +257,7 @@

      ssh - 1:30 + 1:31

      Hexo & NexT.Muse 强力驱动 diff --git a/tags/tmux/index.html b/tags/tmux/index.html index 283ed19e..3571bacc 100644 --- a/tags/tmux/index.html +++ b/tags/tmux/index.html @@ -257,7 +257,7 @@

      tmux - 1:30 + 1:31

      Hexo & NexT.Muse 强力驱动