深入理解JVM底层实现_1 JVM组成结构和对象的创建

                                     深入理解JVM底层实现

                                         1 JVM组成结构和对象的创建

                                                                                                                                                                                                田超凡

                                                                                                                                                                                        2019-11-06

转载请注明原作者

1 JVM组成结构

JVM全称是Java Virtual Machine(Java虚拟机),JVM是Java程序的编译和跨平台运行的核心和重要支撑所在。引入Java虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。

JVM在内存中的组成结构包括编译后生成的class文件、类加载器(ClassLoader)、运行时数据区、执行引擎、运行本地库接口组成。

JVM的核心是运行时数据区,在运行时数据区中,所有编译后得到的数据分为两个片区存放,也就是线程共享区和线程独占区。

线程共享区作用域为全局,是整个应用程序运行的所有线程共享的数据资源、

线程共享区主要由两部分组成,堆区和方法区(元空间)。

堆区:用来存放创建好的实例对象

方法区(元空间):存放运行时的常量池、Class类型对象、静态变量等。

线程独占区作用域为局部,是每一个线程单独享有的数据资源,对其他线程是不可用的。线程独占区和线程独占区之间相互独立、完全隔离,每一个程序运行的线程都会有对应的线程独占区。

线程独占区主要有三部分组成,虚拟机栈、本地方法栈、程序计数器

虚拟机栈:存放局部变量

本地方法栈:JVM所调用的使用native关键字修饰的本地JDK内置方法

程序计数器:记录当前线程执行到多少行代码,相当于一个指针指向当前线程执行到的代码行。

深入理解JVM底层实现_1 JVM组成结构和对象的创建

 

2 栈(Stack)的内存结构-转瞬即逝

2.1 栈(Stack)和栈桢(Stack Frame)

栈是一个后进先出的数据结构,在JVM内存结构中,栈存在于线程独占区,分为虚拟机栈和本地方法栈,栈的作用域一般都是在固定的有限界限中。虚拟机栈存放的是局部变量,本地方法栈存放的是JVM调用的本地JDK内部封装的native原生方法。每一个栈的生命周期都包含在所属线程的生命周期中,线程销毁之后对应的栈也会销毁。线程创建并运行时会动态创建对应的线程独占区(包括虚拟机栈、本地方法栈、程序计数器)来存放线程执行过程中产生的临时数据。

栈桢(Stack Frame)是每一个栈的基本组成单位。每一次函数的调用,都会在调用栈上维护一个独立的栈帧每一个栈可以有多个栈桢组成(具体每个栈的栈桢数视同一个栈的方法调用次数而定)。栈和栈桢的内存结构图如下:

深入理解JVM底层实现_1 JVM组成结构和对象的创建

 

每一个栈桢有四个部分组成:局部变量表、操作数栈、动态链接、返回地址

2.2 局部变量表(Local Variables Table,简称LVT)

局部变量表(Local Variables Table)也可以称之为本地变量表,它包含在一个独立的栈帧中。顾名思义,局部变量表主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类原始数据类型、对象引用(reference),以及returnAddress类型。局部变量表所需的容量大小在编译期就可以被完全确定下来,并保存在方法的Code属性中。如果是定义在方法体外的成员变量,不止是作用域发生了变化,更重要的是,其值也不再是存储在局部变量表里,而是存储在对象内存空间的实例数据中,整体来看也就是存储在Java堆区内(线程共享区)。简单来说,与每个线程上下文相关的数据存储在线程独占区对应的Java栈中(独占),反之则存储在线程共享区的Java堆区内(全局)。

 

2.3 操作数栈(Expression Stack,简称ESS)

每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈(Expression Stack)。操作数栈和局部变量表在访问方式上存在着较大差异,操作数栈并非采用访问索引的方式来进行数据访问的,而是通过标准的入栈和出栈操作来完成一次数据访问。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,一个32 bit的数值可以用一个单位的栈深度来存储,而2个单位的栈深度则可以保存一个64 bit的数值,当然操作数栈所需的容量大小在编译期就可以被完全确定下来,并保存在方法的Code属性中。

