JVM-java中的虚拟机
第三章 jvm中的对象和执行引擎
文章目录
String StringBuilder 字符串常量池
String s = new String("abc");方式创建对象,在内存中创建了几个对象?
两个:一个是堆空间中new结构,另一个是char[]对应的常量池中的数据:"abc"
StringBuilder 自身创建的对象,确实只会在堆内存中生成,绝对不会在字符串常量池中产生对象。
s.intern() 的核心作用就是确保字符串在内存中的唯一性,并返回字符串常量池中的那个唯一引用
- 如果常量池里已经有这个字符串:直接返回常量池中已有字符串的引用。
- 如果常量池里没有这个字符串:在常量池中登记这个字符串(存入引用或复制对象),然后返回常量池中的引用。
- 在 JDK 1.7 及以上版本中,常量池不再复制一份对象,而是直接在常量池中记录下堆内存中 s3 对象的引用地址。
java
public void test3(){
String s1 = "a";
String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;//new StringBuilder().append("a").append("b").toString() --> new String("ab")
System.out.println(s3 == s4);//false
}
@Test
public void test4(){
final String s1 = "a";//变成了编译期常量
final String s2 = "b";
String s3 = "ab";
String s4 = s1 + s2;
System.out.println(s3 == s4);//true
}
//体会执行效率:
public void method1(){
String src = "";
for(int i = 0;i < 10;i++){
src = src + "a";//每次循环都会创建一个StringBuilder
}
System.out.println(src);
}
public void method2(){
StringBuilder src = new StringBuilder();
for (int i = 0; i < 10; i++) {
src.append("a");
}
System.out.println(src);
}
java
public class StringTest4 {
public static void main(String[] args) {
String s = new String("1");
s.intern();
String s2 = "1";
System.out.println(s == s2);//false
String s3 = new String("1") + new String("1");//这里的拼接在底层会通过 StringBuilder 实现
s3.intern();//在 JDK 1.7 及以上版本中,常量池不再复制一份对象,而是直接在常量池中记录下堆内存中 s3 对象的引用地址。
String s4 = "11";
System.out.println(s3 == s4);//true
}
}

上题变形
java
@Test
public void test1(){
String s3 = new String("1") + new String("1");
String s4 = "11";
s3.intern();
System.out.println(s3 == s4);//false
//原因:当执行 String s4 = "11"; 时,常量池里还没有 "11",于是常量池会自己创建一个 "11" 对象。
//随后执行 s3.intern(); 时,发现常量池里已经有 "11" 了,就什么都不做。
//最终 s3 指向堆内存,s4 指向常量池,两者地址不同,返回 false
//注意:那s3.intern()返回的s4引用的值,s3还是引用堆里面的值
}
对象内存布局
实例化
几种方式创建对象
- new 最常见的方式
变形1:Xxx的静态方法 例如:lass.forName(), List.of()
变形2:XxxBuilder/XxxFactory的静态方法 如 StringBuilder - Class的newlnstance():反射的方式,只能调用空参的构造器,权限必须是public
- Constructor的newInstance(Xxx):反射的方式,可以调用空参、带参的构造器,权限没有要求,实用性最广
- 使用clone():不调用任何构造器,当前类需要实现Cloneable接口,实现clone(),默认浅拷贝
- 使用反序列化:从文件中、数据库中、网络中获取一个对象的二进制流,反序列化为内存中的对象
- 第三方库Objenesis,利用了asm字节码技术,动态生成Constructor对象
创建对象执行步骤 new 对象()
1.判断对象对应的类是否加载、链接、初始化(前面介绍过,类加载过程)
2.为对象分配内存
指针碰撞

空闲列表

3.处理并发安全问题
在分配内存空间时,另外一个问题是及时保证new对象时候的线程安全性:创建对象是非常频繁的操作,虚拟机需要解决并发问题。虚拟机采用了两种方式解决并发问题:
- CAS (Compare And Swap失败重试、区域加锁:保证指针更新操作的原子性
- TLAB 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲区,(TLAB,Thread LocalAllocation Buffer)虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定
4.初始化分配到的空间
内存分配结束,虚拟机将分配到的内存空间都初始化为零值(不包括对象头)。这一步保证了对象的实例字段在Java代码中可以不用赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值。
5.设置对象的对象头
将对象的所属类(即类的元数据信息)、对象的HashCode和对象的GC信息、锁信息等数据存储在对象的对象头中。这个过程的具体设置方式取决于JVM实现。
6.执行init方法进行初始化
在Java程序的视角看来,初始化才正式开始。初始化成员变量,执行实例化代码块,调用类的构造方法,并把堆内对象的首地址赋值给引用变量。
因此一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之后会接着就是执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全创建出来。
访问定位
对象访问方式由虚拟机实现而定。主流有两种方式:
-
使用句柄访问

-
使用直接指针访问

java采用第二种
执行引擎
是什么
- 执行引擎是Java虚拟机核心的组成部分之一。
- "虚拟机"是一个相对于"物理机"的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎的结构体系,能够执行那些不被硬件直接支持的指令集格式。
- JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他辅助信息。
- 那么,如果想要让一个Java程序运行起来,执行引擎(ExecutionEngine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。
怎么工作的
从外观上来看,所有的Java虚拟机的执行引擎输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。

1)执行引擎在执行的过程中究竟需要执行什么样的字节码指令完全依赖于PC寄存器。
2)每当执行完一项指令操作后,PC寄存器就会更新下一条需要被执行的指令地址。
3)当然方法在执行的过程中,执行引擎有可能会通过香储在局部变量表中的对象引用准确定位到存储在Java堆区中的对象实例信息,以及通过对象头中的元数据指针定位到目标对象的类型信息。
代码编译和执行过程

