跳至主要內容

JVM组成结构

IT王小二约 3058 字

JVM组成结构

作者:IT王小二

博客:https://itwxe.comopen in new window

一、JavaSE体系

  • JavaSE,Java 平台标准版,为 Java EE 和 Java ME 提供了基础。
  • JDK:Java 开发工具包,JDK 是 JRE 的超集,包含 JRE 中的所有内容,以及开发程序所需的编译器和调试程序等工具。
  • JRE:Java SE 运行时环境 ,提供库、Java 虚拟机和其他组件来运行用 Java 编程语言编写的程序。主要类库,包括:程序部署发布、用户界面工具类、继承库、其他基础库,语言和工具基础库。
  • JVM:Java 虚拟机,负责 JavaSE 平台的硬件和操作系统无关性、编译执行代码(字节码)和平台安全性。

二、运行时数据区

  • 线程私有:程序计数器、虚拟机栈、本地方法栈。
  • 线程共享:堆、方法区。

运行时数据区

三、程序计数器

1. 什么是程序计数器

程序计数器是一块较小的内存空间,它的作用可以看作是当前线程所执行的字节码的行号指示器。在虚拟机的概念模型里字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 -- 摘自《深入理解Java虚拟机》

2. 程序计数器有什么特点

  • 程序计数器会随着线程的启动而创建,各线程之间独立存储,互不影响。
  • 当前线程执行的字节码的行号指示器
  • 如果线程正在执行的是一个 Java 方法,则指明当前线程执行的代字节码行数。
  • 如果正在执行的是 Natvie 方法(本地方法),这个计数器值则为空(Undefined)。
  • 占用较小的内存空间,此内存区域是唯一一个不会出现 OutOfMemoryError(内存溢出) 情况的区域。

3. 用个例子来说明

请无视我文章中取得类名,为了方便实验演示,命名怎么快怎么来。

public class Jvm1 {

    public int test(){
        int a = 100;
        int b = 200;
        return a + b;
    }
}

这样一个类, javac Jvm1.java,编译成Jvm1.class文件。

再使用 javap 反汇编工具javap -c Jvm1.class看下.class文件中数据格式。

javap

这个就是前面提到的 当前线程执行的字节码的行号,而程序计数器则记录的这个数字。

  • 当然这也解释了程序计数器不存在 OutOfMemoryError 的原因,因为它记录的只是数字,占用空间少。
  • 同时也解释了为什么执行的是一个 Java 方法时,则指明当前线程执行的代字节码行数。
  • 而执行 native方法 时程序计数器为 Undefined,因为 native方法 是大多是通过C实现并未编译成需要执行的字节码指令,所以在计数器中当然是空。

四、虚拟机栈

  • 栈有什么特点? 先进后出。
  • 虚拟机栈是每个线程私有的,线程在运行时,在执行每个方法的时候都会打包成一个 栈帧,存储了 局部变量表,操作数据栈,动态链接,方法出口等信息,然后放入栈。每个时刻正在执行的当前方法就是虚拟机栈顶的栈桢。方法的执行就对应着栈帧在虚拟机栈中入栈和出栈的过程。
  • 栈的大小缺省为 1M,可用参数 –Xss 调整大小,例如-Xss256k。

一个例子来看看执行 每个方法入栈出栈 的过程。

public class Jvm2 {

    public static void main(String[] args) {
        A();
    }

    public static void A() {
        System.out.println("A开始");
        // 此处省略100行代码
        B(); // 调用B方法
        System.out.println("A结束");
    }

    public static void B() {
        System.out.println("B开始");
        // 此处省略100行代码
        C(); // 调用B方法
        System.out.println("B结束");
    }

    public static void C() {
        System.out.println("C开始");
        // 此处省略100行代码
        System.out.println("C结束");
    }
}

A开始
B开始
C开始
C结束
B结束
A结束

1. 局部变量表

  • 顾名思义就是局部变量的表,用于存放我们的局部变量的。
  • 主要存放我们的 Java 的八大基础数据类型,如果是局部的一些对象,比如我们的 Object 对象,我们只需要存放它的一个引用地址即可。(基本数据类型、对象引用、returnAddress 类型)。

2. 操作数据栈

  • 存放我们方法执行的操作数的,它就是一个栈,先进后出的栈结构。
  • 操作数栈,就是用来操作的,操作的的元素可以是任意的 java 数据类型。
  • 所以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的,操作数栈运行方法是会一直运行入栈/出栈的操作。

数据重叠优化

虚拟机概念模型中每二个栈帧都是相互独立的,但在实际应用是我们知道一个方法调用另一个方法时,往往存在参数传递,这种做法在虚拟机实现过程中会做一些优化,具体做法如下:令两个栈帧出现一部分重叠。让下面栈帧的一部分操作数栈与上面栈帧的部分局部变量表重叠在一起,进行方法调用时就可以共用一部分数据,无须进行额外的参数复制传递。

重叠优化

3. 动态链接

需要类加载、运行时才能确定具体的方法。

栈帧中会持有一个引用(符号引用),该引用指向某个具体方法。

