JVM学习——运行时数据区

介绍

Java虚拟机在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,已经创建和销毁时间,有的区域随着虚拟机进程的启动而创建,有些区域则依赖用户线程的启动和结束而创建和销毁。

Java 文件中定义的方法、变量、常量等进入内存后,存放的区域以及对应的变化。

Java 虚拟机内存空间就是一块普通的内存空间,只是这部分处理 Java 程序。

Java 虚拟机内存结构按线程数据是否共享分为两部分:

  • 线程共享

    • 方法区
    • 常量池
  • 线程私有

    • PC 寄存器
    • 本地方法栈
    • Java 虚拟机栈

各个分区的细节

基于 JDK 1.8

堆:存放对象的地方

upload successful

Dog dog = new Dog(); new Dog() 这个对象就在堆上

堆进一步可分为:年轻代、老年代。年轻代对象大多数存活时间短,很快会被垃圾回收。年轻代存活久的会进入老年代,当然有些对象比较大,会直接进入老年代。

年轻代内存分区进一步可分为 Eden、survivor0、survivor1 三个部分,内存默认大小比例为 8:1:1。

垃圾回收的时候, Eden 和其中一个 surivor 内的对象大部分会被清除,而没清除的放入另外一个 surivor 中(垃圾回收算法——复制算法)。

JDK 1.8 之前,堆中有永久代(Perm GEN)这个概念,JDK 1.8 开始,去掉永久代,取而代之的是元空间(MetaSpace),元空间不属于堆,元空间的大小仅受限于机器的内存大小。

当 Perm GEN 属于堆的时候,有时候由于堆内存大小不足,报 “java.lang.OutOfMemoryError: PermGen space” 错误,现在这个错误将不复存在;当然对应的 -XX:MaxPermSize 也将不起作用

One important change in Memory Management in Java 8

Java 8: From PermGen to Metaspace

Java8内存模型—永久代(PermGen)和元空间(Metaspace)

方法区

存放类的结构信息:运行时常量池、方法、构造方法

方法区是一个接口概念,是 Java 虚拟机定义的一个规范;而永久代、元空间则被认为是方法区这个规范的实现,并且永久代 HotSpot 虚拟机才有的概念,其它虚拟机没有。

JDK 1.7 时,永久代包含类的元信息、静态变量、常量池(Constant Pool Table);

JDK 1.8 开始元空间(元空间不属于堆,在机器的本地内存中)存储类的元信息。静态变量、常量池并入堆中。

常量池:用于存放编译期生成的各种字面量和符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

jdk8之后永久代去哪了? - Mr Zeng的回答 - 知乎

常量池

JVM常量池浅析

  • Class 文件常量池 class 文件中有定义,包括字面量和符号引用

    • 字面量:比较接近于 Java 层面的常量概念,如文本字符串、被声明为 final 的常量值等
    • 符号引用:

      • 类和接口的全限定名(即带有包名的 Class 名,如:org.lxh.test.TestClass)
      • 字段的名称和描述符(private、static 等描述符)

      • 方法的名称和描述符(private、static 等描述符)

  • 运行时常量池,JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。同时在 jdk 1.8中移除整个永久代,取而代之的是一个叫元空间(Metaspace)的区域

  • 全局字符串常量池

  • 基本类型包装类对象常量池

PC(Program Counter) 程序计数器

当前线程所执行的字节码的行号指示器。唯一一个无 OOM 的区域

如果这个方法不是 native 方法,那么 PC 寄存器就保存 Java 虚拟机正在执行的字节码指令地址。如果是 native 方法,那么 PC 寄存器保存的值是 undefined。

任意时刻,一条 Java 虚拟机线程只会执行一个方法的代码,而这个被线程执行的方法称为该线程的当前方法,其地址被存在 PC 寄存器中。

本地方法栈

「当 Java 虚拟机使用其他语言(例如 C 语言)来实现指令集解释器时,也会使用到本地方法栈。如果 Java 虚拟机不支持 natvie 方法,并且自己也不依赖传统栈的话,可以无需支持本地方法栈。」

Java 虚拟机栈

一个线程就包含一个虚拟机栈,与线程共存亡。

虚拟机栈有大小,如果栈的深度大于 JVM 所允许的范围,会抛出 StackOverflowError;如果申请不到额外空间,会抛出 OutOfMemoryError,这两种错误如果要捕获,需使用 Throwable 进行捕获。

描述的是 Java 方法执行的内存模型,线程执行一个方法时,虚拟机栈就会创建一个栈帧,栈帧内包含局部变量表,操作数栈。方法执行完退出,该栈帧就会清除。

栈帧内容:

  • 局部变量表:是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,其中存放的数据的类型是编译期可知的各种基本数据类型、对象引用(reference)和 returnAddress 类型(它指向了一条字节码指令的地址)。

    对象引用:强引用、软引用、弱引用、虚引用。(在垃圾回收里面详细说)

  • 操作数栈

    操作数栈的最大深度也是在编译的时候就确定了,当一个方法开始执行时,它的操作栈是空的,在方法的执行过程中,会有各种字节码指令(比如:加操作、赋值元算等)向操作栈中写入和提取内容,也就是入栈和出栈操作。

    Java 虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。

    基于栈的指令集最主要的优点是可移植性强,主要的缺点是执行速度相对会慢些;而由于寄存器由硬件直接提供,所以基于寄存器指令集最主要的优点是执行速度快,主要的缺点是可移植性差。

  • 动态链接

    每个栈帧都包含一个指向运行时常量池(在方法区中,后面介绍)中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。Class 文件的常量池中存在有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。

    这些符号引用,一部分会在类加载阶段或第一次使用的时候转化为直接引用(如 final、static 域等),称为静态解析,另一部分将在每一次的运行期间转化为直接引用,这部分称为动态连接。

  • 方法出口

    一般来说,方法正常退出时,调用者的 PC 计数器的值就可以作为返回地址,栈帧中很可能保存了这个计数器值,而方法异常退出时,返回地址是要通过异常处理器来确定的,栈帧中一般不会保存这部分信息。

博客参考

JVM系列第6讲:Java 虚拟机内存结构