过程一:javac.exe的执行:
Java代码编译是由Java源码编译器来完成,流程图如下所示:

过程二:java.exe的执行:
Java字节码的执行是由JVM执行引擎来完成,流程图如下所示:

半解释半编译

什么是解释器(Interpreter)?
当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行将每条字节码文件中的内容"翻译"为对应平台的本地机器指令执行。
解释器分类
在Java的发展历史里,一共有两套解释执行器,即古老的字节码解释器 、现在普遍使用的模板解释器 。
字节码解释器在执行时通过纯软件代码模拟字节码的执行,效率非常低下。而模板解释器将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的机器码,从而很大程度上提高了解释器的性能。
在HotSpot VM中,解释器主要由Interpreter模块和Code模块构成。
Interpreter模块:实现了解释器的核心功能
Code模块:用于管理HotSpot VM在运行时生成的本地机器指令.
JVM 采用了一种混合模式:
解释器 负责快速启动,哪里需要执行哪里,处理那些只跑一两次的代码。
JIT 编译器(动态编译) 在后台悄悄观察,一旦发现哪段代码被反复执行(也就是所谓的"热点代码"),就把它抓出来编译成机器码。
解释器:就是逐行翻译
JIT:是提前翻译一些热点代码。好处:碰见for循环1000,拿过来直接用,不需要像解释器那样翻译1000次
热点代码及探测方式
当然是否需要启动JIT编译器将字节码直接编译为对应平台的本地机器指令,则需要根据代码被调用执行的频率而定。关于那些需要被编译为本地代码的字节码,也被称之为"热点代码,JIT编译器在运行时会针对那些频繁被调用的"热点代码"做出深度优化,将其直接编译为对应平台的本地机器指令,以此提升Java程序的执行性能。
- 一个被多次调用的方法,或者是一个方法体内部循环次数较多的循环体都可以被称之为"热点代码",因此都可以通过JIT编译器编译为本地机器指令。由于这种编译方式发生在方法的执行过程中,因此也被称之为栈上替换,或简称为OSR(OnStackReplacement)编译。
- 一个方法究竟要被调用多少次,或者一个循环体究竟需要执行多少次循环才可以达到这个
标准?必然需要一个明确的阈值,JIT编译器才会将这些"热点代码"编译为本地机器指令执行。这里主要依靠热点探测功能。
目前HotSpot VM所采用的热点探测方式是基于计数器的热点探测。
采用基于计数器的热点探测,HotSpot VM将会为每一个方法都建立2个不同类型的计数器,分别为方法调用计数器(Invocation Counter)和回边计数器(BackEdgeCounter)。
- 方法调用计数器用于统计方法的调用次数
- 回边计数器则用于统计循环体执行的循环次数
在HotSpot VM中内嵌有两个JIT编译器
分别为Client Compiler和Server Compiler,但大多数情况下我们简称为C1编译器和C2编译器。开发人员可以通过如下命令显式指定Java虚拟机在运行时到底使用哪一种即时编译器,如下所示:
- client:指定Java虚拟机运行在Client模式下,并使用C1编译器;
C1编译器会对字节码进行简单和可靠的优化,耗时短。以达到更快的编译速度。
client启动快,占用内存小,执行效率没有server快,默认情况下不进行动态编译,适用于桌面应用程序 - server:指定Java虚拟机运行在Server模式下,并使用c2编译器。
C2进行耗时较长的优化,以及激进优化。但优化的代码执行效率更高。
server启动慢,占用内存多,执行效率高,适用于服务器端应用; - 由-XX:+RewriteFrequentPairs参数控制。client模式默认关闭,server模式默认开启。
C1和C2编译器不同的优化策略: - C1编译器上主要有方法内联,去虚拟化、冗余消除。
1.方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程:
2.去虚拟化:对唯一的实现类进行内联
3.冗余消除:在运行期间把一些不会执行的代码折叠掉。 - C2的优化主要是在全局层面,逃逸分析是优化的基础。基于逃逸分析在C2上有如下几种优化:
1.标量替换:用标量值代替聚合对象的属性值
2.栈上分配:对于未逃逸的对象分配对象在栈而不是堆
3.同步消除:清除同步操作,通常指synchronized