> 文档中心 > JVM执行引擎、及基于JVM的对象的实例化过程

JVM执行引擎、及基于JVM的对象的实例化过程

目录

一、执行引擎 

二、对象的实例化过程

三、StringTable


在介绍JVM执行引擎前我们先了解一下JVM的整体结构:

总体分为三个部分:类加载子系统(Class Loader Subsystem)、运行时数据区(Runtime Data Area)、执行引擎(Execution Engine)

java程序执行过程:java程序经过编译后产生字节码文件(Class Files),字节码文件通过类加载器的(加载、链接、初始化)步骤加载进内存,然后执行引擎通过和运行时数据区的交互解释并执行文件。

类加载过程可详见博文: JVM类加载机制

运行时数据区可详见博文:JVM运行时数据区结构及原理

一、执行引擎 

【概述】

执行引擎是Java虚拟机核心的组成部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、缓存、指令集和操作系统层面上的,而虚拟机的执行引擎则是由软件自行实现的,因此可以不受物理条件制约地定制指令集与执行引擎地结构体系,能够执行那些不被硬件直接支持地指令集格式。

要想了解执行引擎是用来做什么的,我们先来看一看JVM虚拟机想要做什么?

        JVM的主要任务是负责装载字节码到其内部,但字节码并不能够直接运行在操作系统之上,因为字节码指令并非等价于本地机器指令,它内部包含的仅仅只是一些能够被JVM所识别的字节码指令、符号表,以及其他的辅助信息。
        那么,如果想要让一个Java程序运行起来,执行引擎(Execution Engine)的任务就是将字节码指令解释/编译为对应平台上的本地机器指令才可以(这也是Java跨平台特性的原因)。简单来说,JVM中的执行引擎充当了将高级语言翻译为机器语言的译者。

【执行引擎的结构】

执行引擎包括三部分:解释器、JIT编译器(即时编译器)、垃圾回收器

【执行引擎工作过程】

 执行引擎就是根据PC寄存器中的下一条指令的地址取得相应的指令,然后执行一条条的字节码指令,从而完成栈中一系列的操作(入栈、出栈)。

 从外观上看,所有的Java虚拟机的执行引擎的输入、输出都是一致的:输入的是字节码二进制流,处理过程是字节码解析执行的等效过程,输出的是执行结果。

【解释执行和JIT即时编译】

下图是Java代码编译和执行的过程:
橙色是java程序翻译成字节码文件的过程(也称前端编译),与虚拟机无关。
蓝色部分是JVM虚拟机将字节码文件编译成目标代码的过程(也称后端编译或JIT即时编译)。
绿色部分是JVM虚拟机解释执行字节码文件的过程。 

 解释执行需要通过解释器完成,JIT即时编译需要通过JIT编译器完成。那什么是解释器?什么是JIT编译器呢?

  • 解释器:当Java虚拟机启动时会根据预定义的规范对字节码采用逐行解释的方式执行,将每条字节码文件中的内容“翻译”为对应平台的本地机器指令执行。
  • JIT编译器:就是虚拟机将源代码直接编译成本地和本地机器平台相关的机器语言。

 

对于HotSpot VM是目前市面上高性能虚拟机代表作之一。它采用解释器与即时编译器并存的架构。在Java虚拟机运行时,解释器和即时编译器能够相互协作,各自取长补短,尽力去选择最合适的方式来权衡编译本地代码的时间和直接解释执行代码的时间。

对于HotSpot JVM来说,会随着程序运行时间的推移,即时编译器逐渐发挥作用,根据热点探测功能,将有价值的字节码编译为本地机器指令,以换取更高的程序执行效率。

二、对象的实例化过程

【对象创建的常见方式】

  • new(变形:Xxx的静态方法、XxxBuilder/XxxFactory的静态方法)
  • Class的newInstance():放射的方式,只能够调用空参的构造器,权限必须是public
  • Constructor的newInstance():反射的方式,可以调用空参、带参的构造器,权限没有要求
  • 使用Clone():不调用任何构造器,当前类需要实现Cloneable接口,实现clone()方法
  • 使用反序列化:从文件中、或从网络中获取一个对象的二进制流
  • 第三方库Objenesis

【对象创建的六个步骤】

  1. 加载类元信息:判断对象对应的类是否加载、链接、初始化

  2. 为对象分配内存

  3. 处理并发安全问题

  4. 属性的默认初始化(零值初始化)

  5. 设置对象的对象头

  6. 属性的显式初始化、代码块中初始化、构造器中初始化

【对象的内存布局】

【对象的访问定位】

对象的访问方式主要有两种:句柄访问、直接指针

句柄访问:

好处:reference中存储稳定句柄地址,对象被移动(垃圾收集时移动对象很普遍)时只会改变句柄中实例数据指针即可,reference本身不需要被修改。

直接指针:

三、StringTable

String字符串,使用一对""引起来表示。实现了Serializable接口,表示字符串是支持序列化的;也实现了Comparable接口,表示String可以比较大小。String声明为final,不可以被继承。在jdk8以前,其内部定义了final char[] value用于存储字符串数据,jdk9时改为bute[]。

定义String对象的两种方式:

String s1 = "hello";// 字面量的定义方式String s2 = new String("world");

