JVM P2 执行引擎,StringTable
教程:https://www.bilibili.com/video/BV1PJ411n7xZ
Java 17 文档:https://docs.oracle.com/javase/specs/jvms/se17/html
4. 执行引擎
物理机的执行引擎直接建立在处理器、缓存、指令集和 OS 层面上的,而虚拟机的执行引擎是由软件自行实现的,因此不受硬件支持的指令集的约束。
- JVM 负责将字节码文件加载到内部
- 执行引擎的任务是将字节码指令解释/编译为对应平台上的本地机器指令
Note:
执行引擎相当于一个翻译,将虚拟机的指令转换为物理机指令
是不是和 MySQL 的执行引擎有点类似?是不是和 Hypervisor 类似?是不是和 Docker Engine 有点类似?
执行引擎
- 输入:字节码二进制流
- 处理过程:字节码解析执行
- 输出:执行结果
4.1 Java 代码编译和执行过程
- 黄色的是 javac 执行的(前端编译器)(java 代码 ==> class 字节码文件)
- 绿色是解释的过程(class 字节码 ==> 本地机器指令)
- 蓝色是即时编译的过程(java 代码 ==> 本地机器指令)
为什么说 Java 是半编译半解释型语言?
JVM 在执行 Java 代码时,通常会将解释执行(解释器)和编译执行(JIT编译器)二者结合进行。
为什么需要字节码?我直接 Java 文件直接通过虚拟机不行吗(类似 Python )?
@chunquedong :
编译时优化的例子:连续的字符串使用 +
进行拼接,编译时会被优化成 StringBuffer 类型进行拼接
解释器和 JIT 编译器如何配合执行?
JVM 启动后
- 解释器可以先发挥作用,不需要等待即时编译器全部编译完成在执行,省去不必要的编译时间
- 随着程序运行,JIT 编译器开始发挥作用,根据热点探测功能(HotSpot JVM 的来源),将有价值的字节码编译为本地机器指令,换取更高的程序执行效率
- 当编译器进行激进优化不成立的时候,解释器可以作为一个后备方案
4.2 解释器
主要职责:字节码 ==> 本地机器指令
- 当一条字节码指令被解释执行完成后,再根据 PC 寄存器中的记录的下一条需要被执行的字节码指令解释执行
解释器设计实现比较简单,节省内存,响应较,但是执行效率低。
Java 为了解决该问题,支持了一种 JIT 编译的技术,即时编译将整个函数体编译成为机器码,每次函数执行时,只执行编译后的机器码即可,大大提升执行效率。
4.3 JIT 编译器(Just in Time)即时编译器
JIT 编译器和解释器能相互协作执行,尽力选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。
JIT 编译器执行速度快,响应较慢。
Jconsole
和 jvisualvm
可以看到相对应的编译次数和编译时间。
HotSpot JVM 分两种:
- C1 编译器(Client 端)的优化:
- 方法内联:将引用的函数代码编译到引用点处,减少栈帧的生成,减少参数传递以及跳转过程
- 去虚拟化:对唯一的实现类进行内联
- 冗余消除:把运行期间不会执行的代码折叠
- C2 编译器(Server 端)的优化主要在全局层面,基于逃逸分析在 C2 上有如下优化:
- 标量替换
- 栈上分配(HotSpot 没实现,本质还是标量替换后在栈上分配的)
- 同步消除
Note:
HotSpot JVM 默认开启 Tiered Compilers,综合了 C1 的高启动性能及 C2 的高峰值性能。详细可以参考:https://blog.csdn.net/u013490280/article/details/108522427
热点代码以及探测方式
执行频率高的代码被称之 “热点代码”,JIT 编译器对 “热点代码” 进行优化,翻译为本地机器指令以提高程序执行性能。
- 一个被多次调用的方法,或者一个方法体内部循环次数较多的循环体都可以被称之为 “热点代码”,因为该编译方式发生在方法执行过程中,因此称为栈上替换(On Stack Replacement)
- 被调用多少次?这里依靠基于计数器的热点探测功能(详细先不扩展了 P115)
编译器概念
- 前端编译器(javac,ECJ):java 文件 ==> class 字节码文件
- 后端运行的编译器(JIT 编译器,C1,C2):class 字节码 ==> 机器码
- 静态提前编译器(AOT 编译器,GCJ,JET):java 文件 ==> 机器码
4.4 AOT 编译器
在程序运行之前进行编译,可以实现 java 文件 ==> 机器码,并把机器码放置生成的动态共享库中
JDK 9 引入,Java 17 又删除了。静态提前编译器,在程序运行之前进行编译。
4.5 JVM 设置参数
Xint
:完全采用解释器模式Xcomp
:完全采用 JIT 编译器模式,若 JIT 编译出现问题,解释器会介入执行Xmixed
:采用混合模式共同执行(默认模式)
5. StringTable
5.1 基本特性
String 声明为 final,不可被继承:
public final class String
内部存储:
- JDK 9 以及以后的版本使用字节数组存储
private final byte[] value;
- JDK 8 使用
private final char[] value;
不可变性:
- 重新赋值需要重新制定内存
字符串常量池中不会存储相同内容的字符串的:
- String 常量池底层是个固定大小的 HashTable
- JDK17 默认表大小 65536
5.2 内存分配
Java 包装类型默认缓存:
直接使用双引号的字面量会直接存入 StringTable,如果不是双引号声明的 String对象可以使用 intern()
方法存入 StringTable。
Java 7 之后 StringTable 放入了堆中。
- 因为在永久代中默认比较空间小,且永久代垃圾回收频率低
5.3 基本操作
- 同样的 Unicode 字符序列必须执行同一个 String
5.4 字符串拼接操作
- 常量和常量的拼接结果在常量池,原理是编译期优化
- 常量池中不允许存在相同内容的常量
- 只要其中有一个是变量,结果在堆中。变量拼接的原理是
StringBuilder
- 我用的 JDK17 没有用
StringBuilder
,用的makeConcatWithConstants<invokedynamic>
- 我用的 JDK17 没有用
- 如果拼接的结果调用 intern() 方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
例子:
编译期优化:
String s1 = "a" + "b" + "c"; // 编译时期自动拼接为 “abc”
String s2 = "abc";
System.out.println(s1 == s2); // 因此结果为 true
变量拼接:
String a = "a";
String s1 = a + "b" + "c"; // 和变量进行拼接,s1 会赋值为一个堆中创建 String 对象的引用
String s2 = "abc";
System.out.println(s1 == s2); // false
final
拼接:
final String a = "a";
String s1 = a + "b" + "c"; // 编译时期进行拼接,相当于 “abc”
String s2 = "abc";
System.out.println(s1 == s2); // true
intern()
:
String a = "a";
String s1 = a + "b" + "c";
String s2 = "abc";
System.out.println(s1.intern() == s2); // true, intern() 方法会从 StringTable 中拿值
Note:
如果能基本确定需要拼接字符串最后的大小,可以使用 StringBuilder 的构造器提前设置数组大小。防止扩容导致的性能损失。
5.5 intern() 的使用
如果 StringTable 中没有对应的字符串,则在 StringTable 中生成并返回 StringTable 中的字符串地址,如果 StringTable 中有,则返回 StringTable 中的字符串地址。
new String("ab")
会创建几个对象?- 2 个
- 堆中创建一个,StringTable 中创建一个
new String("a") + new String("b")
会创建几个对象?- 6 个
new StringBuilder
,new String("a")
,"a"
,new String("b")
,"b"
,StringBuilder 中的 toString() 执行的new String(char[], 0, count)
String s3 = new String("3") + new String("3");
s3.intern();
String s4 = "33";
System.out.println(s3 == s4); // true
这是因为 Java 7 之后做了优化, s3.intern()
执行的时候,如果存在堆中的字符串对象,会直接在 StringTable 保存对象的引用,而不会重新创建对象。
详细可以看一下美团技术文章。
练习
String s = new String("a") + new String("b");
// 此时 StringTable 中没有 "ab"
String s2 = s.intern();
// 此时 StringTable 保存了在堆中的 "ab" 对象的引用
System.out.println(s2 == "ab");// true
System.out.println(s == "ab"); // true
String x = "ab";
// 此时 StringTable 中有 "ab"
String s = new String("a") + new String("b");
String s2 = s.intern();
// s2 为 StringTable 中 "ab" 的引用
System.out.println(s2 == "ab");// true
System.out.println(s == "ab"); // false
String s1 = new String("a") + new String("b");
s1.intern();
String s2 = "ab";
System.out.println(s1 == s2); // true
String s1 = new String("ab");
s1.intern();
String s2 = "ab";
System.out.println(s1 == s2); // false
System.out.println(s1 == "ab"); // false
System.out.println(s2 == "ab"); // true
Note:
对于程序中大量存在重复字符串,使用intern()
可以节省内存空间。
5.6 StringTable 的垃圾回收
-XX:+PrintStringTableStatistics
5.7 G1 中的 String 去重操作
详细可参考:https://openjdk.org/jeps/192
为了解决以下问题,在堆中多次 new 出相同的字符串的对象,这样就造成了资源浪费:
String s1 = new String("hello");
String s2 = new String("hello");
String s3 = new String("hello");
UseStringDeduplication
默认是不开启的。