diff --git a/2020/08/19/docker-frequently-used-commands/index.html b/2020/08/19/docker-frequently-used-commands/index.html index 003b9df8..9d9b38f1 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 @@

更新于 - + 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 c7c0310d..d968961e 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 @@

更新于 - + 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 f0b28d47..cd5fd817 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 @@

更新于 - + 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 e40ea6bd..1319fa1e 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 @@

更新于 - + 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 79ef594b..cff3380f 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 @@

更新于 - + 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 0d18b7db..fff92c69 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 @@

更新于 - + 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 a5c1f7aa..78a60de8 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 @@

更新于 - + 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 260c6924..01491594 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 @@

更新于 - + 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 c60b8638..df33eb9c 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 @@

更新于 - + diff --git a/2023/06/29/tmux-frequently-used-commands/index.html b/2023/06/29/tmux-frequently-used-commands/index.html index 5c5c96db..2108b44a 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 @@

更新于 - + 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 ee65aca4..2919bafa 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 @@

更新于 - + 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 c9c5d1c0..1741384b 100644 --- a/2023/11/01/testing-and-analysis-of-jvm-gc/index.html +++ b/2023/11/01/testing-and-analysis-of-jvm-gc/index.html @@ -27,7 +27,7 @@ - + @@ -238,7 +238,7 @@

更新于 - + 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 69106622..e3838d8f 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 @@

更新于 - + 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 3a4b767e..4b9faafc 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 @@

更新于 - + diff --git a/2023/11/07/garbage-collection-in-Java/index.html b/2023/11/07/garbage-collection-in-Java/index.html index 64da0658..c2dcae5d 100644 --- a/2023/11/07/garbage-collection-in-Java/index.html +++ b/2023/11/07/garbage-collection-in-Java/index.html @@ -34,7 +34,7 @@ - + @@ -246,7 +246,7 @@

更新于 - + diff --git a/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html b/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html index 54ada396..e52cd297 100644 --- a/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html +++ b/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/index.html @@ -28,7 +28,7 @@ - + @@ -240,7 +240,7 @@

更新于 - + diff --git a/css/main.css b/css/main.css index 94b5e9c9..dc61c598 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: #66e8ee; + background: #6eb8c3; display: inline-block; margin-right: 3px; transform: translateY(-2px); diff --git a/index.html b/index.html index f1e23ebc..f7aa6b4c 100644 --- a/index.html +++ b/index.html @@ -232,7 +232,7 @@

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

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

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

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

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

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

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

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

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

更新于 - + diff --git a/leancloud_counter_security_urls.json b/leancloud_counter_security_urls.json index d2e85419..77979134 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":"Java 类加载器源码分析","url":"/2023/07/13/Java-class-loader-source-code-analysis/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"title":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"},{"title":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"},{"title":"关于 Java 字节码指令的一些例子分析","url":"/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/"}] \ No newline at end of file +[{"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":"使用 OpenVPN 访问家庭内网","url":"/2023/06/07/how-to-use-OpenVPN-to-access-home-network/"},{"title":"在 Windows 10 上安装 OpenVPN 服务器","url":"/2023/06/07/how-to-setup-OpenVPN-server-on-windows-10/"},{"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":"Java 类加载器源码分析","url":"/2023/07/13/Java-class-loader-source-code-analysis/"},{"title":"JVM 内存区域的测试和分析","url":"/2023/11/04/testing-and-analysis-of-jvm-memory-area/"},{"title":"字符串常量池的测试和分析","url":"/2023/11/03/testing-and-analysis-of-StringTable/"},{"title":"关于 Java 字节码指令的一些例子分析","url":"/2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/"},{"title":"Java 垃圾收集","url":"/2023/11/07/garbage-collection-in-Java/"}] \ No newline at end of file diff --git a/page/2/index.html b/page/2/index.html index d9edc44b..9b3858ea 100644 --- a/page/2/index.html +++ b/page/2/index.html @@ -232,7 +232,7 @@

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

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

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

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

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

更新于 - + diff --git a/search.xml b/search.xml index 33f72db4..abc996be 100644 --- a/search.xml +++ b/search.xml @@ -519,38 +519,6 @@

路由器 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 @@ -610,6 +578,38 @@
route add -net 10.8.0.0/24 gw 192.168.46.1

这是为了让 OpenVPN 服务端的其他私有子网上的设备知道来自 10.8.0.0/24 的 IP Packet 应该路由回 OpenVPN 服务端。

参考链接

透过openvpn来访问内网资源
OpenVPN 路由详解
OpenVPN中的另一个路由问题 - 在VPN上无法访问本地计算机
openvpn添加本地路由表
windows开启路由转发
linux route命令的使用详解
Windows命令行route命令使用图解

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

-

字符串常量池的作用

字符串字面量

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

-
String s1 = "a";
- -

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

-

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

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

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

-

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

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

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

常量池的分类

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

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

-
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab

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



0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
- -

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

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

- - -

验证字符串常量池的位置

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

-
public class StringTableTest_8 {  

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

在 JDK 8 中异常如下:

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

在 JDK 6 中异常如下:

-
java.lang.OutOfMemoryError: PermGen space
- -

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

-

关于 intern 的实验

public class StringTableTest_5 {  

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

String x = "ab";

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

public class StringTableTest_6 {

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

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

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

-

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

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

- - -

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

-

进入字符串常量池的时机

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

-
public class StringTableTest_12 {

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

RednaxelaFX 的文章提到:

-
-

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

-
-

xinxi 的文章中补充到:

-
-

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

-
-

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

-
-

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

-
-

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

-

验证延迟实例化

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

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

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

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

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

垃圾回收

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

-
public class StringTableTest_9 {  

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


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

性能优化

调整 buckets size

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

-
public class StringTableTest_10 {  

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

主动运用 intern 的场景

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

-
public class StringTableTest_11 {  

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

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

- - -

字符串拼接

变量的拼接

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

-
public class StringTableTest_2 {  

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

常量的拼接

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

-
public class StringTableTest_3 {

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

参考文章

