【JVM系列】JVM内存模型详解

JVM一直是面试中经常问到的知识点,除此之外,在项目性能调优上,也需要对其十分了解。本系列会从内存模型,再到整个对象的创建流程以及垃圾回收等,将JVM的知识以及面试常见题型全部讲解一遍。

上图是Java 8的JVM内存模型,如果有面试准备或经历的人,肯定非常熟悉了。JVM 内存布局规定了 Java 在运行过程中内存申请、分配、管理的策略,保证了 JVM 的高效稳定运行,其中,堆是最大的区域。下面,会对每个区域展开讲解。

虚拟机栈

首先要知道的是,对于每一个线程,JVM 都会在线程被创建的时候,创建一个单独的栈。而对于线程中的每一个方法,都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。即:栈对应线程,栈帧对应方法。每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。所以就不难理解为什么虚拟机栈是线程私有的了。

局部变量表

存放方法参数和方法内部定义的局部变量的区域。

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

看下面这个例子

public int test(int a, int b) {
    Object obj = new Object();
    return a + b;
}

在上面这段代码中,各变量在局部变量表的存储方式如下图所示。

如果局部变量是 Java 的 8 种基本基本数据类型,则存在局部变量表中,如果是引用类型。例如 new 出来的 String,局部变量表中存的是引用,而实例在堆中。

操作栈

是一个栈结构。当 JVM 为方法创建栈帧的时候,在栈帧中为方法创建一个操作数栈,保证方法内指令可以完成工作。

描述可能不好理解其工作过程,我们还是通过一个例子来说明

public class Test {
    public static int add(int a,int b){
        return a+b;
    }
}

首先,通过 javac 命令生成 .class 文件,再用 javap 命令反汇编查看汇编指令。

然后打开1.txt,查看反编译之后的指令,下面我截取了关键的部分,整个工作过程,通过看注释,应该能明白了。

虽然反汇编之后的代码,已经很接近人能解读的了,但还是有些困难,需要参照:JVM指令对照表

public static int add(int, int);
    descriptor: (II)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2 // 最大栈深度为2 局部变量个数为3
         0: iload_0 // 将第一个 int 型局部变量推送至栈顶
         1: iload_1 // 将第二个 int 型局部变量推送至栈顶
         2: iadd // 将栈顶两 int 型数值相加并将结果压入栈顶
         3: ireturn // 从当前方法返回 int
      LineNumberTable:
        line 8: 0

动态连接

指向 运行时常量池该栈帧所属方法 的 引用。

可能光是读上面这一句,就不太好理解,那再来详细说明一下。

在一个字节码文件中,描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的。那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

方法出口

记录方法被调用的位置,等方法退出后,返回。

方法退出的过程相当于弹出当前栈帧,退出可能有三种方式:

  • 返回值压入上层调用栈帧
  • 异常信息抛给能够处理的栈帧
  • PC 计数器指向方法调用后的下一条指令

本地方法栈

为虚拟机执行 Native 方法服务。

程序计数器

是一块较小的内存空间。它可以看作是当前线程所执行的字节码的行号指示器。

因为代码是在线程中运行的,线程有可能被挂起。为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储。

方法区(元空间)

存储已被虚拟机加载的类元数据信息、常量、静态变量等。

在Java 8中,永久代(≈方法区)被移出了堆,移到了元空间,把字符串常量池移至堆区。需要注意的是,元空间并不在虚拟机中,而是使用本地内存。

运行时常量池

存放编译期生成的各种字面量和符号引用。

它是内存区域中最大的一块区域,被所有线程共享,存储着几乎所有的实例对象、数组,同时,也是垃圾收集器管理的主要区域,以及,是 OOM 故障最主要的发生区域。

其中,可以分为 新生代老年代 ,新生代又可以分为 伊甸区幸存0区幸存1区,内存划分的比例,如模型图所示。它们之间的工作关系,通过下面这个新对象的内存分配流程来详细说明。

 

绝大部分新对象都会在 伊甸区 生成(即在此区分配内存),当 伊甸区 装满的时候,会触发 Minor GC(垃圾回收)。

Minor GC 也叫做 Young GC。垃圾回收的时候,伊甸区 会实现清除策略,没有引用的对象则会直接回收。依然存活的会被送到 幸存区幸存区分为两块,每次 Minor GC 的时候,会将存活的对象,复制到未使用的那块,然后正在使用的这块空间完全清除。

如果Minor GC要移送的对象大于 幸存区 的容量,则直接交给 老年代。若 老年代 也满了,会触发 Full GC。默认情况下,一个对象从 新生代晋升到老年代 的阈值为15,即在 幸存区 交换14次后,晋升到 老年代

通过上面这个流程图以及讲解,能够很清楚的了解到堆中新生代和老年代之间的工作过程,同时还简单的提到了Minor GC 、Full GC 和 垃圾回收。在下一篇的内容中,我会详细讲到这些知识点。

参考

图文并茂,傻瓜都能看懂的 JVM 内存布局

The Structure of the Java Virtual Machine

人已赞赏
数据结构编程语言

时间复杂度与空间复杂度

2020-5-30 20:29:41

Java基础编程语言

【JVM系列】JVM对象创建与垃圾回收

2020-6-7 15:39:37

2 条回复 A文章作者 M管理员
  1. sjian99

    加油~加油~加油~

  2. 我是一只鱼

    加油~加油~加油~

个人中心
今日签到
有新私信 私信列表
搜索