-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
304 lines (168 loc) · 207 KB
/
Copy pathatom.xml
File metadata and controls
304 lines (168 loc) · 207 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>大水怪</title>
<subtitle>一只专注技术的水怪</subtitle>
<link href="https://wtmonster.github.io/atom.xml" rel="self"/>
<link href="https://wtmonster.github.io/"/>
<updated>2025-12-11T16:00:00.000Z</updated>
<id>https://wtmonster.github.io/</id>
<author>
<name>WtMonster</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>JDK25-Leyden</title>
<link href="https://wtmonster.github.io/2024/10/20/2024-10-20-jdk25-leyden/"/>
<id>https://wtmonster.github.io/2024/10/20/2024-10-20-jdk25-leyden/</id>
<published>2024-10-19T16:00:00.000Z</published>
<updated>2025-12-11T16:00:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="Leyden相关进展"><a href="#Leyden相关进展" class="headerlink" title="Leyden相关进展"></a>Leyden相关进展</h1><h2 id="DONE"><a href="#DONE" class="headerlink" title="DONE"></a>DONE</h2><p><a href="https://openjdk.org/jeps/483">JEP 483: Ahead-of-Time Class Loading & Linking</a></p><p><a href="https://openjdk.org/jeps/514">JEP 514: Ahead-of-Time Command-Line Ergonomics</a>(暂时用处不大,其实就是把启动)</p><span id="more"></span><p><a href="https://openjdk.org/jeps/515">JEP 515: Ahead-of-Time Method Profiling</a></p><p><a href="https://bugs.openjdk.org/browse/JDK-8352437">Support –add-exports with -XX:+AOTClassLinking</a></p><p><a href="https://bugs.openjdk.org/browse/JDK-8352003">Support –add-opens with -XX:+AOTClassLinking</a></p><h2 id="DOING"><a href="#DOING" class="headerlink" title="DOING"></a>DOING</h2><p><a href="https://openjdk.org/jeps/516">Ahead-of-Time Object Caching with Any GC</a></p><p><a href="https://openjdk.org/jeps/8335368">JEP draft: Ahead-of-Time Code Compilation</a></p><h1 id="核心JEP解读"><a href="#核心JEP解读" class="headerlink" title="核心JEP解读"></a>核心JEP解读</h1><h2 id="JEP-483"><a href="#JEP-483" class="headerlink" title="JEP 483"></a>JEP 483</h2><p><a href="https://openjdk.org/jeps/483">JEP 483: Ahead-of-Time Class Loading & Linking</a></p><p>存储类加载和链接的训练数据,但当前只支持jdk内置的class loader:System, Extension, & Boot loaders</p><p>tips:</p><p>对lambda等的优化会比较大</p><h2 id="JEP-515"><a href="#JEP-515" class="headerlink" title="JEP 515"></a>JEP 515</h2><p><a href="https://openjdk.org/jeps/515">JEP 515: Ahead-of-Time Method Profiling</a></p><p>存储方法的调用数据(方法执行次数等),以便JIT了解方法的执行情况,可以直接进行对应层级的编译优化</p><p>tips:</p><p>这样做需要注意,由于JIT已知方法的执行情况,理想情况下可以认为热点path代码可以在首次触达时即完成JIT优化,这对CPU资源的开销可能会比较大</p><p>也就是说,如果直接接入峰值流量,可能启动初期消耗的CPU资源会较高,可能依然需要慢启动的流程(不过流量放大的速度可以比现在快很多)</p><p>未来<a href="https://openjdk.org/jeps/8335368">JEP draft: Ahead-of-Time Code Compilation</a>完成后,上述问题会得到解决</p><h2 id="JEP514"><a href="#JEP514" class="headerlink" title="JEP514"></a>JEP514</h2><p><a href="https://openjdk.org/jeps/514">JEP 514: Ahead-of-Time Command-Line Ergonomics</a></p><p>作用是把开始训练/生成训练数据的两个命令行合并成一个命令(在应用结束的时候起一个子进程运行生成训练数据的命令)</p><p>目前存在一些问题,生成训练数据时候的启动参数是和启动应用时一样的。但在应用容器这样的启动方式时,可能存在像-Dcom.sun.management.jmxremote.port=1099这样的启动参数,会因端口冲突导致生成训练数据的步骤失败。</p><h1 id="目前最大的问题"><a href="#目前最大的问题" class="headerlink" title="目前最大的问题"></a>目前最大的问题</h1><h2 id="只支持java内置的classloader"><a href="#只支持java内置的classloader" class="headerlink" title="只支持java内置的classloader"></a>只支持java内置的classloader</h2><p>BootClassLoader</p><p><a href="https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/lang/ClassLoader.html#getPlatformClassLoader()">PlatformClassLoader</a></p><p><a href="https://docs.oracle.com/en/java/javase/22/docs/api/java.base/java/lang/ClassLoader.html#getSystemClassLoader()">AppClassLoader</a></p><p>可能会在未来放开这一限制</p><p>Spring所做的jar包展开本质上也是更换了classLoader(非nestedJar的情况下不再需要使用自定义的classLoader)</p><h2 id="不支持ZGC"><a href="#不支持ZGC" class="headerlink" title="不支持ZGC"></a>不支持ZGC</h2><p>未来会支持</p><h1 id="Leyden和Native-Image的区别"><a href="#Leyden和Native-Image的区别" class="headerlink" title="Leyden和Native Image的区别"></a>Leyden和Native Image的区别</h1><p>Leyden相比Native Image而言,不会砍掉JVM的动态性。对开发而言可以做到基本无感。</p><p><strong>以下是一些详细说明:</strong></p><p>Leyden项目不打算直接合并GraalVM的工作,而是参考一部分Graal思路的同时,在项目开始时保留一部分运行时特征</p><p><a href="https://www.infoq.com/news/2022/06/project-leyden-delays-aot/">https://www.infoq.com/news/2022/06/project-leyden-delays-aot/</a></p><p>这种转向在Leyden项目的一部分问答中有所体现</p><p><a href="https://mail.openjdk.org/pipermail/discuss/2020-April/005429.html">https://mail.openjdk.org/pipermail/discuss/2020-April/005429.html</a></p><p><a href="https://mail.openjdk.org/pipermail/leyden-dev/2022-May/000005.html">https://mail.openjdk.org/pipermail/leyden-dev/2022-May/000005.html</a></p><p>官方描述中也可以看出</p><p><a href="https://openjdk.org/projects/leyden/notes/01-beginnings">https://openjdk.org/projects/leyden/notes/01-beginnings</a></p><p>其他</p><p><a href="https://vived.io/how-committing-graalvm-to-openjdk-changes-the-rules-for-project-leyden-jvm-weekly-110/">https://vived.io/how-committing-graalvm-to-openjdk-changes-the-rules-for-project-leyden-jvm-weekly-110/</a></p><h1 id="需要关注的工作"><a href="#需要关注的工作" class="headerlink" title="需要关注的工作"></a>需要关注的工作</h1><p>jar包展开</p><p><a href="https://spring.io/blog/2024/08/29/spring-boot-cds-support-and-project-leyden-anticipation">https://spring.io/blog/2024/08/29/spring-boot-cds-support-and-project-leyden-anticipation</a></p><p><a href="https://docs.spring.io/spring-boot/reference/packaging/efficient.html">https://docs.spring.io/spring-boot/reference/packaging/efficient.html</a></p><p><a href="https://docs.spring.io/spring-boot/docs/3.2.9/reference/html/executable-jar.html#appendix.executable-jar.nested-jars">https://docs.spring.io/spring-boot/docs/3.2.9/reference/html/executable-jar.html#appendix.executable-jar.nested-jars</a></p>]]></content>
<summary type="html"><h1 id="Leyden相关进展"><a href="#Leyden相关进展" class="headerlink" title="Leyden相关进展"></a>Leyden相关进展</h1><h2 id="DONE"><a href="#DONE" class="headerlink" title="DONE"></a>DONE</h2><p><a href="https://openjdk.org/jeps/483">JEP 483: Ahead-of-Time Class Loading &amp; Linking</a></p>
<p><a href="https://openjdk.org/jeps/514">JEP 514: Ahead-of-Time Command-Line Ergonomics</a>(暂时用处不大,其实就是把启动)</p></summary>
<category term="Java" scheme="https://wtmonster.github.io/categories/Java/"/>
<category term="Java" scheme="https://wtmonster.github.io/tags/Java/"/>
<category term="JVM" scheme="https://wtmonster.github.io/tags/JVM/"/>
<category term="Leyden" scheme="https://wtmonster.github.io/tags/Leyden/"/>
<category term="AOT" scheme="https://wtmonster.github.io/tags/AOT/"/>
</entry>
<entry>
<title>JDK21-25重要特性解读</title>
<link href="https://wtmonster.github.io/2024/10/05/2024-10-05-jdk21-25-features/"/>
<id>https://wtmonster.github.io/2024/10/05/2024-10-05-jdk21-25-features/</id>
<published>2024-10-04T16:00:00.000Z</published>
<updated>2025-11-07T16:00:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="Loom(虚拟线程)"><a href="#Loom(虚拟线程)" class="headerlink" title="Loom(虚拟线程)"></a>Loom(虚拟线程)</h1><table><thead><tr><th>Release</th><th>JEP</th><th>Description</th></tr></thead></table><span id="more"></span><p>| 24 | <a href="https://openjdk.org/jeps/491">JEP 491: Synchronize Virtual Threads without Pinning</a> | 解决了虚拟线程在 synchronized上绑定的问题,虚拟线程从此可以大规模使用了(但类加载依然存在绑定问题,有极小概率死锁) |<br>| 25 | <a href="https://openjdk.org/jeps/506">JEP 506: Scoped Values</a> | 引入了一种全新的,基于显式作用域管理的值传递方式 |</p><h1 id="JVM相关"><a href="#JVM相关" class="headerlink" title="JVM相关"></a>JVM相关</h1><table><thead><tr><th>Release</th><th>JEP</th><th>Description</th></tr></thead><tbody><tr><td>25</td><td><a href="https://openjdk.org/jeps/519">JEP 519: Compact Object Headers</a></td><td>完成了Lilliput项目的第一阶段,将对象头从128位减小到了64位,下一阶段目标是减少到32位。据阿里之前的反馈,约节省7-8%的内存,具体效果和项目本身关系比较大。</td></tr><tr><td>25</td><td><a href="https://openjdk.org/jeps/521">JEP 521: Generational Shenandoah</a></td><td>分代ZGC正式生产可用了,但目前还没有specjbb与其他GC的对比数据<br></td></tr><tr><td>25</td><td><a href="https://openjdk.org/jeps/520">JEP 520: JFR Method Timing & Tracing</a></td><td>支持自定义方法维度的全量耗时tracing</td></tr><tr><td>23<br>24</td><td><a href="https://openjdk.org/jeps/474">JEP 474: ZGC: Generational Mode by Default</a><br><a href="https://openjdk.org/jeps/490">JEP 490: ZGC: Remove the Non-Generational Mode</a></td><td>ZGC以后只有分代模式了,实测认为绝大部分情况下分代ZGC优于单代ZGC(少部分不符合分代假说的极端情况除外,例如sofa jraft的ringbuffer)<br></td></tr></tbody></table><h1 id="语法-amp-core-lib"><a href="#语法-amp-core-lib" class="headerlink" title="语法&core-lib"></a>语法&core-lib</h1><table><thead><tr><th>Release</th><th>JEP</th><th>Description</th></tr></thead><tbody><tr><td>24</td><td><a href="https://openjdk.org/jeps/485">JEP 485: Stream Gatherers</a></td><td>提供了更灵活的stream流处理方式,并且提供了一些内置拓展<br><a href="https://inside.java/2025/04/03/javaone-stream-gatherers/">【inside.java】javaone-stream-gatherers</a></td></tr><tr><td>22</td><td><a href="https://openjdk.org/jeps/456">JEP 456: Unnamed Variables & Patterns</a></td><td>可以使用下划线’_‘来命名未使用的变量</td></tr><tr><td>23</td><td><a href="https://openjdk.org/jeps/467">JEP 467: Markdown Documentation Comments</a></td><td>注释支持Markdown格式</td></tr></tbody></table><h1 id="底层库"><a href="#底层库" class="headerlink" title="底层库"></a>底层库</h1><table><thead><tr><th>Release</th><th>JEP</th><th>Description</th></tr></thead><tbody><tr><td>22</td><td><a href="https://openjdk.org/jeps/454">JEP 454: Foreign Function & Memory API</a></td><td>提供标准的FFM API,用于调用其他语言的代码以及本地内存管理</td></tr><tr><td>24</td><td><a href="https://openjdk.org/jeps/484">JEP 484: Class-File API</a></td><td>提供用于解析、生成和转换 Java 类文件的标准 API</td></tr></tbody></table><h1 id="Leyden-相关"><a href="#Leyden-相关" class="headerlink" title="Leyden 相关"></a>Leyden 相关</h1><table><thead><tr><th>Release</th><th>JEP</th><th>Description</th></tr></thead><tbody><tr><td>24</td><td><a href="https://openjdk.org/jeps/483">JEP 483: Ahead-of-Time Class Loading & Linking</a></td><td>存储类加载和链接的训练数据,但当前只支持jdk内置的class loader: System, Extension, & Boot loaders</td></tr><tr><td>25</td><td><a href="https://openjdk.org/jeps/515">JEP 515: Ahead-of-Time Method Profiling</a></td><td>存储方法的调用数据(方法执行次数等),以便JIT了解方法的执行情况,可以直接进行对应层级的编译优化</td></tr></tbody></table>]]></content>
<summary type="html"><h1 id="Loom(虚拟线程)"><a href="#Loom(虚拟线程)" class="headerlink" title="Loom(虚拟线程)"></a>Loom(虚拟线程)</h1><table>
<thead>
<tr>
<th>Release</th>
<th>JEP</th>
<th>Description</th>
</tr>
</thead>
</table></summary>
<category term="Java" scheme="https://wtmonster.github.io/categories/Java/"/>
<category term="Java" scheme="https://wtmonster.github.io/tags/Java/"/>
<category term="JDK21" scheme="https://wtmonster.github.io/tags/JDK21/"/>
<category term="JVM" scheme="https://wtmonster.github.io/tags/JVM/"/>
<category term="JEP" scheme="https://wtmonster.github.io/tags/JEP/"/>
</entry>
<entry>
<title>JFR实战教程</title>
<link href="https://wtmonster.github.io/2024/09/22/2024-09-22-jfr-practice/"/>
<id>https://wtmonster.github.io/2024/09/22/2024-09-22-jfr-practice/</id>
<published>2024-09-21T16:00:00.000Z</published>
<updated>2025-10-17T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p><strong>本文基于JDK11,对于jdk14的 <a href="https://openjdk.java.net/jeps/349">JFR Event Streaming </a>以及JDK16引入的 <a href="https://bugs.openjdk.org/browse/JDK-8257602">jdk.ObjectAllocationSample </a>事件等不作过多讨论</strong></p><h1 id="什么是JFR"><a href="#什么是JFR" class="headerlink" title="什么是JFR"></a>什么是JFR</h1><p>JFR是 Java Flight Record(前身是JRocket Flight Record),是 JVM 内置的基于事件的JDK监控记录框架。这个起名就是参考了黑匣子对于飞机的作用,将Java进程比喻成飞机飞行。顾名思义,这个记录主要用于问题定位和持续监控。</p><span id="more"></span><p>官方描述 <a href="https://openjdk.org/jeps/328">https://openjdk.org/jeps/328</a></p><p>JFR基于事件思想的起源 <a href="https://openjdk.org/jeps/167">https://openjdk.org/jeps/167</a></p><p><a href="https://bestsolution-at.github.io/jfr-doc/openjdk-11.html">JFR事件大全</a></p><p>JFR是一个开销不超过1%(对比profiler官方声称对系统影响不超过2%,实测远远不止,因此不推荐在线上系统长期跑profiler),目标是用于线上持续监控的特性,在jdk11以前是一个商业特性,在2018年 Java Flight Recorder 开源并作为 OpenJDK 11 的一部分发布。</p><p>jdk8u262后jfr被反向移植</p><p><a href="https://mail.openjdk.org/pipermail/jdk8u-dev/2020-July/012143.html">https://mail.openjdk.org/pipermail/jdk8u-dev/2020-July/012143.html</a></p><p><a href="https://bugs.openjdk.org/browse/JDK-8223147">https://bugs.openjdk.org/browse/JDK-8223147</a></p><p><a href="https://hg.openjdk.org/jdk8u/jdk8u-jfr-incubator/">https://hg.openjdk.org/jdk8u/jdk8u-jfr-incubator/</a></p><p>这意味着在jdk8u262以后的版本也可以自由使用jfr了。</p><p>当然,在某些供应商的open jdk8中可能更早被免费开放,比如Azul的openjdk。</p><p>JFR,具有以下关键的特性: - 低开销(在配置正确的情况下),可在生产环境核心业务进程中始终在线运行。当然,也可以随时开启与关闭。</p><p>- 可以查看出问题时间段内进行分析,可以分析 Java 应用程序,JVM 内部以及当前Java进程运行环境等多因素。</p><p>- JFR基于事件采集,可以分析非常底层的信息,例如对象分配,方法采样与热点方法定位与调用堆栈,安全点分析与锁占用时长与堆栈分析,GC 相关分析以及 JIT 编译器相关分析(例如 CodeCache )</p><p>- 完善的 API 定义,用户可以自定义事件生产与消费。</p><h2 id="为什么要使用JFR"><a href="#为什么要使用JFR" class="headerlink" title="为什么要使用JFR"></a>为什么要使用JFR</h2><p><strong>存在的问题</strong></p><ol><li><p>目前通过 <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/jmx/index.html">JMX </a>对jvm的监控粒度比较粗,线上出现非业务逻辑问题后一般只能通过dump堆快照结合日志来进行分析,对jvm的详细把控只能通过dump来获得瞬时的状态进行分析(且dump操作需要拉出后再进行,可能dump到的数据并不准确)。</p></li><li><p>从业务层面看,目前业务线的代码业务逻辑比较复杂,且部分老代码存在可读性低的情况,常常难以直观感知到性能瓶颈。</p></li></ol><ol start="3"><li><p>从组件框架层面看,公司内部以及业务线的组件封装层次比较深,并且日常工作忙于需求开发,因此日常接触的层次主要集中在业务代码层面,导致难以定位到框架或组件代码中可能存在的性能瓶颈。</p></li><li><p>就业务研发团队与组件研发团队结合而言,组件开发人员感知集中在组件代码层面,容易忽视一些业务场景下的调用特征,可能导致一些业务场景独有的瓶颈。而业务开发人员的感知集中在业务层面,且由于业务复杂,框架封装层次深,难以直观感知到组件层面导致的瓶颈。导致二者难以形成整体的感知,只能各自为政。</p></li></ol><ol start="5"><li>目前对应用的优化以代码重构与架构重构为主,但重构代价大,周期长,往往难以推进。因此有时候通过消除代码层面的瓶颈来提升应用性能也是一个不错的选择。</li></ol><p><strong>使用JFR的好处</strong></p><ol><li><p>定位问题直达本质。jfr的思想是万物皆为event,这些event可以是jvm的事件,例如进入safepoint,stw,锁争用,内存分配,线程上下文切换等,也可以是自定义事件。这些event直接反映了应用在jvm和操作系统等层面的根本开销。并且jfr对各类事件进行聚合,通过火焰图等方式让开发者能够从jvm和操作系统层面反向定位到代码层面,极大程度上消除了代码水平逻辑复杂,调用深度复杂导致性能与开销瓶颈定位困难的问题,在推动jdk升级的背景下,对JVM有更全面的监控是十分必要的。</p></li><li><p>JFR监控开销很低。根据官方描述可以做到将开销控制在1%,是完全可以接受的。一些常用的监控手段,比如jstack和jmap,需要让JVM进入safepoint状态才能获取线程堆栈和内存使用情况等信息。而这样的操作会导致JVM暂停应用程序的执行,从而降低线上应用的性能。 对于JMX等外部监控来说存在网络开销的问题。JFR通过在JVM内部进行数据采集,可以实现非常低的性能开销。</p></li></ol><ol start="3"><li>使用JFR有助于形成对JVM更全面的认知。在日常开发中,JVM的情况往往是一个只暴露少数指标的黑盒,难以做到具体的感知。使用JFR有助于使用者形成对JVM更全面的认知。</li></ol><p><strong>JFR的使用场景</strong></p><ol><li><p>分析应用瓶颈。本地分析应用瓶颈可以通过jmeter模拟生产流量,或者在mirror中回放生产流量的方式来进行监控,这种方式下可以采用比较激进的jfc配置(下面会讲到),对应用进行更全面的事件采集。</p></li><li><p>线上性能监控、辅助问题定位。线上可以使用default.jfc或者自定义配置来进行低开销的事件采集。通过dump一段时间内的jfr记录来分析应用瓶颈和问题所在。</p></li></ol><h1 id="JFR历史"><a href="#JFR历史" class="headerlink" title="JFR历史"></a>JFR历史</h1><p>JFR 0.9 版本对应 JDK 7 和JDK 8:JDK 7u40 之后,实现了和 JRockit Flight Recorder 一样的功能,并添加了各项数据配置,用来打开或者关闭一些统计数据功能。而且,在 JDK 8u40 之后,可以在运行时灵活地打开关闭 JFR。<br>JFR 1.0 版本对应 JDK 9 和 JDK 10: 在这一版本之后,增加了 JFR 事件接口,用户可以生产或者消费某种事件。<br>JFR 2.0 版本对应 JDK 11,这一版本就是本文主要基于的版本。<br>JDK 14 推出了 <a href="https://openjdk.java.net/jeps/349">JFR Event Streaming </a>,让用户处理 JFR 事件更加灵活方便 。</p><h1 id="JDK相关"><a href="#JDK相关" class="headerlink" title="JDK相关"></a>JDK相关</h1><h3 id="JDK下载"><a href="#JDK下载" class="headerlink" title="JDK下载"></a>JDK下载</h3><p><a href="https://jdk.java.net/archive/">https://jdk.java.net/archive/</a></p><h3 id="JDK源码下载"><a href="#JDK源码下载" class="headerlink" title="JDK源码下载"></a>JDK源码下载</h3><p><a href="https://mirrors.omnios.org/openjdk/">https://mirrors.omnios.org/openjdk/</a></p><p><a href="https://github.com/openjdk/jdk/tags">https://github.com/openjdk/jdk/tags</a></p><h3 id="JFR源码"><a href="#JFR源码" class="headerlink" title="JFR源码"></a>JFR源码</h3><p><a href="https://github.com/AdoptOpenJDK/openjdk-jdk11/tree/master/src/jdk.jfr/share/classes/jdk/jfr">https://github.com/AdoptOpenJDK/openjdk-jdk11/tree/master/src/jdk.jfr/share/classes/jdk/jfr</a></p><h1 id="Tools"><a href="#Tools" class="headerlink" title="Tools"></a>Tools</h1><h2 id="JMC安装"><a href="#JMC安装" class="headerlink" title="JMC安装"></a>JMC安装</h2><p>JMC是用于分析JFR事件的开源工具,可以帮助我们更好地分析各类事件,JFR页面如下所示。</p><p><a href="https://github.com/openjdk/jmc">https://github.com/openjdk/jmc</a></p><p><img src="JFR%E5%AE%9E%E6%88%98%E6%95%99%E7%A8%8B-image2023-8-13_18-59-55.png"></p><h2 id="JMC源码"><a href="#JMC源码" class="headerlink" title="JMC源码"></a>JMC源码</h2><p><a href="https://github.com/openjdk/jmc">https://github.com/openjdk/jmc</a></p><h2 id="JMC-JFR-WIN"><a href="#JMC-JFR-WIN" class="headerlink" title="JMC-JFR(WIN)"></a>JMC-JFR(WIN)</h2><h3 id="idea使用JFR(不推荐)注意要点"><a href="#idea使用JFR(不推荐)注意要点" class="headerlink" title="idea使用JFR(不推荐)注意要点"></a>idea使用JFR(不推荐)注意要点</h3><p>idea自带的jfr可能存在各种问题,包括事件采集不全面,功能较少等, <strong>推荐使用JMC进行采集和分析</strong></p><p>并且JFR在低版本的idea中并未得到支持,并且在开始支持的较低版本可能存在各种问题(比如使用idea自带的JFR看不到内存分析等,虽然可能是由于jfc配置导致的),可以在此下载2023.1版本的idea。</p><p><a href="https://www.cnblogs.com/xxcbdhxx/archive/2023/03/30/17271151.html">https://www.cnblogs.com/xxcbdhxx/archive/2023/03/30/17271151.html</a></p><p>idea自带的JFR似乎无法分析由应用容器直接启动的应用(未做进一步确定,可以自己尝试),建议使用SpringBoot启动。</p><p>idea中c对jfr的cpu火焰图似乎还不错。</p><h2 id="JMC-JFR-MAC"><a href="#JMC-JFR-MAC" class="headerlink" title="JMC-JFR(MAC)"></a>JMC-JFR(MAC)</h2><h3 id="JMC安装到使用过程的问题"><a href="#JMC安装到使用过程的问题" class="headerlink" title="JMC安装到使用过程的问题"></a>JMC安装到使用过程的问题</h3><p>1.</p><p><img src="JFR%E5%AE%9E%E6%88%98%E6%95%99%E7%A8%8B-image2023-8-5_14-33-16.png"></p><p><strong>解决:</strong></p><p>电脑环境是Mac m2 ,macOS版本是Ventura 13.2.1。</p><p>mac默认使用/Library/JavaJavaVirtualMachines目录下的最新版本JDK,而系统原本最新JDK版本是11。</p><p>JDK在15以前只有Mac Linux Windows三种环境的版本,在15以后才有支持aarch64的版本,m系列芯片是aarch64(arm64)架构,因此报错。</p><p>2.</p><p><img src="JFR%E5%AE%9E%E6%88%98%E6%95%99%E7%A8%8B-image2023-8-5_14-35-55.png"></p><p><strong>解决:</strong></p><p>使用SpringBoot启动,使用JMC连接虚拟机时需要设置变量:</p><p>System . setProperty ( "java.rmi.server.hostname" , "Ip or DNS of the server" );</p><h2 id="JFR具体使用"><a href="#JFR具体使用" class="headerlink" title="JFR具体使用"></a>JFR具体使用</h2><h3 id="方式1-使用JVM参数启动"><a href="#方式1-使用JVM参数启动" class="headerlink" title="方式1 使用JVM参数启动"></a>方式1 使用JVM参数启动</h3><p>例如</p><p>-XX:StartFlightRecording=disk=true,dumponexit=true,name=profile_online,filename=/tmp/recording.jfr,maxsize=4096m,maxage=1d,settings=/path/to/custom.jfc,path-to-gc-roots=false</p><table><thead><tr><th>key</th><th>默认值</th><th>含义</th></tr></thead><tbody><tr><td>delay</td><td>0</td><td>延迟多久后启动 JFR 记录,支持带单位配置, 例如 delay=60s(秒), delay=20m(分钟), delay=1h(小时), delay=1d(天),不带单位就是秒, 0就是没有延迟直接开始记录。一般为了避免框架初始化等影响,我们会延迟 1 分钟开始记录(例如Spring cloud应用,可以看下日志中应用启动耗时,来决定下这个时间)。</td></tr><tr><td>disk</td><td>true</td><td>是否写入磁盘,这个就是上文提到的, global buffer 满了之后,是直接丢弃还是写入磁盘文件。</td></tr><tr><td>dumponexit</td><td>false</td><td>程序退出时,是否要dump出 .jfr文件</td></tr><tr><td>duration</td><td>0</td><td>JFR 记录持续时间,同样支持单位配置,不带单位就是秒,0代表不限制持续时间,一直记录。</td></tr><tr><td>filename</td><td></td><td>启动目录/hotspot-pid-26732-id-1-2020_03_12_10_07_22.jfr,pid 后面就是 pid, id 后面是第几个 JFR 记录,可以启动多个 JFR 记录。最后就是时间。 | dump的输出文件</td></tr><tr><td>name</td><td>无</td><td>记录名称,由于可以启动多个 JFR 记录,这个名称用于区分,否则只能看到一个记录 id,不好区分。</td></tr><tr><td>maxage</td><td>0</td><td>这个参数只有在 disk 为 true 的情况下才有效。最大文件记录保存时间,就是 global buffer 满了需要刷入本地临时目录下保存,这些文件最多保留多久的。也可以通过单位配置,没有单位就是秒,默认是0,就是不限制</td></tr><tr><td>maxsize</td><td>250MB</td><td>这个参数只有在 disk 为 true 的情况下才有效。最大文件大小,支持单位配置, 不带单位是字节,m或者M代表MB,g或者G代表GB。设置为0代表不限制大小**。虽然官网说默认就是0,但是实际用的时候,不设置会有提示**: No limit specified, using maxsize=250MB as default. 注意,这个配置不能小于后面将会提到的 maxchunksize 这个参数。</td></tr><tr><td>path-to-gc-roots</td><td>false</td><td>是否记录GC根节点到活动对象的路径,一般不打开这个,首先这个在我个人定位问题的时候,很难用到,只要你的编程习惯好。还有就是打开这个,性能损耗比较大,会导致FullGC一般是在怀疑有内存泄漏的时候热启动这种采集,并且通过产生对象堆栈无法定位的时候,动态打开即可。一般通过产生这个对象的堆栈就能定位,如果定位不到,怀疑有其他引用,例如 ThreadLocal 没有释放这样的,可以在 dump 的时候采集 gc roots</td></tr><tr><td>settings</td><td>default.jfc</td><td>这个位于 `$JAVA_HOME/lib/jfr/default.jfc`| 采集 Event 的详细配置,采集的每个 Event 都有自己的详细配置。另一个 JDK 自带的配置是 profile.jfc,位于 `$JAVA_HOME/lib/jfr/profile.jfc`。</td></tr></tbody></table><p><strong>其他配置</strong></p><p><strong>-XX:FlightRecorderOptions</strong></p><table><thead><tr><th></th><th></th><th></th></tr></thead><tbody><tr><td>allow_threadbuffers_to_disk</td><td>false</td><td>是否允许 在 thread buffer 线程阻塞的时候,直接将 thread buffer 的内容写入文件。默认不启用,一般没必要开启这个参数,只要你设置的参数让 global buffer 大小合理不至于刷盘很慢,就行了</td></tr><tr><td>globalbuffersize</td><td>如果不设置,根据设置的 memorysize 自动计算得出</td><td>单个 global buffer 的大小,一般通过 memorysize 设置,不建议自己设置</td></tr><tr><td>maxchunksize</td><td>12M</td><td>存入磁盘的每个临时文件的大小。默认为12MB,不能小于1M。可以用单位配置,不带单位是字节,m或者M代表MB,g或者G代表GB。注意这个大小最好不要比 memorySize 小,更不能比 globalbuffersize 小,否则会导致性能下降</td></tr><tr><td>memorysize</td><td>10M</td><td>JFR的 global buffer 占用的整体内存大小,一般通过设置这个参数,numglobalbuffers 还有 globalbuffersize 会被自动计算出。可以用单位配置,不带单位是字节,m或者M代表MB,g或者G代表GB。</td></tr><tr><td>numglobalbuffers</td><td>如果不设置,根据设置的 memorysize 自动计算得出</td><td>global buffer的个数,一般通过 memorysize 设置,不建议自己设置</td></tr><tr><td>old-object-queue-size</td><td>256</td><td>对于Profiling中的 Old Object Sample 事件,记录多少个 Old Object,这个配置并不是越大越好。一般应用256就够,时间跨度大的,例如 maxage 保存了一周以上的,可以翻倍</td></tr><tr><td>repository</td><td>等同于 - <a href="http://djava.io/">Djava.io </a>.tmpdir 指定的目录</td><td>JFR 保存到磁盘的临时记录的位置</td></tr><tr><td>retransform</td><td>true</td><td>是否通过 JVMTI 转换 JFR 相关 Event 类,如果设置为 false,则只在 Event 类加载的时候添加相应的 Java Instrumentation,这个一般不用改,这点内存 metaspace 还是足够的</td></tr><tr><td>samplethreads</td><td>true</td><td>这个是是否开启线程采集的状态位配置,只有这个配置为 true,并且在 Event 配置中开启线程相关的采集(这个后面会提到),才会采集这些事件。</td></tr><tr><td>stackdepth</td><td>64</td><td>采集事件堆栈深度,有些 Event 会采集堆栈,这个堆栈采集的深度,统一由这个配置指定。注意这个值不能设置过大,如果你采集的 Event种类很多,堆栈深度大很影响性能。比如你用的是 default.jfc 配置的采集,堆栈深度64基本上就是不影响性能的极限了。你可以自定义采集某些事件,增加堆栈深度。</td></tr><tr><td>threadbuffersize</td><td>8KB</td><td>threadBuffer 大小,最好不要修改这个,如果增大,那么随着你的线程数增多,内存占用会增大。过小的话,刷入 global buffer 的次数就会变多。8KB 就是经验中最合适的。</td></tr></tbody></table><h3 id="方式2-使用jcmd"><a href="#方式2-使用jcmd" class="headerlink" title="方式2 使用jcmd"></a>方式2 使用jcmd</h3><p><strong>1.启动 JFR 记录</strong></p><p>jcmd <pid> JFR.start</p><p><strong>例如</strong>jcmd 85 JFR.start name=profile_online filename=/tmp/recording.jfr maxage=1d maxsize=2g settings=opt/app/WEB-INF/tars/fat/custom.jfc</p><p>参数 与上面通过JVM参数启动的方式一致,如果使用JVM参数开启了记录的话dump指定时间的记录就行</p><p><strong>2.dumpJFR记录</strong></p><p>jcmd <pid> JFR.dump</p><p><strong>例如</strong>jcmd 83 JFR.dump begin=16:06 end=16:08 name=profile_online filename=/tmp/profile- `hostname`-`date +%Y_%m_%d_%H_%M_%S`.jfr</p><table><thead><tr><th>参数</th><th>默认值</th><th>描述</th></tr></thead><tbody><tr><td>name</td><td>无</td><td>指定要查看的 JFR 记录名称</td></tr><tr><td>filename</td><td>无</td><td>指定文件输出位置</td></tr><tr><td>maxage</td><td>0</td><td>dump最多的时间范围的文件,可以通过单位配置,没有单位就是秒,默认是0,就是不限制</td></tr><tr><td>maxsize</td><td>0</td><td>dump最大文件大小,支持单位配置, 不带单位是字节,m或者M代表MB,g或者G代表GB。设置为0代表不限制大小</td></tr><tr><td>begin</td><td>无</td><td>dump开始位置, 可以这么配置:09:00, 21:35:00, 2018-06-03T18:12:56.827Z, 2018-06-03T20:13:46.832, -10m, -3h, or -1d</td></tr><tr><td>end</td><td>无</td><td>dump结束位置,可以这么配置: 09:00, 21:35:00, 2018-06-03T18:12:56.827Z, 2018-06-03T20:13:46.832, -10m, -3h, or -1d (STRING, no default value)</td></tr><tr><td>path-to-gc-roots</td><td>false</td><td>是否记录GC根节点到活动对象的路径,一般不记录,dump 的时候打开这个肯定会触发一次 fullGC,对线上应用有影响。最好参考之前对于 JFR 启动记录参数的这个参数的描述,考虑是否有必要</td></tr></tbody></table><p><strong>下载dump文件</strong></p><p><strong>【推荐】方式1:</strong></p><p>jcmd 72 JFR.dump begin=19:59 end=20:01 name=profile_online filename=/tmp/profile_ `hostname`-`date +%Y_%m_%d_%H_%M_%S`.jfr</p><p><strong>方式2:</strong></p><p>在跳板机切到sftp,下载指定jfr文件即可(dump文件建议dump到tmp目录,这样可以在sftp页面直接看到对应文件)</p><p><strong>3.停止JFR记录</strong></p><p>jcmd <pid> JFR.stop</p><p><strong>4.查看正在执行的JFR记录</strong></p><p>jcmd <pid> JFR.check</p><p><strong>5.查看或修改JFR配置参数</strong></p><p>jcmd <pid> JFR.configure</p><p>如果不传入参数,则是查看当前配置,传入参数就是修改配置。</p><p>更多jcmd使用可以查看 <a href="https://docs.oracle.com/javacomponents/jmc-5-5/jfr-runtime-guide/run.htm#JFRRT172">https://docs.oracle.com/javacomponents/jmc-5-5/jfr-runtime-guide/run.htm#JFRRT172</a></p><p>对于参数的介绍也可以查看redHat的文档 <a href="https://access.redhat.com/documentation/zh-cn/openjdk/11/html/using_jdk_flight_recorder_with_openjdk/index">https://access.redhat.com/documentation/zh-cn/openjdk/11/html/using_jdk_flight_recorder_with_openjdk/index</a></p><h3 id="方式3-使用JMC直接连接"><a href="#方式3-使用JMC直接连接" class="headerlink" title="方式3 使用JMC直接连接"></a>方式3 使用JMC直接连接</h3><p>本地分析时非常推荐使用这种方式,操作比较简单,就不展开了</p><h3 id="JFR自定义配置文件"><a href="#JFR自定义配置文件" class="headerlink" title="JFR自定义配置文件"></a>JFR自定义配置文件</h3><p><strong>建议关闭 OldObjectSample事件采集,它可能导致内存泄漏(如果需要开启的话建议在mirror上开启后接入流量观察几天,查看RSS使用率是否正常)</strong></p><p>JDK中自带了两份jfc配置文件</p><ul><li><p><code>default.jfc</code> : <em>default</em>配置文件是一种低开销配置,可以安全地在生产环境中连续使用。开销通常小于 1%。</p></li><li><p><code>profile.jfc</code> : <em>profile</em>配置文件是一种低开销配置,非常适合用于分析瓶颈。开销通常低于 2%(官方宣称)。</p></li></ul><p>对于profile.jfc,经过实测,性能开销不止官方宣称的2%, <strong>不适合用于生产环境长期开启</strong>。</p><p>JFR中有一些指标监控已经在JMX中存在,对于一些异常和ERROR的记录也能通过现有的log进行分析,因此不必重复记录。</p><p>另外对于一些事件的记录开销比较大,例如ObjectAllocationInNewTLAB和ObjectAllocationOutsideTLAB,也一些事件对应用问题的排查参考意义不大,因此需要调整或关闭这部分事件的采集,在需要采集时再动态开启。</p><p>此外有一部分事件的采集频率需要根据应用情况适当调整。</p><p>进行上述调整后得到了自定义配置文件(V1.0),在启动JFR监控时使用这份jfc配置用于线上长期采集。</p><p>这份配置文件采取了 <strong>比较保守</strong>的配置,可以根据自己的应用需求具体调整。</p><p><a href="https://jeyzer.org/resources/jfr/jeyzer.jfc">Jeyzer 的 jfc 配置参考</a></p><h3 id="JFR文件拆分"><a href="#JFR文件拆分" class="headerlink" title="JFR文件拆分"></a>JFR文件拆分</h3><p>我们可能一次性dump了比较多的记录,或者想要一次性dump一段比较长时间的记录进行分析,但是jmc对jfr记录的分析十分消耗cpu和内存,所以对于比较大的jfr记录我们需要对其进行拆分</p><p>我们可以使用jdk自带的命令来完成这个需求</p><p>官方文档 <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/jfr.html">The jfr Command</a></p><p>用法例如</p><p>/Library/Java/JavaVirtualMachines/jdk-17.jdk/Contents/Home/bin/jfr disassemble –max-chunks 1 –output ./ ./profile_data.jfr</p><p><img src="JFR%E5%AE%9E%E6%88%98%E6%95%99%E7%A8%8B-image2023-9-4_13-57-52.png"></p><h3 id="开启JFR对性能的影响测试-todo"><a href="#开启JFR对性能的影响测试-todo" class="headerlink" title="开启JFR对性能的影响测试(todo)"></a>开启JFR对性能的影响测试(todo)</h3><p>本测试的条件是在使用JVM参数开启JFR记录,已经平稳运行的应用上测试dump文件的影响、停止JFR的影响、以及开启JFR的影响。</p><p>主要通过查看执行JFR指令时cpu load和response time的抖动情况来评估</p><h4 id="一个缓存型服务"><a href="#一个缓存型服务" class="headerlink" title="一个缓存型服务"></a>一个缓存型服务</h4><p>一个缓存型服务是一个缓存应用,接口响应时间快、性能高,能承受比较大的并发。如果JFR会导致比较大的开销,比较容易能在响应时间上看出来。</p><p>对一个缓存型服务设置每分钟24k左右的单机流量请求,远高于线上的请求量,一方面JFR会记录到更多事件,放大JFR的开销,另一方面可以测试出在系统压力较大情况下JFR对系统的影响</p><p>测试期间的监控指标如下</p><p>1.dump测试</p><p>在23:25的时候dump了5分钟的数据,从监控可以看出来并没有导致系统出现明显的抖动</p><p>从原理角度也很好解释,因为JFR是持续记录的,我们dump文件时JFR只需要拉取一段时间内已有的记录给我们,而不需要在dump时再进行太多分析。</p><p>2.stop测试</p><p>在23:36时stop JFR,并未观察到明显抖动</p><p>3.start 测试</p><p>在23:55时开启了JFR,开启时也没有观察到系统明显的抖动,或是开启后性能的降低</p><h3 id="待完成部分"><a href="#待完成部分" class="headerlink" title="待完成部分"></a>待完成部分</h3><p><strong>Cryostat:容器化的JFR</strong></p><p><a href="https://cryostat.io/">Cryostat官网</a></p><p><a href="https://developers.redhat.com/articles/2021/11/09/automating-jdk-flight-recorder-containers">Automating JDK Flight Recorder in containers</a></p><p><a href="https://developers.redhat.com/articles/2021/10/18/announcing-cryostat-20-jdk-flight-recorder-containers">Announcing Cryostat 2.0: JDK Flight Recorder for containers</a></p><h3 id="推荐阅读"><a href="#推荐阅读" class="headerlink" title="推荐阅读"></a>推荐阅读</h3><p><a href="https://bestsolution-at.github.io/jfr-doc/openjdk-11.html">JFR事件大全</a></p><p><a href="https://access.redhat.com/documentation/en-us/red_hat_build_of_openjdk/11/html/using_jdk_flight_recorder_with_red_hat_build_of_openjdk/index">JFR教程-redhat-jdk11</a></p><p><a href="https://www.zhihu.com/column/c_1264859821121355776">JFR专题-hashcon</a></p><p><a href="https://www.bilibili.com/video/BV148411K742/?share_source=copy_web&vd_source=d30c780580db9da56e2e4f9fa6acf1a5">JFR编程与使用入门-video</a></p><p><a href="https://www.bilibili.com/video/BV13m4y1i7Nv/?share_source=copy_web&vd_source=d30c780580db9da56e2e4f9fa6acf1a5">JFR-JVM进程最全面低消耗持续线上监控-video</a></p><p><a href="https://stackoverflow.com/questions/64024144/how-to-configure-the-interval-of-a-custom-jfr-event">自定义事件</a></p><h3 id="拓展阅读"><a href="#拓展阅读" class="headerlink" title="拓展阅读"></a>拓展阅读</h3><p><a href="https://jeyzer.org/">Jeyzer:the incident analysis solution for Java</a></p><p><a href="https://jeyzer.org/resources/jfr/jeyzer.jfc">Jeyzer 的 jfc 配置参考</a></p><p><a href="https://jeyzer.org/jeyzer-and-jfr/">jeyzer and jfr</a></p><h3 id="其他阅读"><a href="#其他阅读" class="headerlink" title="其他阅读"></a>其他阅读</h3><p><a href="https://github.com/thegreystone/jmc-tutorial/tree/master/docs">JMC教程(有点过时)</a></p><p><a href="https://confluence.atlassian.com/enterprise/diagnosing-runtime-issues-using-the-java-flight-recorder-1127256408.html">参考</a></p><p><a href="https://www.morling.dev/blog/rest-api-monitoring-with-custom-jdk-flight-recorder-events/">自定义JFR事件</a></p><p><a href="https://www.reddit.com/r/java/comments/13n10ac/how_widely_is_jfr_java_flight_recorder_used/?onetap_auto=true">JFR讨论</a></p><p><a href="https://www.w3computing.com/articles/java-flight-recorder-advanced-profiling-diagnostics-techniques/">JFR使用</a></p><p><a href="https://foojay.io/today/continuous-production-profiling-and-diagnostics/">重点看看</a></p><p><a href="http://missioncontrol.mcnz.com/">JFR-JMC教程</a></p><p><a href="https://docs.newrelic.com/docs/apm/agents/java-agent/features/real-time-profiling-java-using-jfr-metrics/">new Relic集成JFR监控</a></p><p><a href="https://docs.datadoghq.com/profiler/enabling/java/?tab=datadogprofiler"><strong>data dog</strong></a></p><p><a href="https://github.com/cryostatio/jfr-datasource">grafana集成JFR</a></p><p><a href="https://access.redhat.com/documentation/en-us/red_hat_build_of_cryostat/2/html/using_cryostat_to_manage_a_jfr_recording/assembly_integrated-applications_assembly_event-templates">grafana集成JFR-redhat</a></p><p><a href="https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/toc.html">JVM监控指南(包括JFR)</a></p>]]></content>
<summary type="html"><p><strong>本文基于JDK11,对于jdk14的 <a href="https://openjdk.java.net/jeps/349">JFR Event Streaming </a>以及JDK16引入的 <a href="https://bugs.openjdk.org/browse/JDK-8257602">jdk.ObjectAllocationSample </a>事件等不作过多讨论</strong></p>
<h1 id="什么是JFR"><a href="#什么是JFR" class="headerlink" title="什么是JFR"></a>什么是JFR</h1><p>JFR是 Java Flight Record(前身是JRocket Flight Record),是 JVM 内置的基于事件的JDK监控记录框架。这个起名就是参考了黑匣子对于飞机的作用,将Java进程比喻成飞机飞行。顾名思义,这个记录主要用于问题定位和持续监控。</p></summary>
<category term="Java" scheme="https://wtmonster.github.io/categories/Java/"/>
<category term="Java" scheme="https://wtmonster.github.io/tags/Java/"/>
<category term="JVM" scheme="https://wtmonster.github.io/tags/JVM/"/>
<category term="JFR" scheme="https://wtmonster.github.io/tags/JFR/"/>
<category term="JMC" scheme="https://wtmonster.github.io/tags/JMC/"/>
<category term="Performance" scheme="https://wtmonster.github.io/tags/Performance/"/>
</entry>
<entry>
<title>Loom(虚拟线程)相关</title>
<link href="https://wtmonster.github.io/2024/09/05/2024-09-05-loom-virtual-threads/"/>
<id>https://wtmonster.github.io/2024/09/05/2024-09-05-loom-virtual-threads/</id>
<published>2024-09-04T16:00:00.000Z</published>
<updated>2025-09-09T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>JDK21的 虚拟线程 目前存在许多严重bug,尽量不要在可靠性要求较高的服务中使用!</p><h1 id="简单介绍"><a href="#简单介绍" class="headerlink" title="简单介绍"></a>简单介绍</h1><p>虚拟线程使用一个专用的,FIFO模式下的ForkJoin线程池(parallelStream使用的是LIFO模式)。</p><span id="more"></span><p>它的并行度就是承载虚拟线程的平台线程数,默认与可用核心数相等,也可以使用参数 jdk.virtualThreadScheduler.parallelism 来调整。</p><h1 id="部分实现细节"><a href="#部分实现细节" class="headerlink" title="部分实现细节"></a>部分实现细节</h1><p><a href="https://www.bilibili.com/video/BV1Ub4y1M7L3/?share_source=copy_web&vd_source=d30c780580db9da56e2e4f9fa6acf1a5">[JVMLS][精翻+个人补充说明]虚拟线程Continuation实现原理 </a>- 原视频 <a href="https://inside.java/2023/08/26/continuations-under-the-covers/">https://inside.java/2023/08/26/continuations-under-the-covers/</a></p><h1 id="推荐资料"><a href="#推荐资料" class="headerlink" title="推荐资料"></a>推荐资料</h1><p>以下资料主要是对java虚拟线程的设计和思考,具体实现以jdk为准</p><p><a href="https://cr.openjdk.org/~rpressler/loom/Loom-Proposal.html">https://cr.openjdk.org/~rpressler/loom/Loom-Proposal.html</a></p><p><a href="https://cr.openjdk.org/~rpressler/loom/loom/sol1_part1.html">https://cr.openjdk.org/~rpressler/loom/loom/sol1_part1.html</a></p><p><a href="https://cr.openjdk.org/~rpressler/loom/loom/sol1_part2.html">https://cr.openjdk.org/~rpressler/loom/loom/sol1_part2.html</a></p><p><a href="https://inside.java/2021/05/10/networking-io-with-virtual-threads/">https://inside.java/2021/05/10/networking-io-with-virtual-threads/</a></p><p><a href="https://inside.java/2020/08/07/loom-performance/">https://inside.java/2020/08/07/loom-performance/</a></p><p><a href="https://blogs.oracle.com/javamagazine/post/going-inside-javas-project-loom-and-virtual-threads">https://blogs.oracle.com/javamagazine/post/going-inside-javas-project-loom-and-virtual-threads</a></p><p>如果对异步发展史感兴趣,可以观看 <a href="https://www.bilibili.com/video/BV1Fu4m1K7xb/?share_source=copy_web&vd_source=d30c780580db9da56e2e4f9fa6acf1a5">【[Devoxx2023]深入理解Java异步编程发展历史-从线程到响应式再到虚拟线程】 </a>- 原视频 <a href="https://youtu.be/1zSF1259s6w?si=tJGrNK-Uh6Hgw4B9">https://youtu.be/1zSF1259s6w?si=tJGrNK-Uh6Hgw4B9</a></p><h1 id="风险"><a href="#风险" class="headerlink" title="风险"></a>风险</h1><p>虚拟线程支持ThreadLocal和synchronized,但是存在一些缺陷,有一些问题需要我们额外注意</p><h2 id="1-绑定问题"><a href="#1-绑定问题" class="headerlink" title="1.绑定问题"></a>1.绑定问题</h2><ol><li><p>虚拟线程执行时 在 <code>synchronized</code> 块或方法中执行代码时,会 与平台线程绑定(同步块内,以及等待同步锁都会pin),因此需要使用 ReentrantLock替代(未来有望从底层解决-目前已经有相关JEP,会在JDK24上 <a href="https://openjdk.org/jeps/491">https://openjdk.org/jeps/491 </a>)</p></li><li><p>虚拟线程执行native代码或外来函数( <a href="https://openjdk.org/jeps/424">JEP424 </a>)时是无法被中止的(无法从底层解决,目前典型的是类初始化)。</p></li></ol><h3 id="故障实例"><a href="#故障实例" class="headerlink" title="故障实例"></a>故障实例</h3><h4 id="1-P1-死锁(这块风险在JEP中实际上被低估了)"><a href="#1-P1-死锁(这块风险在JEP中实际上被低估了)" class="headerlink" title="1[P1].死锁(这块风险在JEP中实际上被低估了)"></a>1[P1].死锁(这块风险在JEP中实际上被低估了)</h4><h5 id="1-最经典场景(大部分死锁本质都是这类问题)"><a href="#1-最经典场景(大部分死锁本质都是这类问题)" class="headerlink" title="1.最经典场景(大部分死锁本质都是这类问题)"></a>1.最经典场景(大部分死锁本质都是这类问题)</h5><p>这个问题触发概率高,而且影响大,目前已经有许多第三方开源组件都出现了这个问题</p><p><a href="https://www.reddit.com/r/java/comments/1512xuo/virtual_threads_interesting_deadlock/">https://www.reddit.com/r/java/comments/1512xuo/virtual_threads_interesting_deadlock/ </a>(这里的分析实际上有点问题,但是无伤大雅,下面会说到)</p><p><a href="https://mail.openjdk.org/pipermail/loom-dev/2023-July/005993.html">https://mail.openjdk.org/pipermail/loom-dev/2023-July/005993.html</a></p><p><strong>复现代码</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i < Runtime.getRuntime().availableProcessors() + <span class="number">10</span>; i++) {</span><br><span class="line"> Thread.startVirtualThread(() -> {</span><br><span class="line"> System.out.println(Thread.currentThread() + <span class="string">": Before synchronized block"</span>);</span><br><span class="line"> <span class="keyword">synchronized</span> (Main.class) {</span><br><span class="line"> System.out.println(Thread.currentThread() + <span class="string">": Inside synchronized block"</span>);</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>原理分析</strong></p><p>大体原理可以参考上面的这个讨论内容</p><p><a href="https://www.reddit.com/r/java/comments/1512xuo/virtual_threads_interesting_deadlock/">https://www.reddit.com/r/java/comments/1512xuo/virtual_threads_interesting_deadlock/</a></p><p><strong>代码可以简化为</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (<span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i < Runtime.getRuntime().availableProcessors() + <span class="number">1</span>; i++) {</span><br><span class="line"> Thread.startVirtualThread(() -> {</span><br><span class="line"> doSthWithLock();</span><br><span class="line"> <span class="keyword">synchronized</span> (Main.class) {</span><br><span class="line"> doSthWithLock()</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">doSthWithLock</span><span class="params">()</span> {</span><br><span class="line"> lock.lock();</span><br><span class="line"> <span class="comment">// doSomeThing</span></span><br><span class="line"> lock.unlock();</span><br><span class="line"> } </span><br></pre></td></tr></table></figure><p>但是帖子里说到:</p><p>这里的分析略有偏差。如果 [#30]能得到锁的话,说明已经开始运行了,如果它没有yield让出平台线程的话应该是会执行到释放锁逻辑的,所以这个说法应该是不太准确的。</p><p>这里我们取这两个阻塞在System.out.println方法上的线程进行分析,即[#26](阻塞在同步代码块内部),[#30](阻塞在同步代码块外部)。</p><p>Lock内部实现的AQS使用 FIFO 队列来进行线程的排队进行资源抢占(对于已经在队列中的线程来说无论是公平锁还是非公平锁都是遵循FIFO的)。</p><p>这个死锁的场景实际上是FIFO队列中, [#30]排在[#26]前面,而当有一个线程执行完同步块外部的方法释放锁时,会将队列前面的Node,即[#30]进行unpark,然后自身马上停靠在synchronized同步块上被绑定</p><p>此时其他虚拟线程都在pinned状态,占用了所有平台线程(假设8核,即7个线程阻塞在同步块的锁上,1个线程阻塞在同步块内部的锁上)</p><p>因此这个unpark是无效的,[#30]此时没有平台线程可以拿来绑定,因此也就无法上锁,而[#26]又由于排在[#30]后面而无法得到unpark通知,因此死锁。</p><p>对于以上分析,我们可以通过debug来佐证</p><p>1.首先验证锁的情况,可以看到锁的确是并没有被抢占到的</p><p>2.当前AQS队列中#32排在#25前面,而#32阻塞在同步块外部的lock上,而#25阻塞在同步块内部的lock上,其他虚拟线程都在同步块上被绑定着</p><p>vt.json</p><p>如果摒弃ReentrantLock的影响,实际状态可以简化为类似以下代码情况</p><p>下面情况需要只有一个载体线程,即 -Djdk.virtualThreadScheduler.parallelism=1,另外需要模块化参数–add-opens java.base/jdk.internal.misc=ALL-UNNAMED</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> jdk.internal.misc.VirtualThreads;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestThreadDeadLock</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">volatile</span> <span class="keyword">static</span> <span class="type">int</span> <span class="variable">vt1state</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">volatile</span> <span class="keyword">static</span> <span class="type">int</span> <span class="variable">vt2state</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException {</span><br><span class="line"> <span class="type">Thread</span> <span class="variable">vt1</span> <span class="operator">=</span> Thread.startVirtualThread(() -> {</span><br><span class="line"> VirtualThreads.park();</span><br><span class="line"> vt1state++;</span><br><span class="line"> });</span><br><span class="line"> <span class="comment">// 等待t1进入park</span></span><br><span class="line"> <span class="keyword">while</span> (!Thread.State.WAITING.equals(vt1.getState())) {</span><br><span class="line"> Thread.onSpinWait();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="type">Thread</span> <span class="variable">vt2</span> <span class="operator">=</span> Thread.startVirtualThread(() -> {</span><br><span class="line"> <span class="comment">// 唤醒t1</span></span><br><span class="line"> VirtualThreads.unpark(vt1);</span><br><span class="line"> <span class="comment">// 唤醒t1后立即pin住当前唯一的载体线程</span></span><br><span class="line"> syncPark();</span><br><span class="line"> vt2state++;</span><br><span class="line"> });</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span> (<span class="literal">true</span>) {</span><br><span class="line"> Thread.sleep(<span class="number">1000</span>);</span><br><span class="line"> System.out.println(<span class="string">"vt1state:"</span> + vt1state + <span class="string">" vt2state: "</span> + vt2state);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title function_">syncPark</span><span class="params">()</span> {</span><br><span class="line"> VirtualThreads.park();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>运行后输出应该如下,vt1被unpark也无法执行到vt1state++的代码</p><p><strong>相关问题</strong></p><p><a href="https://bugs.openjdk.org/browse/JDK-8320211">https://bugs.openjdk.org/browse/JDK-8320211</a></p><p><a href="https://www.javaspecialists.eu/archive/Issue302-Virtual-Thread-Deadlocks.html">https://www.javaspecialists.eu/archive/Issue302-Virtual-Thread-Deadlocks.html</a></p><p><a href="https://www.reddit.com/r/java/comments/17xkkoc/java_virtual_threads_pitfalls_to_look_out_for/">https://www.reddit.com/r/java/comments/17xkkoc/java_virtual_threads_pitfalls_to_look_out_for/</a></p><p><strong>conf相关问题(conf上面这几个问题实际上也类似上面提到的)</strong></p><h5 id="2-单线程的死锁(更简单)"><a href="#2-单线程的死锁(更简单)" class="headerlink" title="2.单线程的死锁(更简单)"></a>2.单线程的死锁(更简单)</h5><p>一个简化场景是单线程调度器 vt1拿到了lock A然后遇到io让出 vt2拿到sync然后去拿lock A pin住</p><h5 id="3-Native或FFI导致的死锁"><a href="#3-Native或FFI导致的死锁" class="headerlink" title="3.Native或FFI导致的死锁"></a>3.Native或FFI导致的死锁</h5><p>这个可能比较难发生,但是与同步块的死锁是同理的</p><p>例如在类加载内与类加载外存在ReentrantLock,或者其他资源的抢占,就可能发生。</p><h4 id="2-P2-平台线程资源耗尽"><a href="#2-P2-平台线程资源耗尽" class="headerlink" title="2[P2].平台线程资源耗尽"></a>2[P2].平台线程资源耗尽</h4><p><a href="https://blog.ydb.tech/how-we-switched-to-java-21-virtual-threads-and-got-deadlock-in-tpc-c-for-postgresql-cca2fe08d70b">How we switched to Java 21 virtual threads and got a deadlock in TPC-C for PostgreSQL</a></p><p><a href="https://news.ycombinator.com/item?id=39008026">https://news.ycombinator.com/item?id=39008026</a></p><p>这个问题源于线程的pinning,需要注意。</p><h2 id="2-ThreadLocal"><a href="#2-ThreadLocal" class="headerlink" title="2.ThreadLocal"></a>2.ThreadLocal</h2><p>ThreadLocal的问题主要在于线程模型的变化</p><p>没有虚拟线程前我们可以假定线程数不会太多,但是虚拟线程打破了这个限制,一个系统可能同时存在非常多虚拟线程</p><p>因此ThreadLocal的数据量就可能非常非常多,这也就是ThreadLocal的风险所在了。用的时候注意一下就好。</p><p>这里cue一下ScopedValue,有些人可能期望使用ScopedValue完全替代ThreadLocal,其实这是不现实的。</p><p>最典型的就是ThreadLocal用于资源池化的场景,例如jackson中和log4j2中都有这种用法。例如 log4j2的ReusableMessage,现在老实换成普通资源池了 <a href="https://github.com/apache/logging-log4j2/pull/1401">https://github.com/apache/logging-log4j2/pull/1401 </a>。</p><h3 id="故障实例-1"><a href="#故障实例-1" class="headerlink" title="故障实例"></a>故障实例</h3><h4 id="1-P2-线程变量使用不当导致OOM"><a href="#1-P2-线程变量使用不当导致OOM" class="headerlink" title="1.[P2].线程变量使用不当导致OOM"></a>1.[P2].线程变量使用不当导致OOM</h4><p>但是这个其实跟 ThreadLocal关系不大。。。。</p><h2 id="监控"><a href="#监控" class="headerlink" title="监控"></a>监控</h2><p>可以使用JFR来观察虚拟线程的 在绑定时的挂起情况</p><p><code>jdk.VirtualThreadStart</code> 和 <code>jdk.VirtualThreadEnd</code></p><p><code>记录了虚拟线程的启动和结束,默认关闭</code></p><p>jdk.VirtualThreadPinned</p><p> 表示虚拟线程绑定在平台线程上时,如果阻塞那么会产生这个事件(即没有释放其平台线程),阈值为20ms,默认开启</p><p>jdk.VirtualThreadSubmitFailed</p><p>启动或取消unparking虚拟线程失败,默认开启</p><p><a href="https://openjdk.org/jeps/425#JDK-Flight-Recorder-JFR">https://openjdk.org/jeps/425#JDK-Flight-Recorder-JFR</a></p><h3 id="2-【推荐】JFR-EventStream"><a href="#2-【推荐】JFR-EventStream" class="headerlink" title="2.【推荐】JFR EventStream"></a><strong>2.【推荐】JFR EventStream</strong></h3><p>由于 <code>Djdk.tracePinnedThreads本身会导致pin线程(下面会讲到),因此JFR是我们监控pin线程的首选方案</code></p><p><code>但是如果使用通过命令行的方式使用JFR会存在以下问题</code></p><p><code>1. dump文件较大,获取监控数据不方便,难以一次获取长期分析数据</code></p><p><code>2. dump文件需要上机器控制台dump文件,比较麻烦</code></p><p><code>3. 一次只能查看单机数据</code></p><p><code>而如果我们自己去解析JFR文件实际上也能做到持续监控,但是会很麻烦</code></p><p>因此使用JFR event stream来解决这个问题</p><p>JFR的事件流是jdk14引入的特性 <a href="https://openjdk.org/jeps/349">JEP 349: JFR Event Streaming</a></p><p>这边用的是封装过的监控埋点组件,替换成自己的就好</p><p><strong>JFR event stream</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> com.google.common.collect.Maps;</span><br><span class="line"><span class="keyword">import</span> jdk.jfr.consumer.RecordedEvent;</span><br><span class="line"><span class="keyword">import</span> jdk.jfr.consumer.RecordingStream;</span><br><span class="line"><span class="keyword">import</span> jdk.jfr.internal.tool.PrettyWriter;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.PrintWriter;</span><br><span class="line"><span class="keyword">import</span> java.io.StringWriter;</span><br><span class="line"><span class="keyword">import</span> java.time.Duration;</span><br><span class="line"><span class="keyword">import</span> java.util.List;</span><br><span class="line"><span class="keyword">import</span> java.util.Map;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * JFR 事件流监控配置</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2024/04/20</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JFRMonitorConfiguration</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">LogFacade</span> <span class="variable">LOGGER</span> <span class="operator">=</span> LogFacadeFactory.getLogFacade(JFRMonitorConfiguration.class);</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">TITLE</span> <span class="operator">=</span> <span class="string">"JFR_LOG"</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">VIRTUAL_THREAD_PINNED</span> <span class="operator">=</span> <span class="string">"jdk.VirtualThreadPinned"</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">JAVA_MONITOR_ENTER</span> <span class="operator">=</span> <span class="string">"jdk.JavaMonitorEnter"</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">()</span> {</span><br><span class="line"> Thread.ofVirtual().name(<span class="string">"JFRMonitorThread"</span>).start(JFRMonitorConfiguration::startJFREventStream);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">startJFREventStream</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">try</span> (<span class="type">var</span> <span class="variable">rs</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">RecordingStream</span>()) {</span><br><span class="line"> rs.enable(VIRTUAL_THREAD_PINNED).withoutThreshold();</span><br><span class="line"> rs.enable(JAVA_MONITOR_ENTER).withThreshold(Duration.ofMillis(<span class="number">5</span>));</span><br><span class="line"> List.of(VIRTUAL_THREAD_PINNED, JAVA_MONITOR_ENTER)</span><br><span class="line"> .forEach(name -> rs.onEvent(name, event -> doLog(event, name)));</span><br><span class="line"> rs.start();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">doLog</span><span class="params">(RecordedEvent event, String eventName)</span> {</span><br><span class="line"> Map<String, String> tags = Maps.newHashMapWithExpectedSize(<span class="number">2</span>);</span><br><span class="line"> tags.put(<span class="string">"eventName"</span>, eventName);</span><br><span class="line"> tags.put(<span class="string">"isVirtual"</span>, String.valueOf(event.getThread().isVirtual()));</span><br><span class="line"> MetricClient.withTags(tags).recordOne(<span class="string">"JFRDuration"</span>, event.getDuration().toMillis());</span><br><span class="line"> LOGGER.build(TITLE, eventToString(event))</span><br><span class="line"> .tag(<span class="string">"eventName"</span>, eventName)</span><br><span class="line"> .tag(<span class="string">"threadId"</span>, event.getThread().getJavaThreadId())</span><br><span class="line"> .tag(<span class="string">"isVirtual"</span>, event.getThread().isVirtual())</span><br><span class="line"> .warn();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> String <span class="title function_">eventToString</span><span class="params">(RecordedEvent event)</span> {</span><br><span class="line"> <span class="type">StringWriter</span> <span class="variable">s</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">StringWriter</span>();</span><br><span class="line"> <span class="type">PrettyWriter</span> <span class="variable">p</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PrettyWriter</span>(<span class="keyword">new</span> <span class="title class_">PrintWriter</span>(s));</span><br><span class="line"> p.setStackDepth(<span class="number">64</span>);</span><br><span class="line"> p.print(event);</span><br><span class="line"> p.flush(<span class="literal">true</span>);</span><br><span class="line"> <span class="keyword">return</span> s.toString();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>需要加上以下参数</p><p>1.maven-surefire-plugin 中加上</p><p>--add-opens jdk.jfr/jdk.jfr.internal.tool=ALL-UNNAMED</p><p>2.maven-compiler-plugin 中加上</p><p><compilerArgs> <arg>–add-exports</arg> <arg>jdk.jfr/jdk.jfr.internal.tool=ALL-UNNAMED</arg> </compilerArgs></p><p>--add-exports jdk.jfr/jdk.jfr.internal.tool=ALL-UNNAMED</p><p>这个事件的触发条件是:线程在pinned情况下出现park操作的耗时超过duration(默认20ms)</p><p>当然,这样依然会有问题存在<br>1.在JFR启动之前,可能有一些其他的pin事件发生了,例如一些类加载,会被忽略掉<br>2.JFR的启动在premain之后,也就是说对于启动期间静态agent premain操作中的pin事件我们也是不知道的<br>对于问题1,我们可以使用命令行启动JFR的方式解决,随后去机器上dump启动期间的jfr记录就好。或者我们可以自定义一个agent,确保jfr监控在其他类加载之前启动。<br>问题2目前是无解的,这是在JVM里写死的<br>当然,如果启动期间有严重问题会直接导致机器启动不起来,并不会造成太大的影响。</p><h3 id="3-jdk-tracePinnedThreads-只能在非生产使用!"><a href="#3-jdk-tracePinnedThreads-只能在非生产使用!" class="headerlink" title="3. jdk.tracePinnedThreads ( 只能在非生产使用! )"></a><strong>3. <code>jdk.tracePinnedThreads </code></strong><code>( </code><strong><code>只能在非生产使用! </code></strong><code>)</code></h3><p>当线程固定在平台线程上阻塞时</p><p><code>-Djdk.tracePinnedThreads=full</code> 打印完整的堆栈跟踪,并突出显示本机帧和持有锁的帧。</p><p><code>-Djdk.tracePinnedThreads=short</code> 输出限制为仅有问题的帧。</p><p><code>目前存在以下两个严重bug</code></p><h4 id="bug1(deadlock)-https-bugs-openjdk-org-browse-JDK-8322846"><a href="#bug1(deadlock)-https-bugs-openjdk-org-browse-JDK-8322846" class="headerlink" title="bug1(deadlock): https://bugs.openjdk.org/browse/JDK-8322846"></a><code>bug1(deadlock):</code> <a href="https://bugs.openjdk.org/browse/JDK-8322846"><code>https://bugs.openjdk.org/browse/JDK-8322846</code></a></h4><p><code>这个问题实质上就是lock和sync的嵌套导致的死锁问题, </code><strong><code>bug1已经在jdk21.0.4修复</code></strong></p><ul><li><code>解决</code></li></ul><p><code>可以尝试使用- </code><a href="http://djdk.io/"><code>Djdk.io </code></a><code>.useMonitors=true来将内部的ReentrantLock替换成使用synchronized,但是这样有饮鸩止渴的意味,不推荐</code></p><h4 id="bug2(假死问题,hang住虚拟机)-https-bugs-openjdk-org-browse-JDK-8325521"><a href="#bug2(假死问题,hang住虚拟机)-https-bugs-openjdk-org-browse-JDK-8325521" class="headerlink" title="bug2(假死问题,hang住虚拟机): https://bugs.openjdk.org/browse/JDK-8325521"></a><code>bug2(假死问题,hang住虚拟机): </code><a href="https://bugs.openjdk.org/browse/JDK-8325521"><code>https://bugs.openjdk.org/browse/JDK-8325521</code></a></h4><p><code>这里对bug2展开说说</code></p><ul><li>根因</li></ul><p>JVM里的JVMTI存在bug,jdk20时就有相关pr( <a href="https://github.com/openjdk/jdk/pull/10321%EF%BC%89">https://github.com/openjdk/jdk/pull/10321 </a>) ,应该是漏了部分逻辑,将会在23以后完成修复,原因如下</p><ul><li><code>触发条件</code></li></ul><p>1.开启了参数-Djdk.tracePinnedThreads;<br>2.使用某个内部 agent增强了Runnable;<br>3.在某个内部 agent attach之前没有触发过java.lang.PinnedThreadPrinter类里lambda表达式的加载</p><ul><li>解决(如果不想关闭 -Djdk.tracePinnedThreads )</li></ul><p>1.关闭JVM里对JVMTI事件的处理 -XX:-DoJVMTIVirtualThreadTransitions</p><p>2.在某个内部 agent attach之前自己手动触发一次pin</p><ul><li>复现代码(参数 -Djdk.attach.allowAttachSelf=true -Djdk.tracePinnedThreads=full )</li></ul><p><strong>hang JVM复现代码</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> org.example;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.util.concurrent.locks.LockSupport;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">TestAgentJVMHang</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> InterruptedException, ClassNotFoundException {</span><br><span class="line"> <span class="comment">// 1. 启动Agent</span></span><br><span class="line"> System.out.println(<span class="string">"Agent loaded"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 2. 启动VirtualThread park</span></span><br><span class="line"> Thread.ofVirtual().start(TestAgentJVMHang::testPinnedPrinter);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 3. 进行类加载 - hang</span></span><br><span class="line"> Thread.sleep(<span class="number">1000</span>);</span><br><span class="line"> System.out.println(<span class="string">"finished"</span>);</span><br><span class="line"> Class<?> aClass = Class.forName(<span class="string">"org.example.TestAgentJVMHang$TestClass"</span>);</span><br><span class="line"> System.out.println(aClass);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title function_">testPinnedPrinter</span><span class="params">()</span> {</span><br><span class="line"> LockSupport.parkNanos(<span class="number">1</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">TestClass</span> {</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="解决措施"><a href="#解决措施" class="headerlink" title="解决措施"></a>解决措施</h2><ul><li><p>最好的解决方案是官方提供synchronized对虚拟线程的支持(在jdk24中已经实现了适配 <a href="https://openjdk.org/jeps/491">JEP 491 </a>)</p></li><li><p>尽量从synchronized迁移到ReentrantLock</p></li></ul><h3 id="自动档"><a href="#自动档" class="headerlink" title="自动档"></a>自动档</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><dependency></span><br><span class="line"> <groupId>mysql</groupId></span><br><span class="line"> <artifactId>mysql-connector-java</artifactId></span><br><span class="line"> <version><span class="number">8.3</span><span class="number">.0</span>-vt<span class="number">.1</span></version></span><br><span class="line"></dependency></span><br><span class="line"></span><br><span class="line"><dependency></span><br><span class="line"> <groupId>org.apache.httpcomponents.client5</groupId></span><br><span class="line"> <artifactId>httpclient5</artifactId></span><br><span class="line"> <version><span class="number">5.4</span>-alpha2</version></span><br><span class="line"></dependency></span><br><span class="line"><dependency></span><br><span class="line"> <groupId>org.apache.httpcomponents.core5</groupId></span><br><span class="line"> <artifactId>httpcore5</artifactId></span><br><span class="line"> <version><span class="number">5.3</span>-alpha2</version></span><br><span class="line"></dependency></span><br><span class="line"><dependency></span><br><span class="line"> <groupId>org.apache.httpcomponents.core5</groupId></span><br><span class="line"> <artifactId>httpcore5-h2</artifactId></span><br><span class="line"> <version><span class="number">5.3</span>-alpha2</version></span><br><span class="line"></dependency></span><br></pre></td></tr></table></figure><p>以及以下依赖:</p><h3 id="具体如何更好地适配"><a href="#具体如何更好地适配" class="headerlink" title="具体如何更好地适配"></a>具体如何更好地适配</h3><p>1.可以参考 <a href="https://dzone.com/articles/relearning-java-thread-primitives">https://dzone.com/articles/relearning-java-thread-primitives </a>,更优雅地使用Lock进行替换<br>2.对于ConcurrentHashMap这类使用ReentrantLock替换需要付出代价(需要在Node上加lock属性,会导致内存膨胀)的场景,可以考虑使用ConcurrentSkipListMap替换(性能略差,但是是无锁的线程安全HashMap实现)<br>3.由于JFR的event并不包含持有Monitor的帧( <a href="https://bugs.openjdk.org/browse/JDK-8314591">https://bugs.openjdk.org/browse/JDK-8314591 </a>),因此上线前的排查阶段使用 -Djdk.tracePinnedThreads排查比较方便</p><h3 id="排查时需要关注的场景"><a href="#排查时需要关注的场景" class="headerlink" title="排查时需要关注的场景"></a>排查时需要关注的场景</h3><p>1.sync块内外出现了同一资源的竞争,典型的例如上面的sync块内外出现了对同一lock的抢占,这种可能造成死锁</p><p>2.sync里面有park但是无内外资源竞争的,这种只会影响性能</p><p>3.sync里面没有park操作,这种情况无竞争的情况下无影响,有竞争的情况下会导致性能变差(通过JFR事件jdk.JavaMonitorEnter来观察)</p><p>4.其他绑定问题(Native或FFI代码,例如类加载场景)</p><h1 id="TIPS"><a href="#TIPS" class="headerlink" title="TIPS"></a>TIPS</h1><p><a href="https://www.javaperformancetuning.com/news/newtips274.shtml">https://www.javaperformancetuning.com/news/newtips274.shtml</a></p><p>虚拟线程不再需要池化,虚拟线程是非常轻量的,随用随建就行。</p><p>但是线程池 <strong>部分思想依然有效</strong>,例如使用线程池的思想控 <strong>制访问资源的并发量</strong>。</p><p>这这种场景线程池内部可能用Semaphore简单地控制并发量,而不需要有复杂的线程管理逻辑</p><p>也就是说虚拟线程池的目的将不再包含资源的复用,而应该只包含一些更轻量的附加控制</p><h1 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h1><h2 id="线程转储"><a href="#线程转储" class="headerlink" title="线程转储"></a>线程转储</h2><p>在jdk21中使用jstack -l [pid] > [file]是拿不到虚拟线程的</p><p>使用idea中的线程转储也获取不到虚拟线程</p><p>原因:虚拟线程数量非常多,原本扁平化的数据结构无法很好地组织线程之间的结构关系,引入结构化并发后更是如此</p><p>方案:</p><p>提供json结构来输出包含虚拟线程的堆栈转储,而不会在原来的线程转储输出中包含虚拟线程</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">jcmd <pid> Thread.dump_to_file -format=json <file></span><br><span class="line"></span><br><span class="line">新版idea也对此提供了一定支持</span><br></pre></td></tr></table></figure><p>参考</p><p><a href="https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html#GUID-8AEDDBE6-F783-4D77-8786-AC5A79F517C0">Virtual Threads: An Adoption Guide</a></p><p><a href="https://blog.rockthejvm.com/ultimate-guide-to-java-virtual-threads/">The Ultimate Guide to Java Virtual Threads</a></p>]]></content>
<summary type="html"><p>JDK21的 虚拟线程 目前存在许多严重bug,尽量不要在可靠性要求较高的服务中使用!</p>
<h1 id="简单介绍"><a href="#简单介绍" class="headerlink" title="简单介绍"></a>简单介绍</h1><p>虚拟线程使用一个专用的,FIFO模式下的ForkJoin线程池(parallelStream使用的是LIFO模式)。</p></summary>
<category term="Java" scheme="https://wtmonster.github.io/categories/Java/"/>
<category term="Java" scheme="https://wtmonster.github.io/tags/Java/"/>
<category term="JDK21" scheme="https://wtmonster.github.io/tags/JDK21/"/>
<category term="JVM" scheme="https://wtmonster.github.io/tags/JVM/"/>
<category term="Virtual Threads" scheme="https://wtmonster.github.io/tags/Virtual-Threads/"/>
<category term="Loom" scheme="https://wtmonster.github.io/tags/Loom/"/>
<category term="JFR" scheme="https://wtmonster.github.io/tags/JFR/"/>
</entry>
<entry>
<title>JDK21升级记录</title>
<link href="https://wtmonster.github.io/2024/08/18/2024-08-18-jdk21-upgrade-notes/"/>
<id>https://wtmonster.github.io/2024/08/18/2024-08-18-jdk21-upgrade-notes/</id>
<published>2024-08-17T16:00:00.000Z</published>
<updated>2025-07-19T16:00:00.000Z</updated>
<content type="html"><![CDATA[<h1 id="技术分享PPT,可以参考上面来做GC选型,特性选型,升级实践等"><a href="#技术分享PPT,可以参考上面来做GC选型,特性选型,升级实践等" class="headerlink" title="技术分享PPT,可以参考上面来做GC选型,特性选型,升级实践等"></a>技术分享PPT,可以参考上面来做GC选型,特性选型,升级实践等</h1><h1 id="1-准备工作"><a href="#1-准备工作" class="headerlink" title="1.准备工作"></a>1.准备工作</h1><h2 id="idea-升级"><a href="#idea-升级" class="headerlink" title="idea 升级"></a>idea 升级</h2><h2 id="Maven"><a href="#Maven" class="headerlink" title="Maven"></a>Maven</h2><p>maven下图的这个限制是在maven3.5.3加的,在idea 2023.6以后会影响依赖正常import</p><p><img src="21.JDK21%E5%8D%87%E7%BA%A7-image2024-8-14_16-24-37.png"></p><span id="more"></span><ul><li><p>springboot2:3.16.0</p></li><li><p>springboot3:3.10.0-Java21</p></li></ul><h1 id="2-新特性一览"><a href="#2-新特性一览" class="headerlink" title="2.新特性一览"></a>2.新特性一览</h1><h3 id="注意,预览功能非必要不建议使用!"><a href="#注意,预览功能非必要不建议使用!" class="headerlink" title="注意,预览功能非必要不建议使用!"></a>注意,预览功能非必要不建议使用!</h3><h2 id="JDK8-JDK21"><a href="#JDK8-JDK21" class="headerlink" title="JDK8-JDK21"></a>JDK8-JDK21</h2><p><a href="https://openjdk.org/jeps/0">https://openjdk.org/jeps/0</a></p><p><a href="https://openjdk.org/projects/jdk/17/jeps-since-jdk-11">JEPs in JDK 17 integrated since JDK 11</a></p><p><a href="https://openjdk.org/projects/jdk/21/jeps-since-jdk-17">JEPs in JDK 21 integrated since JDK 17</a></p><p><a href="https://openjdk.org/projects/jdk9/">https://openjdk.org/projects/jdk9/</a></p><p><a href="https://openjdk.org/projects/jdk/10/">https://openjdk.org/projects/jdk/10/</a></p><p><a href="https://openjdk.org/projects/jdk/11/">https://openjdk.org/projects/jdk/11/</a></p><p><a href="https://openjdk.org/projects/amber/">https://openjdk.org/projects/amber/</a></p><p>以下表格(去除了大部分日常开发不需要感知的特性)中按照个人理解根据重要性(影响程度,以及我们日常开发需要关注的程度)进行排序,会比较主观,欢迎补充和指正</p><p>这个仓库演示了jdk9-18的新特性 <a href="https://github.com/nipafx/demo-java-x">https://github.com/nipafx/demo-java-x</a></p><p>oracle给出的重大变更指南 <a href="https://docs.oracle.com/en/java/javase/21/migrate/significant-changes-jdk-release.html">https://docs.oracle.com/en/java/javase/21/migrate/significant-changes-jdk-release.html</a></p><h3 id="语言"><a href="#语言" class="headerlink" title="语言"></a>语言</h3><table><thead><tr><th>Reference</th><th>JDK Version</th><th>Description</th><th>Project&Tags</th></tr></thead><tbody><tr><td><a href="https://openjdk.org/jeps/395">JEP 395: Records</a></td><td>16</td><td>充当不可变数据的载体的类,与元组的形式类似,拥有 <strong>极简单的类结构</strong>,有一些序列化相关的特性需要注意。</td><td>Amber - language</td></tr><tr><td><a href="https://openjdk.org/jeps/430">JEP 430: String Templates (Preview)</a></td><td>21</td><td>提供了一种字符串模板和嵌入表达式生成字符串的方式,并且 <strong>性能接近使用+来进行拼接</strong>!JMH测试结果: <br><a href="21.JDK21%E5%8D%87%E7%BA%A7-StringConcatenationBenchmark.svg">StringConcatenationBenchmark.svg</a><br><a href="https://ionutbalosin.com/2024/02/jvm-performance-comparison-for-jdk-21/">https://ionutbalosin.com/2024/02/jvm-performance-comparison-for-jdk-21/</a><br>目前打算重做这个功能,这也是我不建议开启预览功能的原因之一 <a href="https://mail.openjdk.org/pipermail/amber-spec-experts/2024-April/004106.html">https://mail.openjdk.org/pipermail/amber-spec-experts/2024-April/004106.html</a></td><td>Amber - language、performance<br></td></tr><tr><td><a href="https://openjdk.org/jeps/361">JEP 361: Switch Expressions</a><br><a href="https://openjdk.org/jeps/394">JEP 394: Pattern Matching for instanceof</a><br><a href="https://openjdk.org/jeps/440">JEP 440: Record Patterns</a><br><a href="https://openjdk.org/jeps/441">JEP 441: Pattern Matching for switch</a></td><td>14<br>16<br>21<br>21</td><td>对 <strong>switch表达式的简化</strong>,包含instanceof的类型转换简化,对多种类型的支持(包含Record)<br>另外还额外对Record的模式匹配进行了简化,对嵌套的Record简化比较明显 <a href="https://belief-driven-design.com/looking-at-java-21-record-patterns-b5282/">https://belief-driven-design.com/looking-at-java-21-record-patterns-b5282/</a></td><td>Amber - language</td></tr><tr><td><a href="https://openjdk.org/jeps/286">JEP 286: Local-Variable Type Inference</a></td><td>10</td><td>笔者的建议是在特定情境下可以使用本特性,大部分是不关心具体的类型信息,或者可以在上下文中明确直接看到类型信息的时候。<br>例如在Stream中使用groupingBy的时候,这种时候类型信息会很长,但我们其实不太关注那一串具体的类型信息。<br>更具体的使用规范可以看 <a href="https://openjdk.org/projects/amber/guides/lvti-style-guide">https://openjdk.org/projects/amber/guides/lvti-style-guide</a></td><td>Amber - language</td></tr><tr><td><a href="https://openjdk.org/jeps/378">JEP 378: Text Blocks</a></td><td>15</td><td>可以使用三个双引号 <code>"""text""" </code><strong><code>包含多行字符串 </code></strong><code>。</code></td><td>Amber - language</td></tr><tr><td><a href="https://openjdk.org/jeps/409">JEP 409: Sealed Classes</a></td><td>17</td><td>密封类,可以 <strong>限制哪些其他类或接口</strong>可以继承或实现它们。</td><td>Amber - specification</td></tr><tr><td><a href="https://openjdk.org/jeps/431">JEP 431: Sequenced Collections</a></td><td>21</td><td>提供了一套 <strong>有序集合接口标准</strong><a href="https://www.infoq.com/news/2023/03/collections-framework-makeover/">https://www.infoq.com/news/2023/03/collections-framework-makeover/</a></td><td>Amber - specification</td></tr><tr><td><a href="https://openjdk.org/jeps/309">JEP 309: Dynamic Class-File Constants</a></td><td>11</td><td>提供了类似 invokedynamic 的常量创建形式,一种使用场景是通过这种方式实现高性能懒加载的单例</td><td>Valhalla</td></tr><tr><td><a href="https://openjdk.org/jeps/443">JEP 443: Unnamed Patterns and Variables (Preview)</a></td><td>21</td><td>对于没有用的变量可以用_代替命名表示省略<br><a href="https://www.baeldung.com/java-unnamed-patterns-variables">https://www.baeldung.com/java-unnamed-patterns-variables</a></td><td>Amber - language</td></tr></tbody></table><h3 id="JVM-amp-Compiler"><a href="#JVM-amp-Compiler" class="headerlink" title="JVM&Compiler"></a>JVM&Compiler</h3><table><thead><tr><th>Reference</th><th>JDK Version</th><th>Description</th></tr></thead><tbody><tr><td><a href="https://openjdk.org/jeps/377">ZGC(JEP377) </a>& <a href="https://openjdk.org/jeps/379">Shenandoah(JEP379)</a></td><td>15</td><td>ZGC和Shenandoah发布了生产版本</td></tr><tr><td><a href="https://openjdk.org/jeps/439">JEP 439: Generational ZGC</a></td><td>21</td><td>ZGC支持分代了,提升很大,但是目前还存在一些bug</td></tr><tr><td><a href="https://openjdk.org/jeps/307">JEP 307: Parallel Full GC for G1</a></td><td>10</td><td>G1支持并发的fullgc了,16GB堆也可以在秒级以内了</td></tr><tr><td><a href="https://openjdk.org/jeps/158">JEP 158: Unified JVM Logging</a><br><a href="https://openjdk.org/jeps/271">JEP 271: Unified GC Logging</a></td><td>9</td><td>规范化了虚拟机里日志的打印,可以参考 <a href="https://juejin.cn/post/6873020339752796167">OpenJDK 11 JVM日志相关参数解析与使用</a></td></tr><tr><td><a href="https://openjdk.org/jeps/345">JEP 345: NUMA-Aware Memory Allocation for G1</a></td><td>14</td><td>支持了对NUMA架构的感知,性能提升较大</td></tr><tr><td><a href="https://openjdk.org/jeps/280">JEP 280: Indify String Concatenation</a></td><td>9</td><td>编译时通过invokedynamic实现字符串拼接(以前是stringBuilder),可以更灵活地实现字符串拼接了,另外也为多语言铺路</td></tr><tr><td><a href="https://openjdk.org/jeps/312">JEP 312: Thread-Local Handshakes</a></td><td>10</td><td>线程局部握手,对偏向锁之类的场景可以不用等全局安全点了(偏向锁只是一个举例,后面被干掉了)</td></tr><tr><td><a href="https://openjdk.org/jeps/451">JEP 451: Prepare to Disallow the Dynamic Loading of Agents</a></td><td>21</td><td>jdk准备在将来的版本中默认禁止对正在运行的JVM加载agent(启动时可以)</td></tr><tr><td><a href="https://openjdk.org/jeps/346">JEP 346: Promptly Return Unused Committed Memory from G1</a></td><td>12</td><td>G1会更好地返还内存给操作系统(但对我们没用)</td></tr><tr><td><a href="https://openjdk.org/jeps/344">JEP 344: Abortable Mixed Collections for G1</a></td><td>12</td><td>G1能够通过划分必须回收区域、非必需回收区域来更精确地控制MixedGC的耗时。</td></tr><tr><td><a href="https://openjdk.org/jeps/318">JEP 318: Epsilon: A No-Op Garbage Collector (Experimental)</a></td><td>11</td><td>无GC的收集器,这玩意一般用不上,也只打算作为实验特性,开发者担心用户不小心误用,还是作为实验特性的好</td></tr></tbody></table><h3 id="Other"><a href="#Other" class="headerlink" title="Other"></a>Other</h3><table><colgroup><col width="183"><col width="183"><col width="183"><col width="183"></colgroup><thead><tr><th>Reference</th><th>JDK Version</th><th>Description</th><th>Project&Tags</th></tr></thead><tbody><tr><td><a href="https://openjdk.org/jeps/444">JEP 444: Virtual Threads</a></td><td>21</td><td>Loom</td></tr><tr><td><a href="https://openjdk.org/jeps/446">JEP 446: Scoped Values (Preview)</a></td><td>21</td><td>能在许多场景下替代ThreadLocal,但注意! <strong>它并不是为了取代 ThreadLocal </strong>!也取代不了<br>ThreadLocal变量修改难以追踪,难以调试;<br>容易忘记remove导致内存泄漏;<br>使用InheritableThreadLocal在子线程中传递父线程变量时, API 要求在其他线程中看不到更改线程局部变量的线程副本,因此会做拷贝逻辑 。<br><br>Scoped Value管理的变量不可变,代码更易读,并且通过不可变特性避免了拷贝的内存开销;<br>作用域更明确,生命周期结束后自然回收,内存更安全。<br><br>由于二者设计逻辑不同,依然存在一些场景适配工作量较大,例如将threadLocal当作资源池的场景。<br>最典型的,例如jackson的资源池,log4j2的ReusableMessage <a href="https://github.com/apache/logging-log4j2/pull/1401">https://github.com/apache/logging-log4j2/pull/1401</a><br></td><td>Loom</td></tr><tr><td><a href="https://openjdk.org/jeps/446">JEP 453: Structured Concurrency (Preview)</a></td><td>21</td><td><a href="https://www.infoq.com/news/2023/06/structured-concurrency-jdk-21/">https://www.infoq.com/news/2023/06/structured-concurrency-jdk-21/</a><br>例如<br> <pre><code class="language-java">Response handle() throws ExecutionException, InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Supplier<String> user = scope.fork(() -> findUser()); Supplier<Integer> order = scope.fork(() -> fetchOrder());<pre><code> scope.join() // Join both subtasks .throwIfFailed(); // ... and propagate errors // Here, both subtasks have succeeded, so compose their results return new Response(user.get(), order.get());}//...</code></pre><p>}<br></p></code></pre>结构化并发提供了一种任务组的结构化组织方式,我们可以比较灵活地处理这些任务,这种方式与虚拟线程的适配是很好的;<br>通过拓展StructuredTaskScope类,我们可以很灵活地定义自己想要的业务逻辑,并且可以将任务编排与业务逻辑分离开。</td><p></p><td>Loom</td></tr><tr><td><a href="https://openjdk.org/jeps/412">JEP 412: Foreign Function & Memory API (Incubator)</a></td><td>17</td><td>Panama对于JDK的影响其实比Loom可能更大(对我们上层开发者的影响不及Loom),感兴趣的可以看看相关的,Panama/JNI/JNA/JNR <a href="https://glavo.site/blog/2023/03/25/java-ffi-benchmark/">https://glavo.site/blog/2023/03/25/java-ffi-benchmark/</a></td><td>Panama</td></tr><tr><td><a href="https://openjdk.org/jeps/448">JEP 448: Vector API (Sixth Incubator)</a></td><td>21</td><td>支持SIMD向量化加速计算,至于什么是SIMD可以去百度一下,基本思想是通过 元素合并实现 在一个CPU周期内实现多个元素计算的并行<br>相关的还有个很火的项目 <a href="https://github.com/simdjson/simdjson">https://github.com/simdjson/simdjson </a>,使用SIMD加速JSON解析<br>也有java版本的 <a href="https://github.com/simdjson/simdjson-java">https://github.com/simdjson/simdjson-java</a></td><td>Panama</td></tr><tr><td><a href="https://openjdk.org/jeps/358">JEP 358: Helpful NullPointerExceptions</a></td><td>14</td><td>能够更准确地提供空指针异常信息</td><td></td></tr><tr><td><a href="https://openjdk.org/jeps/321">JEP 321: HTTP Client API</a></td><td>11</td><td>jdk提供了一套标准的HTTPClient的api</td><td></td></tr><tr><td><a href="https://openjdk.org/jeps/349">JEP 349: JFR Event Streaming</a></td><td>14</td><td></td></tr><tr><td><a href="https://openjdk.org/jeps/259">JEP 259: Stack-Walking API</a></td><td>9</td><td>在stackWalker之前只能通过 getStackTrace获取完整的线程堆栈,stackWalker允许通过StackFrame的流来执行想要的操作,这允许我们获取一部分堆栈,甚至能拿到堆栈上持有锁(monitor)的情况、局部变量、以及当前帧是编译帧还是解释帧等非常详细的信息。是个很实用的api。</td><td></td></tr><tr><td><a href="https://openjdk.org/jeps/250">JEP 250: Store Interned Strings in CDS Archives</a><br><a href="https://openjdk.org/jeps/310">JEP 310: Application Class-Data Sharing</a><br><a href="https://openjdk.org/jeps/341">JEP 341: Default CDS Archives</a><br><a href="https://openjdk.org/jeps/350">JEP 350: Dynamic CDS Archives</a></td><td>9<br>10<br>12<br>13</td><td>类数据共享存档,主要是需要提前构建好存档有点麻烦,使用可以参考 <a href="https://www.morling.dev/blog/building-class-data-sharing-archives-with-apache-maven/">https://www.morling.dev/blog/building-class-data-sharing-archives-with-apache-maven/</a></td><td></td></tr><tr><td><a href="https://openjdk.org/jeps/193">JEP 193: Variable Handles</a></td><td>9</td><td>通过VarHanlde可以实现更细粒度的内存屏障控制,例如Caffeine中就通过VarHandle来维护时间戳。</td><td>Code Tools</td></tr><tr><td><a href="https://openjdk.org/jeps/254">JEP 254: Compact Strings</a></td><td>9</td><td>String的底层从char变成了byte,粒度更细了,占用内存可以变少一些。但是特定场景下可能会增加cpu开销。 <a href="https://ionutbalosin.com/2018/06/compact-strings-feature-might-slow-down-predominant-utf-16-strings-applications/">https://ionutbalosin.com/2018/06/compact-strings-feature-might-slow-down-predominant-utf-16-strings-applications/</a></td><td></td></tr><tr><td><a href="https://openjdk.org/jeps/285">JEP 285: Spin-Wait Hints</a></td><td>9</td><td>自旋的场景使用 Thread.onSpinWait()可以节省CPU资源,例如<pre><code class="language-java"> volatile boolean eventNotificationNotReceived; void waitForEventAndHandleIt() { while ( eventNotificationNotReceived ) { Thread.onSpinWait(); } readAndProcessEvent(); }</code></pre>onSpinWait可以适当降低自己的优先级,降低对CPU资源的消耗<br>但值得注意的是, onSpinWait在命中C2之前都是空实现的逻辑,等于啥也没干,直到C2优化后才能发挥特性<br><a href="https://bugs.openjdk.org/browse/JDK-8147844">https://bugs.openjdk.org/browse/JDK-8147844</a></td><td></td></tr><tr><td><a href="https://openjdk.org/jeps/416">JEP 416: Reimplement Core Reflection with Method Handles</a></td><td>18</td><td>使用MethodHandles重新实现了反射(部分场景变快,部分变慢,具体见JEP描述,主要目的是规范化)。APIMethodHandle在JDK7中引入,在使用方法上比反射复杂一些,限制也比反射多一点,但是正确的使用可以使性能接近非反射直接调用方法。PR: <a href="https://github.com/openjdk/jdk/pull/5027">https://github.com/openjdk/jdk/pull/5027</a><br>顺带一提,Java表现出来了明显的倾向,想要尽量消除或者屏蔽C代码,想要尽可能替换成Java的实现。</td><td>Loom</td></tr><tr><td></td><td></td><td></td><td></td></tr><tr><td></td><td></td><td></td><td></td></tr></tbody></table><h3 id="废弃-amp-删除"><a href="#废弃-amp-删除" class="headerlink" title="废弃&删除"></a>废弃&删除</h3><table><colgroup><col width="344"><col width="100"><col width="908"></colgroup><thead><tr><th>Reference</th><th>JDK Version</th><th>Description</th></tr></thead><tbody><tr><td><a href="https://openjdk.org/jeps/374">JEP 374: Deprecate and Disable Biased Locking</a></td><td>15</td><td>废弃了偏向锁,这部分代码非常复杂,维护起来很麻烦,并且大部分情况下对性能并没有好处</td></tr><tr><td><a href="https://openjdk.org/jeps/363">JEP 363: Remove the Concurrent Mark Sweep (CMS) Garbage Collector</a></td><td>14</td><td>移除了CMS收集器</td></tr><tr><td><a href="https://openjdk.org/jeps/366">JEP 366: Deprecate the ParallelScavenge + SerialOld GC Combination</a></td><td>14</td><td>废弃了ParallelScavenge + SerialOld GC的组合</td></tr><tr><td><a href="https://openjdk.org/jeps/410">JEP 410: Remove the Experimental AOT and JIT Compiler</a></td><td>17</td><td>移除了实验性质的AOT(jaotc tool)和 JIT编译器,这部分工作和GraalVM一样使用Graal项目,但是没什么人用,还要单独去维护它。<br>因此直接移除了,并且建议需要使用 Graal 编译器用于 AOT 或 JIT的直接去用GraalVM即可。</td></tr><tr><td><a href="https://openjdk.org/jeps/214">JEP 214: Remove GC Combinations Deprecated in JDK 8</a></td><td>9</td><td>删除了以下GC组合<ul><li>DefNew + CMS</li><li>ParNew + SerialOld</li><li>Incremental CMS</li></ul></td></tr><tr><td></td><td></td><td></td></tr></tbody></table><h3 id="其他分类"><a href="#其他分类" class="headerlink" title="其他分类"></a>其他分类</h3><h4 id="jdk版本控制"><a href="#jdk版本控制" class="headerlink" title="jdk版本控制"></a>jdk版本控制</h4><p><a href="https://openjdk.org/jeps/322">JEP 322: Time-Based Release Versioning</a></p><p>重新规范了JDK的发版规则,更多jdk发版有关的信息可以参考 <a href="https://whichjdk.com/">https://whichjdk.com/</a></p><p>发布模型的变更可以看这两篇</p><p><a href="https://blogs.oracle.com/javamagazine/post/java-long-term-support-lts">https://blogs.oracle.com/javamagazine/post/java-long-term-support-lts</a></p><p><a href="https://blogs.oracle.com/java/post/moving-the-jdk-to-a-two-year-lts-cadence">https://blogs.oracle.com/java/post/moving-the-jdk-to-a-two-year-lts-cadence</a></p><h4 id="Project-Jigsaw-项目-模块化"><a href="#Project-Jigsaw-项目-模块化" class="headerlink" title="Project Jigsaw 项目-模块化"></a><a href="https://openjdk.org/projects/jigsaw/">Project Jigsaw </a>项目-模块化</h4><ul><li><p><a href="https://openjdk.org/jeps/200">200: The Modular JDK 200:模块化 JDK</a></p></li><li><p><a href="https://openjdk.org/jeps/201">201: Modular Source Code 201:模块化源代码</a></p></li><li><p><a href="https://openjdk.org/jeps/220">220: Modular Run-Time Images 模块化 Run-Time 映像 –illegal-access</a></p></li><li><p><a href="https://openjdk.org/jeps/260">260: Encapsulate Most Internal APIs 封装大多数内部 API</a></p></li><li><p><a href="https://openjdk.org/jeps/261">261: Module System 261:模块系统</a></p></li><li><p><a href="https://openjdk.org/jeps/403">JEP 403: Strongly Encapsulate JDK Internals 强封装</a></p></li><li><p><a href="https://openjdk.org/jeps/282">282: jlink: The Java Linker</a></p></li></ul><p>这里简单说下,一般升级时需要关注export和opens</p><p>export是模块内部会向外export一些包,而不被模块主动export的则认为是随时可变的,不做任何保证。如果使用了模块没有向外export的API,则会在编译期报错,需要在编译和启动参数手动增加export参数。</p><p>opens是运行期的检查,对应反射访问模块API的情况,根据运行期间的报错加一下就好,这个需要加在启动参数上。</p><p>值得注意的是,在jdk9开始做模块化以后,通过 –illegal-access来做了运行时的模块化控制,也就是opens的统一控制</p><ul><li><p><code>--illegal-access=permit</code> : JDK9以后默认模式,只在第一次非法反射访问时 warning一下</p></li><li><p><code>--illegal-access=warn</code> :与 <code>permit类似,不过每次非法反射都会 warning</code></p></li><li><p><code>--illegal-access=debug</code> :在warn的基础上,加上堆栈输出</p></li><li><p><code>--illegal-access=deny</code> : 拒绝所有非法反射访问内部api,JDK16下的默认模式</p></li></ul><p>但是在JDK17中,通过 <a href="https://openjdk.org/jeps/403">JEP 403: Strongly Encapsulate JDK Internals 强封装 </a>, <code>--illegal-access这个参数被无效化了,相当于只能deny,我们只能通过 opens来控制运行时反射的访问。</code></p><h4 id="适配-amp-兼容"><a href="#适配-amp-兼容" class="headerlink" title="适配&兼容"></a>适配&兼容</h4><p><a href="https://openjdk.org/jeps/247">JEP 247: Compile for Older Platform Versions</a></p><p>编译时可以通过source和target参数灵活控制产物jdk版本,对兼容老版本还是有好处的。</p><p><a href="https://openjdk.org/jeps/238">JEP 238: Multi-Release JAR Files</a></p><p>可以指定不同jdk版本的环境中使用不同的api。例如我维护了一个jar包,其中一个接口在jdk8下希望使用某个api,在jdk21下使用另一个api,就可以通过 <a href="https://openjdk.org/jeps/238">Multi-Release JAR </a>实现,不过这个东西实际比较鸡肋。</p><h3 id="不在JEP内的"><a href="#不在JEP内的" class="headerlink" title="不在JEP内的"></a>不在JEP内的</h3><p>Java 18, ZGC, SerialGC, and ParallelGC support string deduplication</p><p><a href="https://blogs.oracle.com/javamagazine/post/java-18-gems-hidden-subtle-changes-deprecations">https://blogs.oracle.com/javamagazine/post/java-18-gems-hidden-subtle-changes-deprecations</a></p><p>jakarta</p><p><a href="https://wiki.eclipse.org/Jakarta_EE_Maven_Coordinates">https://wiki.eclipse.org/Jakarta_EE_Maven_Coordinates</a></p><p>参考</p><p><a href="https://blogs.oracle.com/java/post/the-arrival-of-java-21">https://blogs.oracle.com/java/post/the-arrival-of-java-21</a></p><p><a href="https://cn.quarkus.io/blog/virtual-thread-1/">https://cn.quarkus.io/blog/virtual-thread-1/</a></p><p><a href="https://chriswhocodes.com/jepmap.html">https://chriswhocodes.com/jepmap.html</a></p>]]></content>
<summary type="html"><h1 id="技术分享PPT,可以参考上面来做GC选型,特性选型,升级实践等"><a href="#技术分享PPT,可以参考上面来做GC选型,特性选型,升级实践等" class="headerlink" title="技术分享PPT,可以参考上面来做GC选型,特性选型,升级实践等"></a>技术分享PPT,可以参考上面来做GC选型,特性选型,升级实践等</h1><h1 id="1-准备工作"><a href="#1-准备工作" class="headerlink" title="1.准备工作"></a>1.准备工作</h1><h2 id="idea-升级"><a href="#idea-升级" class="headerlink" title="idea 升级"></a>idea 升级</h2><h2 id="Maven"><a href="#Maven" class="headerlink" title="Maven"></a>Maven</h2><p>maven下图的这个限制是在maven3.5.3加的,在idea 2023.6以后会影响依赖正常import</p>
<p><img src="21.JDK21%E5%8D%87%E7%BA%A7-image2024-8-14_16-24-37.png"></p></summary>
<category term="Java" scheme="https://wtmonster.github.io/categories/Java/"/>
<category term="Java" scheme="https://wtmonster.github.io/tags/Java/"/>
<category term="JDK21" scheme="https://wtmonster.github.io/tags/JDK21/"/>
<category term="JVM" scheme="https://wtmonster.github.io/tags/JVM/"/>
<category term="JEP" scheme="https://wtmonster.github.io/tags/JEP/"/>
</entry>
<entry>
<title>JDK21适用指南</title>
<link href="https://wtmonster.github.io/2024/08/01/2024-08-01-jdk21-upgrade-guide/"/>
<id>https://wtmonster.github.io/2024/08/01/2024-08-01-jdk21-upgrade-guide/</id>
<published>2024-07-31T16:00:00.000Z</published>
<updated>2025-06-14T16:00:00.000Z</updated>
<content type="html"><![CDATA[<p>当前升级JDK的动力实际上主要来自三项:ZGC、虚拟线程、衍生生态</p><h2 id="1-总体分析"><a href="#1-总体分析" class="headerlink" title="1.总体分析"></a>1.总体分析</h2><p>一下对JDK高版本主要特性做一个总览分析</p><span id="more"></span><h3 id="ZGC"><a href="#ZGC" class="headerlink" title="ZGC"></a>ZGC</h3><p>优点:ZGC基本消除了GC带来的延迟,并且对大对象有更好的处理</p><p>不足:ZGC吞吐量比不上G1,即便分代ZGC吞吐量有了很大提升,但与G1依然有一定差距。</p><h3 id="虚拟线程"><a href="#虚拟线程" class="headerlink" title="虚拟线程"></a>虚拟线程</h3><p>优点:适用IO密集型应用,一方面可以提高系统性能,另一方面可以极大消除线程带来的内存压力</p><p>不足:在目前JDK21的版本中,虚拟线程存在的坑还比较多,个人不建议在可靠性要求较高的应用上使用</p><h3 id="衍生生态"><a href="#衍生生态" class="headerlink" title="衍生生态"></a>衍生生态</h3><p>许多Java生态的框架都基于高版本jdk开发,并不再维护低JDK版本。如果想要探索基于高版本的框架,那么升级JDK也是势在必行的了。</p><h2 id="2-什么应用适合升级JDK?"><a href="#2-什么应用适合升级JDK?" class="headerlink" title="2.什么应用适合升级JDK?"></a>2.什么应用适合升级JDK?</h2><table><colgroup><col width="183"><col width="183"><col width="183"><col width="183"></colgroup><thead><tr><th>目标</th><th>适用应用</th><th>具体版本</th><th>风险</th></tr></thead><tbody><tr><td><strong>ZGC</strong></td><td><ol><li><strong>GC延迟敏感 </strong>的应用,例如缓存应用等</li><li>本地有 <strong>大缓存 </strong>, <strong>频繁生成大对象 </strong>的应用</li></ol></td><td>如果对吞吐量追求不高,可以考虑JDK17以上;<br>如果对吞吐量有较高要求,需要升级JDK21,并且开启分代特性。</td><td>分代ZGC并没有完全成熟,经过充分的验证后才可上线</td></tr><tr><td><strong>虚拟线程</strong></td><td><strong>IO密集型应用 </strong>都可以升级(文件IO除外)</td><td>JDK21</td></tr><tr><td><strong>衍生生态</strong></td><td>需要按照自己实际的调研进行升级考虑,例如S pring Modulith等</td><td>——</td><td>——</td></tr></tbody></table><h2 id="3-实际收益"><a href="#3-实际收益" class="headerlink" title="3. 实际收益"></a>3. 实际收益</h2><h3 id="分代ZGC"><a href="#分代ZGC" class="headerlink" title="分代ZGC"></a>分代ZGC</h3><p>我们对一个缓存型服务升级了JDK21,并使用了分代ZGC,在没有明显观察到资源占用提高的前提下解决了GC带来的长尾问题。</p><p>我们在3.28下午发布了一个集群,这是这个集群发布前后的长尾响应耗时情况</p><p>这是与JDK11 G1集群的对比,黄线是JDK11+G1,绿线是JDK21+ZGC</p><p>另外也可以参考Netflix这篇很好的文章 <a href="https://netflixtechblog.com/bending-pause-times-to-your-will-with-generational-zgc-256629c9386b">https://netflixtechblog.com/bending-pause-times-to-your-will-with-generational-zgc-256629c9386b</a></p><h3 id="虚拟线程-1"><a href="#虚拟线程-1" class="headerlink" title="虚拟线程"></a>虚拟线程</h3><p>我们在一个 IO 密集型服务进行了全面jdk21升级,并进行了虚拟线程灰度验证。</p><p>下图验证时对比非虚拟线程,平台线程数从2000降低到300+(7.19-7.26期间是由于某个依赖组件不支持虚拟线程,某个依赖组件里面开了300个平台线程,7.26后某个依赖组件替换为虚拟线程池)</p><p>对于虚拟线程,我们单独开了一个集群用于灰度(下图vt 集群)</p><p>下图中黄色线是JDK21集群,绿色为JDK21+虚拟线程集群</p><table><thead><tr><th></th><th>非虚拟线程(jdk21) p95</th><th>虚拟线程(jdk21) p95</th><th>降幅</th></tr></thead><tbody><tr><td>列表查询</td><td>284</td><td>239</td><td>19%</td></tr><tr><td>详情查询</td><td>589</td><td>493</td><td>16.3%</td></tr></tbody></table><p>加上升级jdk21本身带来的优化,对比jdk11的应用,虚拟线程+jdk21带来的p95降幅大概在20%</p><p>TODO:</p><p>使用虚拟线程后,不再需要像传统线程池一样进行参数调整,例如线程数,队列大小等等。</p><p>以前使用的线程池似乎会在某些场景下存在任务积压的问题,导致耗时大面积上升,切换后我们会进行压测,预期能将单机流量提升3倍以上。</p>]]></content>
<summary type="html"><p>当前升级JDK的动力实际上主要来自三项:ZGC、虚拟线程、衍生生态</p>
<h2 id="1-总体分析"><a href="#1-总体分析" class="headerlink" title="1.总体分析"></a>1.总体分析</h2><p>一下对JDK高版本主要特性做一个总览分析</p></summary>
<category term="Java" scheme="https://wtmonster.github.io/categories/Java/"/>
<category term="Java" scheme="https://wtmonster.github.io/tags/Java/"/>
<category term="JDK21" scheme="https://wtmonster.github.io/tags/JDK21/"/>
<category term="JVM" scheme="https://wtmonster.github.io/tags/JVM/"/>
<category term="ZGC" scheme="https://wtmonster.github.io/tags/ZGC/"/>
<category term="Virtual Threads" scheme="https://wtmonster.github.io/tags/Virtual-Threads/"/>
</entry>
<entry>
<title>ThreadLocal</title>
<link href="https://wtmonster.github.io/2022/09/12/ThreadLocal/"/>
<id>https://wtmonster.github.io/2022/09/12/ThreadLocal/</id>
<published>2022-09-12T09:24:27.000Z</published>
<updated>2022-09-15T10:18:27.000Z</updated>
<content type="html"><![CDATA[<h3 id="ThreadLocal"><a href="#ThreadLocal" class="headerlink" title="ThreadLocal"></a>ThreadLocal</h3><p>Thread有成员变量ThreadLocalMap对象(threadlocals)</p><p>ThreadLocal有内部类ThreadLocalMap用于存放线程数据</p><p>ThreadLocalMap底层通过Entry数组保存数据</p><span id="more"></span><p>所以其实是每个线程都有一份自己的ThreadLocalMap用于存放所有的entry,独立管理,每个threadlocals都是直接放在线程本身内部当做成员变量的,而每个new出来的threadlocal则是与值封装成Entry,类似key的功能。</p><p>Entry继承了弱引用的ThreadLocal,设置时会将ThreadLocal设置进去,当做key</p><blockquote><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">tab[i] = new Entry(key, value);</span><br></pre></td></tr></table></figure></blockquote><p><img src="image-20220401210612680.png" alt="image-20220401210612680"></p><p><strong>为什么是弱引用?</strong></p><p>一旦发现了<strong>只具有</strong>弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。</p><p>所以避免了key的内存泄露,但value还是需要手动使用set或remove回收</p><p>一个线程可以有多份数据存储,通过不同的threadlocal对象来获取</p><p><img src="image-20220401215400663.png" alt="image-20220401215400663"></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * The difference between successively generated hash codes - turns</span></span><br><span class="line"><span class="comment"> * implicit sequential thread-local IDs into near-optimally spread</span></span><br><span class="line"><span class="comment"> * multiplicative hash values for power-of-two-sized tables.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">HASH_INCREMENT</span> <span class="operator">=</span> <span class="number">0x61c88647</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Returns the next hash code.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="type">int</span> <span class="title function_">nextHashCode</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> nextHashCode.getAndAdd(HASH_INCREMENT);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>底层通过AtomicInteger维护一个静态成员变量实现ThreadLocal的哈希值生成</p><p>AutomicInteger可以保证多线程情况下创建threadLocal得到的hash值必定不同,避免了equals不同而hash值相同的情况。</p><blockquote><p>每当创建一个<code>ThreadLocal</code>对象,这个<code>ThreadLocal.nextHashCode</code> 这个值就会增长 <code>0x61c88647</code> 。</p><p>这个值很特殊,它是<strong>斐波那契数</strong> 也叫 <strong>黄金分割数</strong>。<code>hash</code>增量为 这个数字,带来的好处就是 <code>hash</code> <strong>分布非常均匀</strong>。</p></blockquote><blockquote><p>ThreadLocalMap使用闭散列:(开放地址法或者也叫线性探测法)解决哈希冲突,线性探测法的地址增量di = 1, 2, … 其中,i为探测次数。该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。<br>按照上面的描述,可以把table看成一个环形数组。</p></blockquote><h4 id="对比FastThreadLocal"><a href="#对比FastThreadLocal" class="headerlink" title="对比FastThreadLocal"></a>对比FastThreadLocal</h4><p>其实就是FastThreadLocal使用index直接记录元素下标,避免了hash计算,提高了效率。</p><p><strong>疑惑</strong></p><ul><li>为什么NextIndex是static的</li></ul><h4 id="补充与思考"><a href="#补充与思考" class="headerlink" title="补充与思考"></a>补充与思考</h4><p><strong>ThreadLocal为了防止内存泄露所做的</strong></p><ol><li><p>ThreadLocal通过弱引用使得reference,也就是ThreadLocal这个key可以被回收(在只有这个弱引用关系时)</p></li><li><p>如果set的是一个全新为null的slot,而非被回收了弱引用的,也非正常使用的entry,那么set时会顺带进行一次启发式清理,这个清理是从当前下标开始清理log2(n)<strong>次</strong>(n是当前size,也就是元素数量)</p><blockquote><p>这里只举例说明了slot为null的情况,其他情况比如key为null也是会尝试进行清理的,另外get在哈希下标位置没找到的时候也会尝试进行清理</p></blockquote><blockquote><p>值得注意的</p><p>这里的清理是按次数来算的,每次清理都会以遇到空slot为止,并且会对清理过程中的有效entry重新hash</p><p>这里修改了<em>Knuth</em>高德纳的著作TAOCP(《计算机程序设计艺术》)的<em>6.4</em>章节(散列) 中的<em>R</em>算法</p></blockquote></li></ol><p>另外值得注意的是ThreadLocal如果出现线程复用的话可能导致脏数据问题。如果在实现的线程 run()方法体中不显示地调用 remove 清理与线程相关的 ThreadLocal 对象,并且复用时不调用 set 设置初始值,就可能 get () 到重用的线程信息。</p><p><strong>巧妙之处</strong></p><p>弱引用对ThreadLocal的设计上造成了一定的问题,<strong>主要体现在</strong>key被回收了而value依然存在,entry不为null。</p><blockquote><p>为什么不能value也设置成弱引用的原因是因为value只被entry引用,会导致误回收</p></blockquote><p>但如果<strong>结合ThreadLocal解决哈希冲突的策略</strong>(开放定址法)来看这一设计就变得比较合理了。</p><p>ThreadLocal使用开放定址法解决哈希冲突,如果当前slot已经在使用,那么需要增加下标值继续向后探测,而正是因为value并没有被回收,所以开放定址法能作为一个有效的策略。</p><blockquote><p>如果整个entry都能直接被回收</p><p>考虑这种情况</p><p>以下entry都是同一哈希值,通过开放定址法集聚在一起</p><p>entry0 entry1 entry2 entry3 entry4</p><p>直接将部分entry回收</p><p>null null entry2 entry3 entry4</p><p>那么就无法通过哈希函数有效查找到后面三个entry了</p></blockquote><p>所以实际上value的保留相当于打上了一个标记,为之后的查找和重哈希提供了遍历。</p><h5 id="关于解决哈希冲突方案的选择"><a href="#关于解决哈希冲突方案的选择" class="headerlink" title="关于解决哈希冲突方案的选择"></a>关于解决哈希冲突方案的选择</h5><p>HashMap使用拉链法和红黑树,redis使用拉链法,而ThreadLocal使用开放定址法</p><p>除了网上都在说的红黑树更复杂,拉链法又比开放定址法复杂,以及指针带来的空间开销外</p><p>个人感觉一个重要的原因<strong>是哈希函数的限制</strong></p><p>对于HashMap,<strong>无法确定用户会给出一个怎样的hash函数</strong>,所以需要尽可能考虑到所有的情况,需要有兜底的策略。</p><p>而对于redis来说,数据结构的数量和种类是固定的,<strong>哈希函数都经过精心设计</strong>,所以通过拉链法就够用了。</p><p>而对于ThreadLocal来说,<strong>哈希函数直接通过黄金分割数配合长度为2的幂的数组来使用</strong>,极大降低了哈希冲突的可能性,所以可以直接使用开放定址法即可。</p>]]></content>
<summary type="html"><h3 id="ThreadLocal"><a href="#ThreadLocal" class="headerlink" title="ThreadLocal"></a>ThreadLocal</h3><p>Thread有成员变量ThreadLocalMap对象(threadlocals)</p>
<p>ThreadLocal有内部类ThreadLocalMap用于存放线程数据</p>
<p>ThreadLocalMap底层通过Entry数组保存数据</p></summary>
<category term="Java" scheme="https://wtmonster.github.io/categories/Java/"/>
<category term="Netty" scheme="https://wtmonster.github.io/categories/Java/Netty/"/>
<category term="ThreadLocal" scheme="https://wtmonster.github.io/tags/ThreadLocal/"/>
<category term="FastThreadLocal" scheme="https://wtmonster.github.io/tags/FastThreadLocal/"/>
</entry>
<entry>
<title>一个关于二级索引的优化思路</title>
<link href="https://wtmonster.github.io/2022/08/26/%E4%BA%8C%E7%BA%A7%E7%B4%A2%E5%BC%95%E7%9A%84%E5%9B%B0%E6%83%91/"/>
<id>https://wtmonster.github.io/2022/08/26/%E4%BA%8C%E7%BA%A7%E7%B4%A2%E5%BC%95%E7%9A%84%E5%9B%B0%E6%83%91/</id>
<published>2022-08-26T10:08:40.000Z</published>
<updated>2026-05-03T08:23:29.872Z</updated>
<content type="html"><![CDATA[<p>一个今年春招至今没解决的疑问(目前已经解决!感谢大佬们的解答和肯定!)</p><span id="more"></span><h2 id="开端"><a href="#开端" class="headerlink" title="开端"></a>开端</h2><blockquote><p>以下讨论范围是以B+树为数据结构的索引</p><p>并且不考虑二级索引关于事务版本的的字段可见性问题</p></blockquote><p>对于聚簇索引,非叶子节点的记录包括<strong>主键列</strong>,而叶子节点中的记录包括<strong>全部数据列(包括主键)</strong>。</p><p>对于自己建立的二级索引(辅助索引),非叶子节点的记录<strong>包括索引列</strong>,而叶子节点中的记录包括<strong>主键以及索引列</strong>。</p><p><strong>接下来我们只考虑除主键列外的其他列</strong></p><p>对比可以看出,聚簇索引的与叶子节点与非叶子节点记录明显是不对等的,叶子节点的记录包含比非叶子节点记录更多的列。</p><p>而二级索引非叶子节点包含的记录的列则与叶子节点中记录所包含的列一致。</p><p><strong>不难看出,除了聚簇索引外建立的索引都只能属于后者。</strong></p><p><strong>那么考虑下面这种情况</strong></p><p>有一个表 t_bl 有以下字段 (id,a,b)</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">select</span> a,b <span class="keyword">from</span> t_bl <span class="keyword">where</span> a <span class="operator">=</span> "XXX";</span><br></pre></td></tr></table></figure><p>这种情况下b字段并不作为检索的依据,但却需要在检索结果中出现。</p><p>那么有以下两种索引建立方案</p><ol><li><p><strong>建立 a 字段的索引</strong></p><p><strong>优点</strong>:使得索引的每个节点都减少了存储 b 字段的空间开销。</p><p><strong>缺点</strong>:返回查询结果需要通过二级索引的主键 id 回表到聚簇索引上查询 b 字段的值,影响查询性能。</p></li><li><p><strong>建立 a,b 字段的联合索引</strong></p><p><strong>优点</strong>:可以用到覆盖索引,减少回表,提高查询效率。</p><p><strong>缺点</strong>:使得二级索引中的每个非节点都增加了存储 b 字段的不必要的空间开销(因为b字段并不是检索依据)。</p><blockquote><p>9.16补充:这种非叶子节点上的索引数据空间的额外开销还会导致扇出的降低</p></blockquote></li></ol><p>那么为什么不提供一种机制,使得用户能建立非叶子节点与叶子节点不对等的索引?</p><p>比如在本例中,用户更好的选择其实是建立一个<strong>非叶子节点存储 a 字段</strong>,而<strong>叶子节点存储 a,b 两个字段的索引</strong>。</p><h2 id="思考"><a href="#思考" class="headerlink" title="思考"></a><strong>思考</strong></h2><p>innodb B+树索引页面大小默认是16kb,索引单位越小扇出越大。</p><p>一个三层,扇出为100的 B+ 树,按照页面为单位,第三层假设10000个页,那么第二层就是100个页,第一层为1个页。</p><p><strong>因此实际上叶子节点才是索引结构的主要空间开销,导致这个优化的收益并不是很高。</strong></p><h2 id="大佬提供的思路"><a href="#大佬提供的思路" class="headerlink" title="大佬提供的思路"></a>大佬提供的思路</h2><p><strong>在此感谢公众号「一树一溪」大佬提供的思路</strong></p><blockquote><p>这位大佬专门写 MySQL 源码文章,感兴趣的朋友,可以微信搜索关注</p></blockquote><ol><li>代码实现复杂度更高,如果支持这种非对称索引,相当于 B+ 树多了另一种索引结构,需要兼容。</li><li>支持这种非对称结构并不会带来很大的好处,因为少量回表并不会对性能有太大影响,只要把回表次数控制在一定范围内就没问题。</li><li>如果为了减少回表建了很多这种非对称索引,在计算执行成本的时候,这些索引都要考虑进去的,在查询优化阶段反而会增加花费的时间。</li></ol><p><strong>根据 Bill Karwin(《SQL反模式》作者 )的<a href="https://stackoverflow.com/questions/73524637/build-secondary-indexes-more-freely/73524732#73524732">回答</a></strong></p><p>目前<strong>Microsoft SQL Server</strong>已经实现了这个功能</p><p><a href="https://docs.microsoft.com/en-us/sql/relational-databases/indexes/create-indexes-with-included-columns?view=sql-server-ver16">https://docs.microsoft.com/en-us/sql/relational-databases/indexes/create-indexes-with-included-columns?view=sql-server-ver16</a></p><p>而InnoDB 目前还没有实现这个可行的优化,猜想应该是因为优先级不高。</p><p><strong>如果有其他思路,欢迎评论交流!</strong></p>]]></content>
<summary type="html"><p>一个今年春招至今没解决的疑问(目前已经解决!感谢大佬们的解答和肯定!)</p></summary>
<category term="MySQL" scheme="https://wtmonster.github.io/categories/MySQL/"/>
<category term="疑问" scheme="https://wtmonster.github.io/categories/MySQL/%E7%96%91%E9%97%AE/"/>
<category term="MySQL" scheme="https://wtmonster.github.io/tags/MySQL/"/>
<category term="索引" scheme="https://wtmonster.github.io/tags/%E7%B4%A2%E5%BC%95/"/>
</entry>
<entry>
<title>MVCC与覆盖索引</title>
<link href="https://wtmonster.github.io/2022/08/25/MVCC%E4%B8%8E%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95/"/>
<id>https://wtmonster.github.io/2022/08/25/MVCC%E4%B8%8E%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95/</id>
<published>2022-08-25T13:47:18.000Z</published>
<updated>2026-05-03T08:23:29.908Z</updated>
<content type="html"><![CDATA[<p>近期看到这样一个问题:<code>走覆盖索引不用回表,但是如何用MVCC判断可见性?因为二级索引上没有隐藏列trx_id和roll_ptr。而MVCC是基于trx_id和roll_ptr。那走覆盖索引如何判断是否可见呢?</code></p><span id="more"></span><h3 id="前置知识"><a href="#前置知识" class="headerlink" title="前置知识"></a>前置知识</h3><blockquote><p>参考资料</p><p><a href="https://www.modb.pro/db/173167">https://www.modb.pro/db/173167</a></p><p><a href="https://www.zhihu.com/question/27674363/answer/38034982">https://www.zhihu.com/question/27674363/answer/38034982</a></p><p>《Innodb存储引擎》</p></blockquote><p><strong>以下讨论基于Innodb的RR隔离级别</strong></p><h4 id="聚簇索引行记录"><a href="#聚簇索引行记录" class="headerlink" title="聚簇索引行记录"></a>聚簇索引行记录</h4><p>根据《Innodb存储引擎》:InnoDB每行有隐藏列<strong>TransactionID</strong>和<strong>Roll Pointer</strong></p><p>其中TransactionID是用于记录修改该记录的最新事务id,<strong>用于MVCC判断可见性及回滚</strong>。</p><p>而Roll Pointer也就是回滚指针,用于配合undolog进行<strong>数据版本回溯</strong>。</p><h4 id="二级索引记录"><a href="#二级索引记录" class="headerlink" title="二级索引记录"></a>二级索引记录</h4><p>问题中提到二级索引上没有隐藏列trx_id和roll_ptr,这的确是正确的。</p><p>但二级索引的页面具有<strong>PAGE_MAX_TRX_ID</strong>,用于记录修改页内数据的最新事务id。</p><p><strong>所以</strong></p><p>如果PAGE_MAX_TRX_ID的值是在Read View创建前提交的,那么该页的全部索引都可见;</p><p>如果不是,则需要根据二级索引的主键进行回表,进行再次判断。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a><strong>总结</strong></h3><p>二级索引的页面含有PAGE_MAX_TRX_ID用于记录修改页内数据的最新事务id,如果PAGE_MAX_TRX_ID的值是在Read View创建前提交的,那么该页的全部索引都可见;如果不是,则需要根据二级索引的主键进行回表,进行再次判断。</p><p>所以覆盖索引不是总不需要回表的。</p><h3 id="引申思考"><a href="#引申思考" class="headerlink" title="引申思考"></a>引申思考</h3><ul><li><p>为什么二级索引要选择存储页面最新的事务id,而非单条索引记录的最新事务id?</p><p>如果每条索引记录都存储最新事务id,那么可以减少不必要的回表,因为页内可能有一部分索引记录的最新事务是在当前事务创建之前提交的。</p><p>而以页面为单位存储最新事务id可以减少索引记录的数据量。</p><p>个人认为这是一种效率与空间之间的权衡。</p></li></ul>]]></content>
<summary type="html"><p>近期看到这样一个问题:<code>走覆盖索引不用回表,但是如何用MVCC判断可见性?因为二级索引上没有隐藏列trx_id和roll_ptr。而MVCC是基于trx_id和roll_ptr。那走覆盖索引如何判断是否可见呢?</code></p></summary>
<category term="MySQL" scheme="https://wtmonster.github.io/categories/MySQL/"/>
<category term="MySQL" scheme="https://wtmonster.github.io/tags/MySQL/"/>
<category term="MVCC" scheme="https://wtmonster.github.io/tags/MVCC/"/>
<category term="索引" scheme="https://wtmonster.github.io/tags/%E7%B4%A2%E5%BC%95/"/>
</entry>
<entry>
<title>常量池详解</title>
<link href="https://wtmonster.github.io/2022/06/25/%E5%B8%B8%E9%87%8F%E6%B1%A0%E8%AF%A6%E8%A7%A3/"/>
<id>https://wtmonster.github.io/2022/06/25/%E5%B8%B8%E9%87%8F%E6%B1%A0%E8%AF%A6%E8%A7%A3/</id>
<published>2022-06-25T05:25:50.000Z</published>
<updated>2026-05-03T08:23:29.834Z</updated>
<content type="html"><![CDATA[<h2 id="关于常量池"><a href="#关于常量池" class="headerlink" title="关于常量池"></a>关于常量池</h2><h3 id="常量池-Class常量池(Constant-pool)"><a href="#常量池-Class常量池(Constant-pool)" class="headerlink" title="常量池/Class常量池(Constant pool)"></a>常量池/Class常量池(Constant pool)</h3><p>常量池,也叫 Class 常量池(常量池 == Class常量池)。Java文件被编译成 Class文件,Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池,常量池是当Class文件被Java虚拟机加载进来后存放在方法区 各种字面量 (Literal)和 符号引用 。</p><span id="more"></span><p><img src="image-20220825185951061.png" alt="image-20220825185951061"></p><h3 id="运行时常量池(Runtime-constant-pool)"><a href="#运行时常量池(Runtime-constant-pool)" class="headerlink" title="运行时常量池(Runtime constant pool)"></a>运行时常量池(Runtime constant pool)</h3><p>运行时常量池是方法区的一部分。运行时常量池是当Class文件被加载到内存后,Java虚拟机会 **将Class文件常量池里的内容转移到运行时常量池里(运行时常量池也是每个类都有一个)**。运行时常量池相对于Class文件常量池的另外一个重要特征是具备动态性,Java语言并不要求常量一定只有编译期才能产生,也就是并非预置入Class文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中</p><p><strong>只存放字符串引用</strong></p><p><img src="watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1NzM3MDY4,size_16,color_FFFFFF,t_70-16547411527242-166140658303417.png" alt="watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ1NzM3MDY4,size_16,color_FFFFFF,t_70-16547411527242"></p><h3 id="字符串常量池(String-pool-String-table)"><a href="#字符串常量池(String-pool-String-table)" class="headerlink" title="字符串常量池(String pool/String table)"></a>字符串常量池(String pool/String table)</h3><p>字符串常量池逻辑上是运行时常量池的子集,具体可见<a href="https://github.com/fenixsoft/jvm_book/issues/112">https://github.com/fenixsoft/jvm_book/issues/112</a></p><p>字符串常量池又称为:字符串池,全局字符串池,英文也叫String Pool。 在工作中,String类是我们使用频率非常高的一种对象类型。JVM为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间:字符串常量池。<strong>字符串常量池由String类私有的维护</strong>。</p><h3 id="常量池和字符串常量池的版本变化"><a href="#常量池和字符串常量池的版本变化" class="headerlink" title="常量池和字符串常量池的版本变化"></a>常量池和字符串常量池的版本变化</h3><ul><li><p>在JDK1.7之前运行时常量池逻辑包含字符串常量池存放在方法区, 此时hotspot虚拟机对方法区的实现为永久代</p></li><li><p>在JDK1.7 字符串常量池、静态变量等被从方法区拿到了堆中, 这里没有提到运行时常量池,也就是说 字符串常量池被单独拿到堆,运行时常量池剩下的东西还在方法区, 也就是hotspot中的永久代</p><blockquote><p>字符串常量池本身并没有被移动,还是在native memory,移动的是String实例,也就是逻辑上移动到了堆</p></blockquote></li><li><p>在JDK1.8 hotspot移除了永久代用元空间(Metaspace)取而代之, 这时候字符串常量池还在堆, 运行时常量池还在方法区, 只不过方法区的实现从永久代变成了元空间(Metaspace)</p><blockquote><p>字符串常量池(StringTable)在native memory,一般逻辑上认为在堆里面</p></blockquote></li><li><p>JDK1.8-1.9,String底层从char数组变成了byte数组,原因是部分字符仅占一个byte,而堆中含有大量的String字符串,该优化能节省较多空间。</p></li><li><p>补充:</p><ul><li>对象只能存在于堆中(虚拟机规范的定义),所以String实体只能存在于堆中</li><li>运行时常量池存放的是字面量引用</li><li>使用双引号方式显式声明的字符串,则直接放入字符串常量池中(final修饰的“变量”可以直接看作双引号字面量)</li></ul></li></ul><p><strong>StringTable为什么要调整</strong>(1.6-1.7)</p><ul><li>permSize默认比较小</li><li>永久代垃圾回收频率低</li></ul><p><strong>字符串拼接操作</strong></p><ul><li>常量与常量的拼接结果在常量池,原理是编译器优化</li><li>常量池中不会存在相同内容的常量</li><li>只要其中一个是变量,结果就在堆中。变量拼接的原理是StringBuilder(final不算变量),返回String对象</li><li>如果拼接的结果调用intern()方法,则注定将常量池中还没有的字符串对象放入池中,并返回此对象地址</li></ul><p><strong>所以建议多使用final定义字符串,并且不使用new,对于new String()声明final不会优化</strong></p><p>示例</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">IsInPool</span><span class="params">(String str)</span>{</span><br><span class="line"> <span class="keyword">return</span> str == str.intern();</span><br><span class="line">}</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">a1</span> <span class="operator">=</span> <span class="string">"a"</span>;</span><br><span class="line"> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">a2</span> <span class="operator">=</span> <span class="string">"b"</span>;</span><br><span class="line"> <span class="type">String</span> <span class="variable">a3</span> <span class="operator">=</span> a1 + a2;</span><br><span class="line"> <span class="type">String</span> <span class="variable">limit1</span> <span class="operator">=</span> <span class="string">"ab"</span>;</span><br><span class="line"></span><br><span class="line"> <span class="type">String</span> <span class="variable">a4</span> <span class="operator">=</span> <span class="string">"c"</span>;</span><br><span class="line"> <span class="type">String</span> <span class="variable">a5</span> <span class="operator">=</span> <span class="string">"d"</span>;</span><br><span class="line"> <span class="type">String</span> <span class="variable">a6</span> <span class="operator">=</span> a4 + a5;</span><br><span class="line"> <span class="type">String</span> <span class="variable">limit2</span> <span class="operator">=</span> <span class="string">"cd"</span>;</span><br><span class="line"></span><br><span class="line"> <span class="type">String</span> <span class="variable">b1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"e"</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">b2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"f"</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">b3</span> <span class="operator">=</span> b1 + b2;</span><br><span class="line"> <span class="type">String</span> <span class="variable">limit3</span> <span class="operator">=</span> <span class="string">"ef"</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">b4</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"g"</span>);</span><br><span class="line"> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">b5</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"h"</span>);</span><br><span class="line"> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">b6</span> <span class="operator">=</span> b4 + b5;</span><br><span class="line"> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">limit4</span> <span class="operator">=</span> <span class="string">"gh"</span>;</span><br><span class="line"></span><br><span class="line"> System.out.println(IsInPool(a3));<span class="comment">//true</span></span><br><span class="line"> System.out.println(IsInPool(a6));<span class="comment">//false</span></span><br><span class="line"> System.out.println(IsInPool(b3));<span class="comment">//false</span></span><br><span class="line"> System.out.println(IsInPool(b6));<span class="comment">//false</span></span><br></pre></td></tr></table></figure><img src="image-20211008215003210.png" alt="image-20211008215003210" style="zoom:50%;"><h2 id="字符串的创建与常量池"><a href="#字符串的创建与常量池" class="headerlink" title="字符串的创建与常量池"></a><strong>字符串的创建与常量池</strong></h2><p><strong>String两种创建方式</strong></p><ul><li>方式一(str值和字符串常量池中字面量地址相等)</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">str</span> <span class="operator">=</span> <span class="string">"abc"</span></span><br></pre></td></tr></table></figure><ol><li>检查字符串常量池是否存在该字符串,存在则不创建并且返回string对象的引用</li><li>不存在则在堆中创建该字符串常量对应的string对象并将其引用存入字符串常量池中,然后返回该创建对象的引用</li></ol><ul><li>方式二(str值和字符串常量池中字面量地址不相等)</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">str</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"abc"</span>)</span><br></pre></td></tr></table></figure><ol><li><p>检查字符串常量池是否存在该字符串,存在则不创建string对象,不存在则在堆中创建该字符串常量对应的string对象并将其引用存入字符串常量池中</p><blockquote><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// hotspot/src/share/vm/classfile/symbolTable.cpp</span></span><br><span class="line"><span class="comment">// try to reuse the string if possible</span></span><br><span class="line"><span class="keyword">if</span> (!string_or_null.is_null()) {</span><br><span class="line"> <span class="built_in">string</span> = string_or_null;</span><br><span class="line">} <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">string</span> = java_lang_String::create_from_unicode(name, len, CHECK_NULL);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></blockquote></li><li><p>在堆中创建该对象,对象内部的char数组(实际的value)与常量池中创建的string对象共用一个char数组</p></li></ol><p><strong>测试代码</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">String</span> <span class="variable">str1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"abcd"</span>);</span><br><span class="line"><span class="type">String</span> <span class="variable">str2</span> <span class="operator">=</span> <span class="string">"abcd"</span>;</span><br><span class="line"><span class="type">Field</span> <span class="variable">value</span> <span class="operator">=</span> str1.getClass().getDeclaredField(<span class="string">"value"</span>);</span><br><span class="line">value.setAccessible(<span class="literal">true</span>);</span><br><span class="line">System.out.println(System.identityHashCode(str1));<span class="comment">//990368553</span></span><br><span class="line">System.out.println(System.identityHashCode(value.get(str1)));<span class="comment">//1096979270</span></span><br><span class="line">System.out.println(System.identityHashCode(value.get(str2)));<span class="comment">//1096979270</span></span><br><span class="line">System.out.println(System.identityHashCode(str2));<span class="comment">//1078694789</span></span><br><span class="line">System.out.println(System.identityHashCode(str1.intern()));<span class="comment">//1078694789</span></span><br></pre></td></tr></table></figure><p><strong>普遍地</strong></p><p>使用双引号方式显式存在的字符串,则直接放入字符串常量池中(final修饰的“变量”可以直接看作双引号字面量)</p><h3 id="一些测试(JDK1-8)"><a href="#一些测试(JDK1-8)" class="headerlink" title="一些测试(JDK1.8)"></a><strong>一些测试</strong>(JDK1.8)</h3><p>情况一:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">a1</span> <span class="operator">=</span> <span class="string">"a"</span>;</span><br><span class="line"> <span class="type">String</span> <span class="variable">a2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"b"</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">a3</span> <span class="operator">=</span> <span class="string">"c"</span>;</span><br><span class="line"> <span class="type">String</span> <span class="variable">a4</span> <span class="operator">=</span> a1 + a2;</span><br><span class="line"> <span class="type">String</span> <span class="variable">a5</span> <span class="operator">=</span> a1 + a3;</span><br><span class="line"><span class="comment">// String at = "a"+"c";</span></span><br><span class="line"><span class="comment">// String at1 = "a"+"b";</span></span><br><span class="line"></span><br><span class="line"> System.out.println(IsInPool(a1));<span class="comment">//true</span></span><br><span class="line"> System.out.println(IsInPool(<span class="string">"b"</span>));<span class="comment">//true</span></span><br><span class="line"> System.out.println(IsInPool(a2));<span class="comment">//false</span></span><br><span class="line"> System.out.println(IsInPool(a3));<span class="comment">//true</span></span><br><span class="line"></span><br><span class="line"> System.out.println(IsInPool(a4));<span class="comment">//true</span></span><br><span class="line"> System.out.println(IsInPool(<span class="string">"ab"</span>));<span class="comment">//true</span></span><br><span class="line"> System.out.println(IsInPool(a5));<span class="comment">//true</span></span><br><span class="line"> System.out.println(IsInPool(<span class="string">"ac"</span>));<span class="comment">//true</span></span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>情况二:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="type">String</span> <span class="variable">a1</span> <span class="operator">=</span> <span class="string">"a"</span>;</span><br><span class="line"> <span class="type">String</span> <span class="variable">a2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"b"</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">a3</span> <span class="operator">=</span> <span class="string">"c"</span>;</span><br><span class="line"> <span class="type">String</span> <span class="variable">a4</span> <span class="operator">=</span> a1 + a2;</span><br><span class="line"> <span class="type">String</span> <span class="variable">a5</span> <span class="operator">=</span> a1 + a3;</span><br><span class="line"> <span class="type">String</span> <span class="variable">at</span> <span class="operator">=</span> <span class="string">"a"</span>+<span class="string">"c"</span>;</span><br><span class="line"> <span class="type">String</span> <span class="variable">at1</span> <span class="operator">=</span> <span class="string">"a"</span>+<span class="string">"b"</span>;</span><br><span class="line"></span><br><span class="line"> System.out.println(IsInPool(a1));<span class="comment">//true</span></span><br><span class="line"> System.out.println(IsInPool(<span class="string">"b"</span>));<span class="comment">//true</span></span><br><span class="line"> System.out.println(IsInPool(a2));<span class="comment">//false</span></span><br><span class="line"> System.out.println(IsInPool(a3));<span class="comment">//true</span></span><br><span class="line"></span><br><span class="line"> System.out.println(IsInPool(a4));<span class="comment">//false</span></span><br><span class="line"> System.out.println(IsInPool(<span class="string">"ab"</span>));<span class="comment">//true</span></span><br><span class="line"> System.out.println(IsInPool(a5));<span class="comment">//false</span></span><br><span class="line"> System.out.println(IsInPool(<span class="string">"ac"</span>));<span class="comment">//t</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>IsInpool函数</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"> <span class="comment">//使用这个函数需要在这个函数前使用双引号字面量使得字符串直接被加入常量池中</span></span><br><span class="line"><span class="comment">//使用顺序应为待测字符串->字面量->IsInPool</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">IsInPool</span><span class="params">(String str)</span>{</span><br><span class="line"> <span class="keyword">return</span> str == str.intern();</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><h3 id="对于intern函数的理解"><a href="#对于intern函数的理解" class="headerlink" title="对于intern函数的理解"></a>对于intern函数的理解</h3><p>调用这个方法之后就是去看当前字符串是否在字符串常量池中已经存在引用</p><p>(1)<strong>存 在</strong>:那就直接返回该字符串在字符串常量池中所对应的地址给栈中要引用这个字符串的变量。</p><p>(2)<strong>不存在</strong>:<br>① jdk 1.6:先在字符串常量池中创建该字符串,地址与堆中字符串地址不相同(方法区)。然后再返回刚创建的字符串在字符串常量池中所对应的地址给栈中要引用这个字符串的变量。</p><p>② jdk 1.7及以后:直接将堆中(不是字符串常量池中)该字符串的地址复制到字符串常量池中,这样字符串常量池就有了该字符串的地址引用,也可以说此时字符串常量池中的字符串只是一个对堆中字符串对象的引用,它们两个的地址相同,然后再把这个地址返回给栈中要引用这个字符串的变量。</p><blockquote><p>具体看下面源码解析,这里描述可能不太精确</p></blockquote><h3 id="对测试的解释"><a href="#对测试的解释" class="headerlink" title="对测试的解释"></a>对测试的解释</h3><ul><li>第一次两个拼接测试为true,因为intern函数将堆中字符串对象引用复制到字符串常量池中,所以二者自然相等</li><li>第二次两个拼接测试为false,因为intern检查到已经存在该字符常量,且堆常量池中保存的是字符串的值,二者自然不相等</li></ul><h3 id="对StringBuilder的优化"><a href="#对StringBuilder的优化" class="headerlink" title="对StringBuilder的优化"></a>对StringBuilder的优化</h3><p>对于StringBuilder.append,底层默认为长度为16的char型数组,超出长度则需要新建数组重新依次赋值</p><p>如果明确知道长度不高于某个值,可以使用new StringBuilder(max)来初始化 </p><h2 id="String的不可变性与强制修改"><a href="#String的不可变性与强制修改" class="headerlink" title="String的不可变性与强制修改"></a>String的不可变性与强制修改</h2><h3 id="String为什么不可变?"><a href="#String为什么不可变?" class="headerlink" title="String为什么不可变?"></a>String为什么不可变?</h3><ul><li>因为String底层是private修饰的final类型的char数组</li></ul><blockquote><p>Java9之后改用byte数组,原因是部分字符仅占一个byte,而堆中含有大量的String字符串,该优化能节省较多空间。</p></blockquote><ul><li><p>value数组创建出来后本身长度不可变,final修饰后也不能修改为其他引用</p></li><li><p>并且String本身是final类,也就是不可被继承的</p></li></ul><p><strong>以上都是不考虑反射的说法</strong></p><h3 id="强制修改String"><a href="#强制修改String" class="headerlink" title="强制修改String"></a>强制修改String</h3><p><strong>通过反射可以修改String</strong></p><p><strong>两块测试代码块应独立测试,因为第一部分代码块的isInPool会对常量池造成影响</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">isInPool</span><span class="params">(String str)</span>{</span><br><span class="line"> <span class="keyword">return</span> str == str.intern();</span><br><span class="line">}</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="type">String</span> <span class="variable">s1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"String of "</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">s2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"test"</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">s</span> <span class="operator">=</span> s1 + s2;</span><br><span class="line"> <span class="type">String</span> <span class="variable">origin</span> <span class="operator">=</span> <span class="string">"String of test"</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment">//用于检测常量池字面量是否被修改</span></span><br><span class="line"> <span class="comment">//如果被修改,那么intern返回的应该是常量池中地址,跟对象地址应该不一样</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">s3</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"Reflect of "</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">s4</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">String</span>(<span class="string">"test"</span>);</span><br><span class="line"> <span class="type">String</span> <span class="variable">s5</span> <span class="operator">=</span> s3 + s4;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Influential---修改常量池中对象</span></span><br><span class="line"><span class="comment"> * 如果修改了,那么等下检测时str == str.intern()肯定会是true,</span></span><br><span class="line"><span class="comment"> * 因为常量池中不存在修改后的字面量,所以intern返回的是对象本身</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> System.out.println(<span class="string">"修改堆对象"</span>);</span><br><span class="line"> System.out.println(<span class="string">"修改前地址:"</span>+System.identityHashCode(s));</span><br><span class="line"> System.out.println(<span class="string">"修改前值:"</span>+s);</span><br><span class="line"> <span class="type">Field</span> <span class="variable">value</span> <span class="operator">=</span> s.getClass().getDeclaredField(<span class="string">"value"</span>);</span><br><span class="line"> value.setAccessible(<span class="literal">true</span>);</span><br><span class="line"> <span class="type">char</span>[] o = (<span class="type">char</span>[]) value.get(s);</span><br><span class="line"> System.out.println(<span class="string">"反射数据地址:"</span>+System.identityHashCode(o));</span><br><span class="line"> o[<span class="number">0</span>] = <span class="string">'R'</span>;</span><br><span class="line"> o[<span class="number">1</span>] = <span class="string">'e'</span>;</span><br><span class="line"> o[<span class="number">2</span>] = <span class="string">'f'</span>;</span><br><span class="line"> o[<span class="number">3</span>] = <span class="string">'l'</span>;</span><br><span class="line"> o[<span class="number">4</span>] = <span class="string">'e'</span>;</span><br><span class="line"> o[<span class="number">5</span>] = <span class="string">'c'</span>;</span><br><span class="line"> o[<span class="number">6</span>] = <span class="string">'t'</span>;</span><br><span class="line"> System.out.println(<span class="string">"修改后地址:"</span>+System.identityHashCode(s));</span><br><span class="line"> System.out.println(<span class="string">"修改后值:"</span>+s);</span><br><span class="line"> System.out.println(isInPool(s));<span class="comment">//true</span></span><br><span class="line"> System.out.println(<span class="string">"地址:"</span>+System.identityHashCode(s));</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Influential---修改String对象</span></span><br><span class="line"><span class="comment"> * 如果修改了常量池中String字面量,那么等下检测时str == str.intern()应该要是false,</span></span><br><span class="line"><span class="comment"> * 因为常量池按理已经存在了修改后的字面量,所以intern返回的常量池中地址</span></span><br><span class="line"><span class="comment"> * 那么为什么会是true?</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> System.out.println(<span class="string">"===============分割线==============="</span>);</span><br><span class="line"> System.out.println(<span class="string">"下面是修改常量池"</span>);</span><br><span class="line"> System.out.println(<span class="string">"修改前地址:"</span>+System.identityHashCode(origin));</span><br><span class="line"> System.out.println(<span class="string">"修改前值:"</span>+origin);</span><br><span class="line"> <span class="type">Field</span> <span class="variable">value2</span> <span class="operator">=</span> origin.getClass().getDeclaredField(<span class="string">"value"</span>);</span><br><span class="line"> value2.setAccessible(<span class="literal">true</span>);</span><br><span class="line"> <span class="type">char</span>[] o2 = (<span class="type">char</span>[]) value2.get(origin);</span><br><span class="line"> System.out.println(<span class="string">"反射数据地址:"</span>+System.identityHashCode(o2));</span><br><span class="line"> o2[<span class="number">0</span>] = <span class="string">'R'</span>;</span><br><span class="line"> o2[<span class="number">1</span>] = <span class="string">'e'</span>;</span><br><span class="line"> o2[<span class="number">2</span>] = <span class="string">'f'</span>;</span><br><span class="line"> o2[<span class="number">3</span>] = <span class="string">'l'</span>;</span><br><span class="line"> o2[<span class="number">4</span>] = <span class="string">'e'</span>;</span><br><span class="line"> o2[<span class="number">5</span>] = <span class="string">'c'</span>;</span><br><span class="line"> o2[<span class="number">6</span>] = <span class="string">'t'</span>;</span><br><span class="line"> System.out.println(<span class="string">"修改后地址:"</span>+System.identityHashCode(origin));</span><br><span class="line"> System.out.println(<span class="string">"修改后值:"</span>+origin);</span><br><span class="line"></span><br><span class="line"> System.out.println(isInPool(s5));<span class="comment">//true</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>对于上面第二部分代码的解释</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> <span class="type">String</span> <span class="variable">origin</span> <span class="operator">=</span> <span class="string">"String of test"</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment">//用于检测常量池字面量是否被修改</span></span><br><span class="line"> <span class="comment">//如果被修改,那么intern返回的应该是常量池中地址,跟对象地址应该不一样</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">s3</span> <span class="operator">=</span><span class="string">"Reflect of test"</span>;</span><br><span class="line"> System.out.println(System.identityHashCode(s3));<span class="comment">//true</span></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Influential---修改String对象</span></span><br><span class="line"><span class="comment"> * 如果修改了常量池中String字面量,那么等下检测时str == str.intern()应该要是false,</span></span><br><span class="line"><span class="comment"> * 因为常量池按理已经存在了修改后的字面量,所以intern返回的常量池中地址</span></span><br><span class="line"><span class="comment"> * 那么为什么会是true?</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> System.out.println(<span class="string">"===============分割线==============="</span>);</span><br><span class="line"> System.out.println(<span class="string">"下面是修改常量池"</span>);</span><br><span class="line"> System.out.println(<span class="string">"修改前地址:"</span> + System.identityHashCode(origin));</span><br><span class="line"> System.out.println(<span class="string">"修改前值:"</span> + origin);</span><br><span class="line"> <span class="type">Field</span> <span class="variable">value2</span> <span class="operator">=</span> origin.getClass().getDeclaredField(<span class="string">"value"</span>);</span><br><span class="line"> value2.setAccessible(<span class="literal">true</span>);</span><br><span class="line"> <span class="type">char</span>[] o2 = (<span class="type">char</span>[]) value2.get(origin);</span><br><span class="line"> System.out.println(<span class="string">"反射数据地址:"</span> + System.identityHashCode(o2));</span><br><span class="line"> o2[<span class="number">0</span>] = <span class="string">'R'</span>;</span><br><span class="line"> o2[<span class="number">1</span>] = <span class="string">'e'</span>;</span><br><span class="line"> o2[<span class="number">2</span>] = <span class="string">'f'</span>;</span><br><span class="line"> o2[<span class="number">3</span>] = <span class="string">'l'</span>;</span><br><span class="line"> o2[<span class="number">4</span>] = <span class="string">'e'</span>;</span><br><span class="line"> o2[<span class="number">5</span>] = <span class="string">'c'</span>;</span><br><span class="line"> o2[<span class="number">6</span>] = <span class="string">'t'</span>;</span><br><span class="line"> System.out.println(<span class="string">"修改后地址:"</span> + System.identityHashCode(origin));</span><br><span class="line"> System.out.println(<span class="string">"修改后值:"</span> + origin);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//此时S3"Reflect of test"</span></span><br><span class="line"> <span class="comment">//origin为"Reflect of test"</span></span><br><span class="line"> System.out.println(<span class="string">"修改前常量池的string of reflect地址"</span>+System.identityHashCode(s3));<span class="comment">//true</span></span><br><span class="line"> System.out.println(<span class="string">"修改string of test产生的string of reflect地址"</span>+System.identityHashCode(origin));<span class="comment">//true</span></span><br><span class="line"> String ss=<span class="string">"Reflect of test"</span>;</span><br><span class="line"> System.out.println(System.identityHashCode(ss));</span><br><span class="line"></span><br><span class="line"> String sss=<span class="string">"String of test"</span>;</span><br><span class="line"> System.out.println(System.identityHashCode(sss));</span><br><span class="line"> System.out.println(origin);</span><br><span class="line"> System.out.println(sss);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这段代码会导致最后声明的sss输出结果变成Reflect of test</p><p><img src="image-20220410235820491-166140589670210.png" alt="image-20220410235820491"></p><p><strong>原因是:</strong></p><p>字符串常量池底层其实是个hash表,这也很好地解释了字符串常量池检查重复的机制,查重能达到O(1)的复杂度。</p><p>将一个字符串值存入常量池时,会在将hash值,字符串数组作为kv存入查重的hash表中。</p><blockquote><p>引用对象实体内容修改以后hash表里面字面量还是没变。</p></blockquote><p>上面这句话是错误的解释,常量的操作主要是三种</p><ul><li>一方面是编译时会优化</li><li>一方面显示声明会自动加常量池</li><li>一方面即时编译器也会优化</li></ul><p>实际上图中的问题是编译期间将多个常量只创建了一份,也就是通过的是class常量池,并非通过字符串常量池去重。</p><p>不过通过字面量显示声明的常量创建还是会走常量池的(无论是第一次创建还是后续重复创建)。</p><p>测试的时候可以通过将常量分别在多个类中编写来避免这种编译期间的去重(并且使用-Xint指定节解释执行)。</p><p>SymbolTable底层是基于hashtable实现的,结构是数组+链表,当存储的数据足够多,遇到hash碰撞严重时(Hotspot触发rehash的条件是:查找一个字符串超过100次),是通过切换hash算法实现的。</p><h2 id="intern源码研究-JDK8"><a href="#intern源码研究-JDK8" class="headerlink" title="intern源码研究(JDK8)"></a>intern源码研究(JDK8)</h2><p><a href="https://blog.csdn.net/qq_19648191/article/details/117358082">https://blog.csdn.net/qq_19648191/article/details/117358082</a></p><p><a href="https://www.freesion.com/article/61901174982/">https://www.freesion.com/article/61901174982/</a></p><p><a href="https://blog.csdn.net/qq_33678688/article/details/89091930">https://blog.csdn.net/qq_33678688/article/details/89091930</a></p><p><a href="https://zhuanlan.zhihu.com/p/28973077">https://zhuanlan.zhihu.com/p/28973077</a></p><ol><li><p><code>hotspot/src/share/vm/prims/jvm.cpp</code></p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// String support ///////////////////////////////////////////////////////////////////////////</span></span><br><span class="line"></span><br><span class="line"><span class="built_in">JVM_ENTRY</span>(jstring, <span class="built_in">JVM_InternString</span>(JNIEnv *env, jstring str))</span><br><span class="line"> <span class="built_in">JVMWrapper</span>(<span class="string">"JVM_InternString"</span>);</span><br><span class="line"> JvmtiVMObjectAllocEventCollector oam;</span><br><span class="line"> <span class="keyword">if</span> (str == <span class="literal">NULL</span>) <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"> oop string = JNIHandles::<span class="built_in">resolve_non_null</span>(str);</span><br><span class="line"> oop result = StringTable::<span class="built_in">intern</span>(string, CHECK_NULL);</span><br><span class="line"> <span class="keyword">return</span> (jstring) JNIHandles::<span class="built_in">make_local</span>(env, result);</span><br><span class="line">JVM_END</span><br></pre></td></tr></table></figure></li><li><p><code>hotspot/src/share/vm/classfile/symbolTable.cpp</code></p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">oop <span class="title">StringTable::intern</span><span class="params">(oop string, TRAPS)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (string == <span class="literal">NULL</span>) <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line"> <span class="function">ResourceMark <span class="title">rm</span><span class="params">(THREAD)</span></span>;</span><br><span class="line"> <span class="type">int</span> length;</span><br><span class="line"> <span class="comment">// 创建string的句柄</span></span><br><span class="line"> <span class="function">Handle <span class="title">h_string</span> <span class="params">(THREAD, string)</span></span>;</span><br><span class="line"> <span class="comment">// 转换成Unicode编码的玩意,这里似乎是重新开空间创建一个Unicode的字符串</span></span><br><span class="line"> <span class="comment">// 但是在后面这个Unicode字符串只用于计算哈希值和查找比对,除非下一个intern方法中发现句柄没有创建才会用到这个Unicode来创建句柄</span></span><br><span class="line"> jchar* chars = java_lang_String::<span class="built_in">as_unicode_string</span>(string, length,</span><br><span class="line"> CHECK_NULL);</span><br><span class="line"> <span class="comment">// 调用最终的intern</span></span><br><span class="line"> oop result = <span class="built_in">intern</span>(h_string, chars, length, CHECK_NULL);</span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p><code>hotspot/src/share/vm/classfile/symbolTable.cpp</code></p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// string是个handle,chars是转成了unicode的原字符串</span></span><br><span class="line"><span class="function">oop <span class="title">StringTable::intern</span><span class="params">(Handle string_or_null, jchar* name,</span></span></span><br><span class="line"><span class="params"><span class="function"> <span class="type">int</span> len, TRAPS)</span> </span>{ y</span><br><span class="line"> <span class="type">unsigned</span> <span class="type">int</span> hashValue = <span class="built_in">hash_string</span>(name, len);</span><br><span class="line"> <span class="type">int</span> index = <span class="built_in">the_table</span>()-><span class="built_in">hash_to_index</span>(hashValue);</span><br><span class="line"> oop found_string = <span class="built_in">the_table</span>()-><span class="built_in">lookup</span>(index, name, len, hashValue);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Found</span></span><br><span class="line"> <span class="keyword">if</span> (found_string != <span class="literal">NULL</span>) <span class="keyword">return</span> found_string;</span><br><span class="line"></span><br><span class="line"> <span class="built_in">debug_only</span>(StableMemoryChecker <span class="built_in">smc</span>(name, len * <span class="built_in">sizeof</span>(name[<span class="number">0</span>])));</span><br><span class="line"> <span class="built_in">assert</span>(!Universe::<span class="built_in">heap</span>()-><span class="built_in">is_in_reserved</span>(name),</span><br><span class="line"> <span class="string">"proposed name of symbol must be stable"</span>);</span><br><span class="line"></span><br><span class="line"> Handle string;</span><br><span class="line"> <span class="comment">// try to reuse the string if possible</span></span><br><span class="line"> <span class="comment">// 如果这个句柄已经创建好了能直接用那就直接用</span></span><br><span class="line"> <span class="keyword">if</span> (!string_or_null.<span class="built_in">is_null</span>()) {</span><br><span class="line"> string = string_or_null;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="comment">// 如果句柄没有创建好那就从转为Unicode编码后的这个字符串创建一个句柄</span></span><br><span class="line"> <span class="comment">// 目前看来似乎是编译期间使用双引号创建字面量会走这个逻辑,字符串常量池会直接创建一个string</span></span><br><span class="line"> string = java_lang_String::<span class="built_in">create_from_unicode</span>(name, len, CHECK_NULL);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Grab the StringTable_lock before getting the_table() because it could</span></span><br><span class="line"> <span class="comment">// change at safepoint.</span></span><br><span class="line"> <span class="function">MutexLocker <span class="title">ml</span><span class="params">(StringTable_lock, THREAD)</span></span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Otherwise, add to symbol to table</span></span><br><span class="line"> <span class="comment">// 向stringtable里面添加句柄</span></span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">the_table</span>()-><span class="built_in">basic_add</span>(index, string, name, len,</span><br><span class="line"> hashValue, CHECK_NULL);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p><code>hotspot/src/share/vm/classfile/symbolTable.cpp</code></p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">oop <span class="title">StringTable::basic_add</span><span class="params">(<span class="type">int</span> index_arg, Handle string, jchar* name,</span></span></span><br><span class="line"><span class="params"><span class="function"> <span class="type">int</span> len, <span class="type">unsigned</span> <span class="type">int</span> hashValue_arg, TRAPS)</span> </span>{</span><br><span class="line"></span><br><span class="line"> <span class="built_in">assert</span>(java_lang_String::<span class="built_in">equals</span>(<span class="built_in">string</span>(), name, len),</span><br><span class="line"> <span class="string">"string must be properly initialized"</span>);</span><br><span class="line"> <span class="comment">// Cannot hit a safepoint in this function because the "this" pointer can move.</span></span><br><span class="line"> No_Safepoint_Verifier nsv;</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Check if the symbol table has been rehashed, if so, need to recalculate</span></span><br><span class="line"> <span class="comment">// the hash value and index before second lookup.</span></span><br><span class="line"> <span class="type">unsigned</span> <span class="type">int</span> hashValue;</span><br><span class="line"> <span class="type">int</span> index;</span><br><span class="line"> <span class="keyword">if</span> (<span class="built_in">use_alternate_hashcode</span>()) {</span><br><span class="line"> hashValue = <span class="built_in">hash_string</span>(name, len);</span><br><span class="line"> index = <span class="built_in">hash_to_index</span>(hashValue);</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> hashValue = hashValue_arg;</span><br><span class="line"> index = index_arg;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Since look-up was done lock-free, we need to check if another</span></span><br><span class="line"> <span class="comment">// thread beat us in the race to insert the symbol.</span></span><br><span class="line"> <span class="comment">// 如果已经有了就不用添加了并且可以直接返回</span></span><br><span class="line"> oop test = <span class="built_in">lookup</span>(index, name, len, hashValue); <span class="comment">// calls lookup(u1*, int)</span></span><br><span class="line"> <span class="keyword">if</span> (test != <span class="literal">NULL</span>) {</span><br><span class="line"> <span class="comment">// Entry already added</span></span><br><span class="line"> <span class="keyword">return</span> test;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// HashtableEntry 里面保存hash值以及这个字符串的引用(_literal),有了引用就可以得到字符串的字面量和地址了</span></span><br><span class="line"> HashtableEntry<oop, mtSymbol>* entry = <span class="built_in">new_entry</span>(hashValue, <span class="built_in">string</span>());</span><br><span class="line"> <span class="built_in">add_entry</span>(index, entry);</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">string</span>();</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ol><h2 id="补充"><a href="#补充" class="headerlink" title="补充"></a><strong>补充</strong></h2><p><strong>关于内存分配位置</strong></p><ol><li><p><code>hotspot/src/share/vm/runtime/handles.inline.hpp</code></p><p>句柄的创建用的是线程本地内存,也就是堆空间</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">inline</span> <span class="title function_">Handle::Handle</span><span class="params">(Thread* thread, oop obj)</span> {</span><br><span class="line"> assert(thread == Thread::current(), <span class="string">"sanity check"</span>);</span><br><span class="line"> <span class="keyword">if</span> (obj == <span class="literal">NULL</span>) {</span><br><span class="line"> _handle = <span class="literal">NULL</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> _handle = thread->handle_area()->allocate_handle(obj);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p><code>hotspot/src/share/vm/classfile/symbolTable.cpp</code></p><p>Unicode字符串的创建也是用的堆空间</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">jchar* <span class="title function_">java_lang_String::as_unicode_string</span><span class="params">(oop java_string, <span class="type">int</span>& length, TRAPS)</span> {</span><br><span class="line"> typeArrayOop value = java_lang_String::value(java_string);</span><br><span class="line"> <span class="type">int</span> offset = java_lang_String::offset(java_string);</span><br><span class="line"> length = java_lang_String::length(java_string);</span><br><span class="line"></span><br><span class="line"> jchar* result = NEW_RESOURCE_ARRAY_RETURN_NULL(jchar, length);</span><br><span class="line"> <span class="keyword">if</span> (result != <span class="literal">NULL</span>) {</span><br><span class="line"> <span class="keyword">for</span> (<span class="type">int</span> index = <span class="number">0</span>; index < length; index++) {</span><br><span class="line"> result[index] = value->char_at(index + offset);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> THROW_MSG_0(vmSymbols::java_lang_OutOfMemoryError(), <span class="string">"could not allocate Unicode string"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// hotspot/src/share/vm/memory/allocation.hpp</span></span><br><span class="line"><span class="meta">#<span class="keyword">define</span> NEW_RESOURCE_ARRAY_RETURN_NULL(type, size)\</span></span><br><span class="line"><span class="meta"> (type*) resource_allocate_bytes((size) * sizeof(type), AllocFailStrategy::RETURN_NULL)</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// hotspot/src/share/vm/memory/resourceArea.cpp</span></span><br><span class="line"><span class="comment">// Allocation in thread-local resource area</span></span><br><span class="line"><span class="keyword">extern</span> <span class="type">char</span>* <span class="title function_">resource_allocate_bytes</span><span class="params">(<span class="type">size_t</span> size, AllocFailType alloc_failmode)</span> {</span><br><span class="line"> <span class="keyword">return</span> Thread::current()->resource_area()->allocate_bytes(size, alloc_failmode);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>StringTable的创建实际上是在nativememory</p><ol><li><p><a href="https://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html">https://docs.oracle.com/javase/8/docs/technotes/guides/vm/enhancements-7.html</a></p><p>官方文档说明说的是将interned strings移到堆内存中,而非StringTable</p><blockquote><p><strong>Area</strong>: HotSpot<br><strong>Standard/Platform</strong>: JDK 7<br><strong>Synopsis</strong>: In JDK 7, interned strings are no longer allocated in the permanent generation of the Java heap, but are instead allocated in the main part of the Java heap (known as the young and old generations), along with the other objects created by the application. This change will result in more data residing in the main Java heap, and lessw data in the permanent generation, and thus may require heap sizes to be adjusted. Most applications will see only relatively small differences in heap usage due to this change, but larger applications that load many classes or make heavy use of the <code>String.intern()</code> method will see more significant differences.<br><strong>RFE</strong>: <a href="http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6962931">6962931</a></p></blockquote></li><li><p>深入理解Java虚拟机中<code>3.2.2 可达性分析算法</code>写到</p><blockquote><p>在Java技术体系里面,固定可作为GC Roots的对象包括以下几种: </p><p>·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的 参数、局部变量、临时变量等。 </p><p>·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。 </p><p>·<strong>在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用</strong></p></blockquote></li><li><p>R大说的</p><blockquote><p>常量池”如果说的是SymbolTable / StringTable,这俩table自身原本就一直在native memory里,是它们所引用的东西在哪里更有意思。上面说了,java7是把SymbolTable引用的Symbol移动到了native memory,而StringTable引用的java.lang.String实例则从PermGen移动到了普通Java heap。</p></blockquote></li><li><p>StringTable的创建源码</p><p>hotspot/src/share/vm/memory/universe.cpp</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">jint <span class="title function_">universe_init</span><span class="params">()</span> {</span><br><span class="line"></span><br><span class="line"> ......</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (UseSharedSpaces) {</span><br><span class="line"> <span class="comment">// Read the data structures supporting the shared spaces (shared</span></span><br><span class="line"> <span class="comment">// system dictionary, symbol table, etc.). After that, access to</span></span><br><span class="line"> <span class="comment">// the file (other than the mapped regions) is no longer needed, and</span></span><br><span class="line"> <span class="comment">// the file is closed. Closing the file does not affect the</span></span><br><span class="line"> <span class="comment">// currently mapped regions.</span></span><br><span class="line"> MetaspaceShared::initialize_shared_spaces();</span><br><span class="line"> StringTable::create_table();</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> SymbolTable::create_table();</span><br><span class="line"> StringTable::create_table();</span><br><span class="line"> ClassLoader::create_package_info_table();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> ......</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>调用链(jdk12)</p><p><img src="image-20220720175742728-16614058967029.png" alt="image-20220720175742728"></p><p>最终创建一个CHeap对象,不在 Java 的几个托管内存里,其实也就是native memory</p></li></ol></li></ol><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>总的来说,字符串常量池一直在nativememory中</p><p>而JDK1.7之后interned strings被移动到了堆内存中,而StringTable保存的是字符串的hashvalue以及字符串本身</p><p>另外,SymbolTable也在native memory中,至于SymbolTable的作用见<a href="https://hllvm-group.iteye.com/group/topic/26412#post-187861">https://hllvm-group.iteye.com/group/topic/26412#post-187861</a></p><blockquote><p>这些Utf8常量在HotSpot VM里以symbolOopDesc对象(下面简称symbol对象)来表现;它们可以通过一个全局的SymbolTable对象找到。注意:constantPool对象并不“包含”这些symbol对象,而只是引用着它们而已;或者说,constantPool对象只存了对symbol对象的引用,而没有存它们的内容。</p></blockquote><p><a href="https://www.zhihu.com/question/29352626/answer/44050736">https://www.zhihu.com/question/29352626/answer/44050736</a></p><blockquote><p>Symbol内嵌的存储不是char数组。如果要CONSTANT_String(String)引用了CONSTANT_Utf8(Symbol),那么ldc在初始化这个String对象的时候会从Symbol那边读取内容并创建对应的char[]以及String对象。</p></blockquote><blockquote><p>Symbols在JDK7和JDK8里都在native memory里,但不在Metaspace里。<br>Symbols在native memory里通过引用计数管理,同时有个全局的SymbolTable管理着所有Symbol。</p></blockquote>]]></content>
<summary type="html"><h2 id="关于常量池"><a href="#关于常量池" class="headerlink" title="关于常量池"></a>关于常量池</h2><h3 id="常量池-Class常量池(Constant-pool)"><a href="#常量池-Class常量池(Constant-pool)" class="headerlink" title="常量池/Class常量池(Constant pool)"></a>常量池/Class常量池(Constant pool)</h3><p>常量池,也叫 Class 常量池(常量池 == Class常量池)。Java文件被编译成 Class文件,Class文件中除了包含类的版本、字段、方法、接口等描述信息外,还有一项就是常量池,常量池是当Class文件被Java虚拟机加载进来后存放在方法区 各种字面量 (Literal)和 符号引用 。</p></summary>
<category term="JVM" scheme="https://wtmonster.github.io/categories/JVM/"/>
<category term="JVM" scheme="https://wtmonster.github.io/tags/JVM/"/>
<category term="常量池" scheme="https://wtmonster.github.io/tags/%E5%B8%B8%E9%87%8F%E6%B1%A0/"/>
<category term="HotSpot" scheme="https://wtmonster.github.io/tags/HotSpot/"/>
</entry>
</feed>