操作数栈栈深度和数值大小的换算公式是:1个单位栈深度=32 bit数据

 

2.4 动态链接(Dynamic Linking,简称DLK)

每一个栈帧内部除了包含局部变量表和操作数栈之外,还包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。在运行时常量池,一个有效的字节码文件中除了包含类的版本信息、字段、方法以及接口等描述信息外,还包含一项信息,那就是常量池表(Constant Pool Table),那么运行时常量池就是字节码文件中常量池表的运行时表示形式。在一个字节码文件中,描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用(Symbolic Reference)来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

 

2.5 方法返回值(返回地址)

一个方法在执行的过程中将会产生两种调用结果:一种是方法正常调用完成,而另外一种则是方法异常调用完成。如果是方法正常调用完成,那么这就意味着,被调用的当前方法在执行的过程中将不会有任何的异常被抛出,并且方法在执行的过程中一旦遇见字节码返回指令时,将会把方法的返回值返回给它的调用者,不过一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定。在字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用。

 

3 运行时常量池(方法区/元空间)

默认对象不是new的时候会直接把我们的字符串对象放入我们的运行时常量池之中,所有s1==s2返回是为true

对象使用new会后会在堆内存中创建新的对象,所以得到的S3==S1返回的值为false

在调用我们intern方法之后会把这个对象放入运行时常量池中。这时候因为常量池数据结构类似于HashSet,则他们引入地址值就会一样导致s3.intern() == s1返回true

public class Test2 {
    public static void main(String[] args) {
        String s1 ="abc";
        String s2 = "abc";
        System.out.println(s1 == s2);
        String s3 = new String("abc");
        System.out.println(s3 == s1);
        System.out.println(s3.intern() == s1);
    }
}

深入理解JVM底层实现_1 JVM组成结构和对象的创建

 

4 对象的创建过程

深入理解JVM底层实现_1 JVM组成结构和对象的创建

 

  1. 使用new关键字实例化对象
  2. 根据实例化的对象类型在运行时常量池中获取该类型对应的符号引用
  3. 如果这个符号引用不存在,则表示当前类还没有加载到类加载器,需要把当前类加载到类加载器并解析和初始化。
  4. 虚拟机为对象分配堆中的存放内存(常用的分配策略有指针碰撞、空闲列表、栈上分配、内存逃逸),需要注意这个操作是在线程共享区的堆区中完成的。
  5. 把在堆中分配的内存初始化为零值(不包括对象头)
  6. 调用对象的init方法初始化对象
  7. 执行对象的构造方法

 

5 对象内存分配-指针碰撞和空闲列表

5.1 指针碰撞

我们内存分配为规整的(每次分配依靠指针位移来分配对象)(但是我们电脑给Java分配的内存不一定是规整的,可能区域并不是连续的) ,如下图所示

深入理解JVM底层实现_1 JVM组成结构和对象的创建

 

5.2 空闲列表

堆内部有一个列表来存储我们堆中空闲的地方。我们创建对象则去找列表中对应的空闲区域去创建我们的对象。

 

深入理解JVM底层实现_1 JVM组成结构和对象的创建

5.3 内存分配规整性的决定因素

堆区的内存分配是否规整是有垃圾回收器来决定的,如果垃圾回收器带有压缩算法,那么他会规整的分配我们的对象占用的堆区内存(采用指针碰撞的对象内存分配方式)

 

5.4 对象内存分配的线程安全性

指针碰撞有线程安全问题使用cas无锁机制(UnSafe => public final native boolean compareAndSwapInt()),空闲列表则采用我们的本地线程分配缓存,线程占满则采用我们的cas加锁方式,再去使用本地线程分配缓存。

 

转载请注明原作者