《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了

简介: 《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了

你的手边有第2版吗?

来,翻到第57页。这里面有个“坑”,看看你当时发现了没,有没有在这页做笔记呢?

image.png

没有也没关系,我带你先回顾一下这一页的内容,再让你看看我三年前第一次看这书的时候做的笔记。

第2版57页讲了啥?

也许你根本就没看过《深入理解Java虚拟机(第2版)》这本书。但是你一定见过位于本书第57页的示例代码:

image.png

由于JDK 6常量池位于方法区,JDK 7以后常量池位于堆中,所以用两个版本的jdk跑上面的代码就会出现神奇的事情。甚至用JDK 8来跑,也会出现你想不到的结果。且听我慢慢道来。

先说一下intern是干啥的。

该方法的作用是把首次遇到的字符串加载到常量池中

再看一下intern的注释:

image.png

其中标记了☆的红框翻译过来就是:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。

回到最开始的代码中。引用第三版的描述如下:

这段代码在JDK 6中运行,会得到两个false,而在JDK 7中运行,会得到一个true和一个false。

产生差异的原因是:在JDK 6中,intern()方法会把首次遇到的字符串实例复制到永久代的字符串常量池中存储,返回的也是永久代里面这个字符串实例的引用,而由StringBuilder创建的字符串实例在Java堆上,所以必然不是同一个引用,将返回false。

而JDK 1.7(以及部分其他虚拟机,例如JRocki)的intern()实现就不需要再拷贝字符串的实例到永久代了,既然字符串常量池已经移到了Java堆中,那只需要在常量池里记录一下首次出现的实例引用即可,因此intern()返回的引用和由StringBuilder创建的那个字符串实例就是同一个。

对str2比较返回false是**因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过,**字符串常量池中已经有它的引用了,不符合intern()方法要求“首次出现”的原则,而“计算机软件”这个字符串则是首次出现的,因此返回true。

image.png


挖坑不填,坑哭读者

读到这里你有没有一些不惑呢?有没有感觉到一丝丝不对呢?

我们再看看原文:


image.png


为什么在JDK 7里面会返回fasle,上面红框框起来的部分是关键答案:

因为“java”这个字符串在执行StringBuilder.toString()之前已经出现过。

这句话就是“坑”,已经出现过?在哪出现的,你倒是告诉我啊!我当时的内心想法和下面的老大哥是一样一样的:


我第一次看这本书是在2016年,看这个地方的时候,我就百思不得其解,在哪就出现过了呀?

当时也不知道是在哪个写的似是而非的博客里面找到“java是关键字,已存在常量池中”这句“骚话”。还正正经经的抄了上去,虽然是错误的描述,虽然字是丑了点......


你当年或者现在看的时候有这个疑惑吗?

之后我又完整的看过几次这本书,我清楚的记得,我再一次看到这里的时候我就觉得**“java是关键字,已存在常量池中”这个描述是不对的**,所以我在后面打了一把叉,再次去找了相关资料,找到了sun.misc.version,终于解决了“在哪出现的”这个问题。

第3版注脚填坑

这个2013年(第二版出版那年)挖下的坑,在2016年10月1日,就被R大在知乎上给填上了。R大的这个回答也被作者周志明写在了2019年底出版的《深入理解Java虚拟机(第三版)》的注脚里面:

image.png

里面的RednaxelaFX就是R大,一个把虚拟机玩到极致,凭一己之力撑起了知乎java半边天的男人,后面我会详细介绍一下的。

你只要了解到一点就行:他的回答,就是权威。


在R大的这个知乎回答中,帮周志明大大填了这个坑,我强烈建议你一定要去看看,链接如下

https://www.zhihu.com/question/51102308/answer/124441115

R大帮忙填坑

我这里只是结合R大的回答和个人的一点点经验,谈谈自己的认知。

在2016年我读这本书的时候,我才刚刚大学毕业,刚从新手村出来。当时的我对于这个问题是绝对没有任何思路的,必须直接在网上查询答案。

现在的我,略有一点经验,再次遇到这个问题,就算没看书中的描述、R大的回答,我肯定也会想到:在书中的示例里面,第二个输出false,说明调用main方法之前,肯定在字符串常量池里面已经有了这个“java”字符串了。

怎么验证一下呢?

我们在main方法的第一行打上一个断点,debug运行程序后,可以看到Memory,然后过滤出String,如下:

image.png

然后双击过滤出来的java.lang.String,可以看到下图:

image.png

在这个页面我们可以继续过滤:

image.png

