运行时数据区

通过下图了解一下运行时数据区的各个组成部分:

image.png

线程独占区、线程共享区通过下图了解一下:

JVM 内存管理 - 图2

程序计数器

记录当前线程所执行的字节码的行号。

  • 程序计数器处于线程独占区;
  • 如果线程执行的是 Java 方法,这个计数器记录的是正在执行的字节码指令的地址,如果正在执行的是 native 方法,这个计数器的值为 undefined;
  • 此区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域;

问题:当前线程的字节码指令已经在执行了,为什么还要记录下来?

Java 中最小的执行单元是线程,线程是在 CPU 上执行的,而 CPU 是通过时间片的策略来执行的,多个线程在 CPU 上运行,时间片会被不同的线程抢占,当前正在执行的指令不一定能运行完,所以会把当前线程正在执行的字节码指令存下来。

虚拟机栈

存放方法运行时所需的数据,成为栈帧。

  • 虚拟机栈描述的是 Java 方法执行的动态内存模型;
  • 栈帧
    • 栈帧是虚拟机栈最小的出栈、入栈的单元。每个方法执行,都会创建一个栈帧,压入虚拟机栈中,当存在方法调方法的情况,会创建多个栈帧;
    • 栈帧用于存储局部变量表操作数栈动态链接方法出口等;
  • 当一个线程的栈深度大于虚拟机所允许的深度的时候,将会抛出 StackOverflowError 异常;

栈帧存储的内容如下图所示:

image.png

局部变量表

存放编译期可知的各种基本数据类型,引用类型,returnAddress 类型。

  • 局部变量表的内存空间在编译期完成分配,当进入一个方法时,这个方法需要在帧中分配多少内存是固定的,在方法运行期间是不会改变局部变量表的大小;
  • 局部变量表中的数据是定长的区块,一块是32位,如果是 int 类型的局部变量刚好存储成一块,如果是 long 类型的需要存放成两块;

操作数栈

操作数栈可理解为 Java 虚拟机栈中的一个用于计算的临时数据存储区。

  1. # 反汇编字节码文件后,得到操作指令
  2. javap -c Test.class > p.txt

JVM 内存管理 - 图4

上述方法执行的指令含义如下:

本地变量表:this,i,j。顺序为0,1,2

  1. 加载本地变量表变量1的值,压入操作数栈中;
  2. 加载本地变量表变量2的值,压入操作数栈中;
  3. 压入操作指令 iadd,计算出结果并将之前的弹出栈外;
  4. 返回 int 类型的值;

动态链接

多态情况下,一个接口有多个实现类,在方法执行时,执行真正的实例,需要用到动态链接。

问题:动态链接为什么存放在栈帧里面?

只有在方法执行的时候,才会去解析,指向真正的实例。

方法出口

当一个方法开始执行后,只有两种方式可以退出当前方法:

  • 当执行遇到返回指令,会将返回值传递给上层的方法调用者,这种退出的方式称为正常完成出口(Normal Method Invocation Completion),一般来说,调用者的程序计数器可以作为返回地址;
  • 当执行遇到异常,并且当前方法体内没有得到处理,就会导致方法退出,此时是没有返回值的,称为异常完成出口(Abrupt Method Invocation Completion),返回地址要通过异常处理器表来确定;

当方法返回时,可能进行3个操作:

  • 恢复上层方法的局部变量表和操作数栈;
  • 把返回值压入调用者调用者栈帧的操作数栈;
  • 调整程序计数器的值以指向方法调用指令后面的一条指令;

本地方法栈

为 JVM 所调用到的 native,即本地方法服务。

本地方法栈和虚拟机栈类似,区别是本地方法栈是为虚拟机执行 native 方法服务的,而虚拟机栈是为虚拟机执行 Java 方法服务的。

方法区

存储运行时常量池,已被虚拟机加载的类信息,常量,静态变量,即时编译器编译后的代码等数据。

当对空间不足且无法扩展,会抛出 OutOfMemoryError 异常。

问题一:方法区和永久代的关系?

Hotspot 虚拟机把 GC 分代收集扩展到了方法区中,使用永久代来实现方法区,好处是 Hotspot 垃圾收集器可以像管理堆一样管理方法区,可以省去为方法区编写内存管理代码的工作。其实这两者并不等价,只是对 Hotspot 虚拟机来讲是等价的,其他虚拟机不存在永久代的概念,虚拟机的规范也没有详细介绍永久代的概念。

堆(heap)

  • 堆(heap)是虚拟机中最大的一块内存区域了,被所有线程共享,在虚拟机启动时创建。它的目的是存放对象实例。
  • 堆是垃圾收集器管理的主要区域,因此很多时候也被称为 ‘GC’ 堆(Garbage Collected Heap);
  • 从垃圾回收的角度来讲,现在的收集器包括 Hotspot 都采用分代收集算法,所以堆又可以分为:新生代(Young)和老年代(Tenured),在细致一点,新生代又可分为 Eden、From Survivor、To Survivor 空间;
  • 从内存分配的角度来讲,又可以分为若干个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB);
  • 当对空间不足且无法扩展,会抛出 OutOfMemoryError 异常;

问题:所有的对象实例都会在堆上分配内存么?

不一定,参考《Java 逃逸分析》

运行时常量池

运行时常量池是属于方法区的一块。运行时常量池用于存放编译器生成各种字面量、以及符号引用,这部分内容将在类加载后,进入方法区的运行时常量池存放。

运行时常量池存放该类型所用到的常量的有序集合,包括直接常量(如字符串、整数、浮点数的常量)和对其他类型、字段、方法的符号引用。常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存中所有类型使用到的类型、字段、方法的字符引用,所以它也是动态链接的主要对象(在动态链接中起到核心作用)。

通过反汇编字节码文件后,得到操作指令:

  1. javap -v Test.class > p.txt

image.png

堆外内存(直接内存)

直接内存不是 Java 虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域,但是确实存在的一块内存区域,也被我们频繁的使用。

当申请不到堆外内存,也会抛出 OutOfMemoryError 异常。

问题:那么直接内存到底是什么呢?

内存对象分配在 Java 虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。使用未公开的 Unsafe 和 NIO 包下 ByteBuffer 来创建堆外内存。

作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/iol7g4 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。