【String的内存分配】

 从jdk6及以前 -----> jdk7 ------> jdk8及以后 字符常量池位置的变化可详见博文:JVM堆和方法区底层结构及原理

中方法区的演进模块。

【字符串拼接操作】

字符串拼接面试常见问题:

public class StringTableTest01{    public static void main(String[] args) { String s1 = "javaEE"; String s2 = "hadoop"; String s3 = "javaEEhadoop"; // 两个字面量的字符串+,在编译器就会优化成"javaEEhadoop" String s4 = "javaEE" + "hadoop"; // 如果拼接符号的前后出现了变量,则相当于在堆空间中new String(),具体的内容为拼接的结果"javaEEhadoop" String s5 = s1 + "hadoop"; String s6 = "javaEE" + s2; String s7 = s1 + s2; System.out.println(s3 == s4);// true System.out.println(s3 == s5);// false System.out.println(s3 == s6);// false System.out.println(s3 == s7);// false System.out.println(s5 == s6);// false System.out.println(s5 == s7);// false System.out.println(s6 == s7);// false // intern():判断字符串常量池中是否存在javaEEhadoop值,如果存在,则返回常量池中javaEEhadoop的地址 // 如果字符串常量池中不存在javaEEhadoop值,则在常量池中加载一份javaEEhadoop,并返回该对象的地址 String s8 = s6.intern(); System.out.println(s3 == s8);// true    }}

变量字符串相加的底层原理:是创建了一个StringBuilder对象(jdk5.0之后,之前用的是StringBuffer),并通过调用其 append() 方法将两个字符串添加进字符串缓冲区,最后调用 toString() 方法(类似于new String()),转换为字符串

如果字符串拼接符号两边都是字符互传常量或常量引用(final修饰),则仍然使用编译期优化,即非StringBuilder的方式。
针对于final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用上final的时候建议使用上(例如:使用final修饰字符串变量时,会将其当作字符串常量,在进行字符串拼接时,就不必再重新new一个对象存放在堆上了,直接再字符串常量池中判断有无)。

public class StringTableTest02 {    public static void main(String[] args) { String s1 = "javaEEhadoop"; String s2 = "javaEE"; String s3 = s2 + "hadoop"; System.out.println(s1 == s3);// false final String s4 = "javaEE";// final修饰的s4是字符串常量 String s5 = s4 + "hadoop"; System.out.println(s1 == s5);// true    }}

【intern()的使用】

即保证变量s指向的是字符串常量池中的数据的两种方式:

  • 方式一:
    • String s = "abcd";
  • 方式二:
    • String s = new String("abcd").intern()
    • String s = new StringBuilder("abcd").toString().intern();

下面来一道看似简单,但你大概率做不对的面试题:

public class StringTableTest03 {    public static void main(String[] args) { String s1 = new String("1"); s1.intern(); String s2 = "1"; System.out.println(s1 == s2); String s3 = new String("1") + new String("1"); s3.intern(); String s4 = "11"; System.out.println(s3 == s4);    }}

问两个输出的结果是多少?

在解答上述问题前,再抛出一个问题:
new String("ab");会创建几个对象?   new String("a") + new String("b");呢?

new String("ab");创建了2个对象:

  1. 首先,new关键字会在堆空间开辟一个空间,存放String类型的对象
  2. 其次,会在字符串常量池中会存放一个"ab"的对象。

new String("a") + new String("b");创建了6个对象:

  1. 拼接变量字符串时需要new StringBuilder()对象
  2. 在堆中new String("a")对象
  3. 常量池中的"a"对象
  4. 在堆中new String("b")对象
  5. 常量池中的"b"对象
  6. StringBuilder的toString()方法会new String("ab")对象

强调:在最后一步toString()方法的调用,不会在字符常量池中在生成"ab"对象

在弄明白这两件事后,我们就很容易地能够弄清楚第一个问题:

答案就是:

        在jdk6及以前:两个输出都为false

        在jdk7及以后:第一个输出false,第二个输出true

下面我们逐行解释一下代码:

public class StringTableTest03 {    public static void main(String[] args) { // 注意:这里的堆中"1"对象和字符串常量池中的"1"对象不是同一个对象 String s1 = new String("1");// s1指向堆中的"1"对象,并在字符串常量池生成"1"对象 s1.intern();// 调用此方法前,字符串常量池中已经存在了"1"对象,所以这里不必做任何事 String s2 = "1";// s2指向字符串常量池中的"1"对象 System.out.println(s1 == s2);// jdk6:false      jdk7/8:false // s3变量记录的地址为:new String("11"),s3指向堆中的"11"对象 // 并且在执行完下一句代码后,常量池中不存在"11"对象 String s3 = new String("1") + new String("1"); // 这里intern()在 jdk6及以前 和 jdk7及以后 的作用不同: //      jdk6:创建一个新的字符串常量池对象"11",也就是有新的地址 //      jdk7:此时常量池中并没有创建新的对象"11",而是指向堆空间中的对象"11" s3.intern();// 在字符串常量池中生成"11"对象 String s4 = "11";// s4指向字符串常量池中的"11"对象 System.out.println(s3 == s4);// jdk6:false      jdk7/8:true    }}

下面是在 jdk6 和 jdk7 环境下的图解:

 

对于程序中大量存在的字符串,尤其其中存在很多重复字符串时,使用intern()方法可以节省很多内存空间。