符号引用是一个地址位置的代号,在编译的时候我们是不知道某个方法在运行的时候是放到哪里的,这时我用代号 com/enjoy/pojo/User.Say:()V 指代某个类的方法,将来可以把符号引用转换成直接引用进行真实的调用。用符号引用转化成直接引用的解析时机,把解析分为两大类:

  • 静态解析:符号引用在类加载阶段或者第一次使用的时候就直接转换成直接引用。
  • 动态连接:符号引用在每次运行期间转换为直接引用,即每次运行都重新转换。

4. 方法出口

1、正常返回(调用程序计数器中的地址作为返回)三步曲

  • 恢复上层方法的局部变量表和操作数栈
  • 把返回值(如果有的话)压入调用者栈帧的操作数栈中
  • 调整 PC 计数器的值以指向方法调用指令后面的一条指令

2、异常返回

指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出

5. 栈溢出

  • java.lang.StackOverflowError:一般的方法调用是很难出现的,如果出现了要考虑是否有 无限递归 ,虚拟机栈带给我们的启示:方法的执行因为要打包成栈桢,所以天生要比实现同样功能的循环慢,所以树的遍历算法中:递归和非递归(循环来实现)都有存在的意义。递归代码简洁,非递归代码复杂但是速度较快。
  • OutOfMemoryError:不断建立线程(一般演示不出,演示出来机器也死了)。

五、本地方法栈

  • 本地方法栈和虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的 Native 方法服务。
  • 虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。
  • 本地方法栈 native 方法通过 JNI 调用到了底层的 C/C++(c/c++可以触发汇编语言,然后驱动硬件)。
  • 当一个JVM创建的线程调用native方法后,JVM不再为其在虚拟机栈中创建栈帧,JVM只是简单地动态链接并直接调用native方法。

六、方法区

主要存储类信息、常量池、静态变量、即时编译期编译后的代码等数据。

永久代和元空间

方法区在 jdk1.7 及其之前又背称为永久代,jdk1.8 又被称为元空间,怎么理解呢?

  • jdk1.8移除了永久代,新增了元空间。
  • 可以理解为方法区是一个规范,但是具体怎么实现要看具体的jvm怎么实现。
  • 就类似于提供了一个接口方法(规范),只要实现了这个接口的类,那么就要去实现里面接口方法(具体实现就是各种版本jvm之间和版本之间的差异了)。
  • 各种版本jvm 。
    • HotSpot VM(SUN) 以前使用范围最广的Java虚拟机。
    • JRockit VM(BEA) 号称世界上最快的JVM 。
    • Dalvik VM(Google) google自己开发的。
    • HotSPont VM(ORACLE) 目前以前使用范围最广的Java虚拟机。
  • 版本差异(jdk1.7, jdk1.8) 。

参数设置

  • jdk1.7 及以前:-XX:PermSize;-XX:MaxPermSize;
  • jdk1.8 以后:-XX:MetaspaceSize; -XX:MaxMetaspaceSize
  • jdk1.8 以后大小就只受本机总内存的限制

七、堆

  • 几乎所有对象都分配在堆内存,也是垃圾回收发生的主要区域。
  • 堆内存由多个线程共享。堆内存随着JVM启动而创建。

参数设置

-Xms:堆的最小值
-Xmx:堆的最大值
-Xmn:新生代的大小
-XX:NewSize;新生代最小值
-XX:MaxNewSize:新生代最大值

八、运行时常量池

1. 符号引用

  • 一个 java 类(假设为 People 类)被编译成一个 class 文件时,如果 People 类引用了 Tool 类,但是在编译时 People 类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。
  • 而在类装载器装载 People 类时,此时可以通过虚拟机获取 Tool 类的实际内存地址,因此便可以既将符号 org.simple.Tool 替换为 Tool 类的实际内存地址,及直接引用地址。
  • 即在编译时用符号引用来代替引用类,在加载时再通过虚拟机获取该引用类的实际地址。
  • 以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局是无关的,引用的目标不一定已经加载到内存中。

2. 字面量

  • 文本字符串 String a = "abc",这个 abc 就是字面量。
  • 八种基本类型 int a = 1; 这个 1 就是字面量。
  • 声明为 final 的常量。

3. jvm各版本运行时常量池变化

  • 运行时常量池:Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
  • JDK1.6:运行时常量池在方法区(永久代)中。
  • JDK1.7:运行时常量池在堆中。
  • JDK1.8:去永久代:使用元空间(空间大小只受制于机器的内存)替代永久代。

4. 直接内存

内存对象分配在JVM中堆以外的内存,也可以称为直接内存,这些内存直接受操作系统管理(而不是JVM),这样做的好处是能够在一定程度上减少垃圾回收对应用程序造成的影响。

  • 使用 Native 函数库直接分配堆外内存(NIO)。
  • 并不是 JVM 运行时数据区域的一部分,但是会被频繁使用(可以通过-XX:MaxDirectMemorySize 来设置(默认与堆内存最大值一样,也会出现 OOM 异常)。
  • 避免了在 Java 堆和 Native 堆中来回复制数据,能够提高效率。