Tips: 内存区, 也叫运行时数据区.

Tips: 没有单独注明的情况下, 本文主要讨论HotSpot虚拟机.

一般来讲, Java运行时数据区构成如图所示:


其中, 垃圾回收算法主要就运作于堆中.

线程私有部分

程序计数器

  • 程序计数器中存储了当前正在执行的字节码地址.

  • 如图所示, 和虚拟机栈、本地方法栈一样, 程序计数器是线程隔离的, 每个线程都有自己单独的程序计数器.

  • 单线程处理的场景下, 可以不用计数器, 因为程序可以根据字节码指令跳转到指定运行代码. 但是代码运行往往是由多个线程协同工作的. 一旦一个线程被挂起, 再次执行的时候, 就需要程序计数器来确认当前线程执行到了什么位置.

  • 程序计数器生命周期和线程同步, 创建/结束.

虚拟机栈

  • 虚拟机栈和计数器一样是线程私有的, 生命周期和线程同步.

  • 方法的调用数据通过栈进行传递, 每次调用都会有对应的栈帧被压栈, 方法调用结束之后会有一个栈帧被弹出.

栈帧

  • 栈中的元素为栈帧(Stack<栈帧>)的数据结构如图所示, 由局部变量、操作数、动态链接、方法返回地址构成.

  • java虚拟机栈栈顶的栈帧就是当前执行方法的栈帧. 当这个方法调用其他方法的时候就会创建一个新的栈帧, 并把新的栈帧压栈到栈顶.

  • 对于单个线程的虚拟机栈来说, 只有栈顶的栈帧才是有效帧, 也只有有效帧的本地变量才能被使用.

  • 有效栈帧(或者说活动栈帧、当前栈帧), 关联的方法被称为当前方法.

  • 当这个栈帧所有指令都完成的时候, 这个栈帧被弹栈, 新的活动栈帧产生, 被移除栈帧的返回值变为当前活动栈帧的一个操作数.

  • 栈帧中的局部变量表、操作数在编译完成之后就已经确定了. 所以一个栈帧需要分配的内存不受到程序运行期变量数据的影响, 仅取决于具体虚拟机的实现.

局部变量表
  • 官方定义: 局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量.
(1)public class Example {
(2)    public int func(int x, int y) {
(3)        long i = 1L;
(4)        {
(5)            long j = 1;
(6)            i = j;
(7)        }
(8)        int z = x + y;
(9)        return z;
(10)    }
(11)}

假如说有这样一段代码, 对应的编译后的Class为:

Code:
  stack=2, locals=7, args_size=3 
  // stack 代表操作数栈的对大深度
  // locals 代表局部容量的最大容量
  // args_size 代表方法参数个数, x、y、this 一共三个(成员方法会包含this, 静态方法不包含)
    ...
    省略一些字节码指令
    ...
  LineNumberTable:
    ...
    省略一些行号对应关系
    ...
  Example:
  // Start 代表字节码指令中的行号
  // Length 代表作用的有效行数, 比如 j 在5~8的作用域里是有效的.
  // Slot 代表变量槽的索引
  // Name 代表变量名
  // Signature 代表变量类型, I = int, J = long
    Start  Length  Slot  Name   Signature
        5       3     5     j   J
        0      16     0  this   example/Example;
        0      16     1     x   I
        0      16     2     y   I
        2      14     3     i   J
       13       3     5     z   I

变量槽(Slot)

  1. 变量槽一个槽的容量为 32bit.
  2. 变量槽是局部变量表的最小存储单元, 可以存储:
    i. 基本数据类型: byte、boolean、short、int、float、long、double、char
    ii. 引用数据类型: 含对象类型、数组类型, 对应引用数据的引用地址, 占用一个变量槽
  3. 变量槽是可以复用的, 如上所示, j 和 z 的变量槽索引都是 5, 因为变量j在作用完 5~8 之后就没有用了, 变量z从 13 开始作用到 16, 可以复用slot, 提高资源的利用率.
  • 局部变量表本质上是一个 int[], 特殊类型的如boolean用0和非0标识, 超出int范围的long 和 double 用两个连续元素存储, 获取的时候用开始的下标获取(比如说, long类型的i, 存储之后, 再到z的时候, 下标是5而不是4, 因为i占用了3~4).

  • 局部变量在超出作用域的时候, 如果此时发生GC, 变量不一定会被回收, 具体取决于变量在变量槽中是否被其他变量复用.

  • 线程执行一个方法的时候, 会把方法的参数放入局部变量表, 其中:

    1. 基本类型的变量和值都存储在局部变量表中.
    2. 引用类型会现在堆中创建对象, 再把引用放入局部变量表中.
    3. 局部变量在栈帧的内存占用比较高, 如果局部变量过多, 就会引起 SOE(StackOverFlow) 异常.
    4. 局部变量在进入局部变量表的时候没有初始化, 如果判定没有赋值就会编译失败.
操作数
  • 用于存放方法执行过程中产生的中间计算结果和计算过程中产生的临时变量.

  • 操作数栈的基础元素和局部变量表类似, 都是最大存储32bit, 存不下的用64bit, boolean转换为int类型等, 区别在于操作数的存储数据结构为后入先出栈, 随着方法执行, 回吧局部变量表或者对象实例中的数据进行压栈, 读取时再进行弹栈.

动态链接
  • 一个方法, 在内存中的直接引用, 存在于本地内存的方法区中的运行时常量池. 与之映射的, 就是每个虚拟机栈中的, 持有这个引用的动态链接.

  • 和类加载同步生成引用的, 成为静态解析, 在实例化的时候转化为引用信息的, 叫动态链接.

方法返回地址
  • 方法有两种返回方式, 异常返回和正常返回, 无论任何一种形式, 只要方法结束了, 栈帧就会被弹出销毁.

  • 一般来讲, 方法退出时, 需要返回方法调用的位置, 而调用位置一般就存储在方法返回地址中.

  • 方法退出时, 一般会执行:

    1. 恢复上层方法的局部变量表和操作数栈
    2. 把返回值压入调用者的操作数栈中
    3. 调整PC计数器的值以便于调用该方法的小一条指令的执行

Tips: 栈的存储空间不是无限的, 当程序陷入错误的大量循环时, 由于函数被反复调用, 栈中会不断的压入新的栈帧得不到释放, 当线程压入的栈深度超过虚拟机栈的最大深度时, 就会抛出 StackOverFlowError 的异常.

Tips: HotSpot虚拟机中, 栈的内存容量是不可以动态变化的, 只要线程申请栈内存空间成功, 就不会出现OOM(OutOfMemoryError 内存溢出)异常, 但是如果申请过程中出现了无法申请到足够的内存情况, 也会抛出OOM异常.

本地方法栈

  • 本地方法栈和虚拟机栈基本相同, 区别在于虚拟机栈中为执行Java字节码方法铺路调度, 而本地方法栈则为虚拟机使用的Native方法运筹帷幄, 同样创建栈帧, 用于存储局部变量表、操作数栈、动态链接、返回方法等.

  • 常见的native方法有: Thread的 start0(), Object 的 getClass()hashCode()clone()

Tips: 和虚拟机栈一样, 本地方法栈也会出现 SOF 和 OOM 的异常错误.

Tips: Native方法指的是被native关键字修饰的Java方法, 通常由其他语言实现, 来保证Java代码执行的效率. 可以视为Java调用非Java方法的接口.

字符串常量池

方法区

运行时常量池

直接内存

To Be Continued…