    -
  1. Java 中new String(“字面量”) 中 “字面量” 是何时进入字符串常量池的? - xinxi的回答 - 知乎
  2. -
  3. 请别再拿“String s = new String(“xyz”);创建了多少个String实例”来面试了吧
  4. -
  5. JVM中字符串常量池StringTable在内存中形式分析
  6. -
-]]>
- - Java - jvm - -
JVM 内存区域的测试和分析 /2023/11/04/testing-and-analysis-of-jvm-memory-area/ @@ -1389,6 +1277,198 @@ jvm + + 字符串常量池的测试和分析 + /2023/11/03/testing-and-analysis-of-StringTable/ + 如果你准备过 Java 的面试,应该看到过一个问题:“String s1 = new String("abc"); 这个语句创建了几个字符串对象”。这个问题曾经困扰我,当时的我不能理解这个问题想要考察的是什么?
答案中或许提及了字符串常量池,但是如果细究起来,会发现答案并不完善,有些令人困惑,甚至问题本身就有一定的误导作用。它很容易让初学者以为创建一个字符串对象和创建一个其他类型的对象在过程上是有一些区别的。
其实关键的地方在于 “abc” 而不是 new String("abc")

+

字符串常量池的作用

字符串字面量

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

+
String s1 = "a";
+ +

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

+

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

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

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

+

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

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

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

常量池的分类

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

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

+
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = String #25 // a
#3 = String #26 // b
#4 = String #27 // ab

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



0: ldc #2 // String a
2: astore_1
3: ldc #3 // String b
5: astore_2
6: ldc #4 // String ab
8: astore_3
9: return
+ +

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

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

+ + +

验证字符串常量池的位置

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

+
public class StringTableTest_8 {  

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

在 JDK 8 中异常如下:

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

在 JDK 6 中异常如下:

+
java.lang.OutOfMemoryError: PermGen space
+ +

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

+

关于 intern 的实验

public class StringTableTest_5 {  

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

String x = "ab";

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

public class StringTableTest_6 {

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

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

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

+

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

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

+ + +

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

+

进入字符串常量池的时机

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

+
public class StringTableTest_12 {

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

RednaxelaFX 的文章提到:

+
+

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

+
+

xinxi 的文章中补充到:

+
+

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

+
+

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

+
+

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

+
+

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

+

验证延迟实例化

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

+
    +
  1. 直到第一次运行到字符串字面量时,才会创建对应的字符串对象。
  2. +
  3. 相同的字符串常量,不会重复创建字符串对象。
  4. +
+
public class StringTableTest_4 {  

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

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

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

垃圾回收

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

+
public class StringTableTest_9 {  

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


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

性能优化

调整 buckets size

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

+
public class StringTableTest_10 {  

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

主动运用 intern 的场景

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

+
public class StringTableTest_11 {  

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

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

+ + +

字符串拼接

变量的拼接

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

+
public class StringTableTest_2 {  

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

常量的拼接

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

+
public class StringTableTest_3 {

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

参考文章

    +
  1. Java 中new String(“字面量”) 中 “字面量” 是何时进入字符串常量池的? - xinxi的回答 - 知乎
  2. +
  3. 请别再拿“String s = new String(“xyz”);创建了多少个String实例”来面试了吧
  4. +
  5. JVM中字符串常量池StringTable在内存中形式分析
  6. +
+]]>
+ + Java + jvm + +
+ + 关于 Java 字节码指令的一些例子分析 + /2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/ + 演示字节码指令的执行
public class ByteCodeTest_2 {

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

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

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

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

本地变量表

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

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

运行时常量池

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

字节码指令

 0: bipush        10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
+ +

分析 a++ 和 ++a

public class ByteCodeTest_3 {

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

字节码指令

 0: bipush        10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
+ + +

分析判断条件

public class ByteCodeTest_4 {

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

字节码指令

 0: iconst_0
1: istore_1
2: iload_1
3: ifne 12
6: bipush 10
8: istore_1
9: goto 15
12: bipush 20
14: istore_1
15: return
+ + +

涉及的字节码指令

+]]>
+ + Java + bytecode + +
Java 垃圾收集 /2023/11/07/garbage-collection-in-Java/ @@ -1734,84 +1814,4 @@ jvm - - 关于 Java 字节码指令的一些例子分析 - /2023/11/09/some-examples-of-Java-bytecode-instruction-analysis/ - 演示字节码指令的执行
public class ByteCodeTest_2 {

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

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

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

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

本地变量表

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

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

运行时常量池

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

字节码指令

 0: bipush        10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
- -

分析 a++ 和 ++a

public class ByteCodeTest_3 {

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

字节码指令

 0: bipush        10
2: istore_1
3: iload_1
4: iinc 1, 1
7: iinc 1, 1
10: iload_1
11: iadd
12: iload_1
13: iinc 1, -1
16: iadd
17: istore_2
- - -

分析判断条件

public class ByteCodeTest_4 {

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

字节码指令

 0: iconst_0
1: istore_1
2: iload_1
3: ifne 12
6: bipush 10
8: istore_1
9: goto 15
12: bipush 20
14: istore_1
15: return
- - -

涉及的字节码指令

-]]>
- - Java - bytecode - -
diff --git a/tags/index.html b/tags/index.html index 157d755b..266293a5 100644 --- a/tags/index.html +++ b/tags/index.html @@ -27,7 +27,7 @@ - +