果然,在程序还没执行第14行之前,“java”已经出现了。

从这个结果我们可以推断出:Java标准库在JVM启动过程中加载的部分,可能里面就有类里有引用“java”字符串字面量,这个字面量被初次引用的时候就会被intern,加入到字符串常量池中去。

**而到底是哪个类导致了这个“java”字符串被intern的呢?**R大主要就是回答了这个问题。

我截取一下R大最终的答案,具体探索的过程去看他的回答吧,很强很硬核:

image.png

我们可以看到sun.misc.Version里面的launcher_name字段的值就是“java”:

image.png

而根据R大的回答,我们可以找到java.lang.System类:

image.png

根据System类的注释我们可以知道,它是由虚拟机自动调用的。而其initializeSystemClass方法会调用sun.misc.Version.init()方法。

到此就真相大白了。

Java标准库在JVM启动过程中会调用sun.misc.Version的init()方法。所以sun.misc.Version会进行类加载的操作,而类加载的初始化阶段时,会对静态常量字段进行真正的赋值操作,但是由于sun.misc.Version的launcher_name字段是final修饰的,所以引用的字符串“java”在准备阶段就被intern到了字符串常量池里面了。

可以在心里在默默的复习一下类加载的过程:加载、验证、准备、解析和初始化这五个阶段哦。

另外书中给出的示例代码也有一定的局限性,R大是这样说的:

其实这事情很简单:首先,这个行为必然是要针对某个具体的JDK/JRE实现来讨论的,因为Java语言规范/JVM规范/Java SE标准库的JavaDoc(也是Java SE平台规范的一部分)都没有、也不会强制指定哪个类里一定要引用“java”这个字符串常量,而且它必须是第一个使得“java”被intern的类 --- 规定这个也太无聊了。

比如这个示例我在JDK8u212-b03上跑出来,就是两个true:

image.png

在这个版本里面,sun.misc.Version的launcher_name变成了“openjdk”:

image.png

那么根据我们之前的猜测,把程序成下面这样的,效果就是一样的了:


image.png

万变不离其宗,现在你知道为什么这里用openjdk返回也是false了吧。

知其然,还要知其所以然。


目录
打赏
0
0
1
0
93
分享
相关文章
《深入理解Java虚拟机》第2版挖的坑终于在第4版中被R大填平了 (下)
《深入理解Java虚拟机》第2版挖的坑终于在第4版中被R大填平了 (下)
1701 0
《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了 (上)
《深入理解Java虚拟机》第2版挖的坑终于在第3版中被R大填平了 (上)
210 0
|
2月前
|
【Java并发】【线程池】带你从0-1入门线程池
欢迎来到我的技术博客!我是一名热爱编程的开发者,梦想是编写高端CRUD应用。2025年我正在沉淀中,博客更新速度加快,期待与你一起成长。 线程池是一种复用线程资源的机制,通过预先创建一定数量的线程并管理其生命周期,避免频繁创建/销毁线程带来的性能开销。它解决了线程创建成本高、资源耗尽风险、响应速度慢和任务执行缺乏管理等问题。
173 60
【Java并发】【线程池】带你从0-1入门线程池
Java网络编程,多线程,IO流综合小项目一一ChatBoxes
**项目介绍**:本项目实现了一个基于TCP协议的C/S架构控制台聊天室,支持局域网内多客户端同时聊天。用户需注册并登录,用户名唯一,密码格式为字母开头加纯数字。登录后可实时聊天,服务端负责验证用户信息并转发消息。 **项目亮点**: - **C/S架构**:客户端与服务端通过TCP连接通信。 - **多线程**:采用多线程处理多个客户端的并发请求,确保实时交互。 - **IO流**:使用BufferedReader和BufferedWriter进行数据传输,确保高效稳定的通信。 - **线程安全**:通过同步代码块和锁机制保证共享数据的安全性。
73 23
|
30天前
|
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
当我们创建一个`ThreadPoolExecutor`的时候,你是否会好奇🤔,它到底发生了什么?比如:我传的拒绝策略、线程工厂是啥时候被使用的? 核心线程数是个啥?最大线程数和它又有什么关系?线程池,它是怎么调度,我们传入的线程?...不要着急,小手手点上关注、点赞、收藏。主播马上从源码的角度带你们探索神秘线程池的世界...
101 0
【源码】【Java并发】【线程池】邀请您从0-1阅读ThreadPoolExecutor源码
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
144 14
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
66 13
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
3月前
|
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
141 17
|
4月前
|
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
100 1
Java—多线程实现生产消费者
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等