运行时数据区


概述

这部分是JVM中最为重要的部分。我们研究JVM主要的目的就是为了对JVM进行调优,本质上就是针对运行时数据区进行调优。

所谓运行时数据区,其实就是JVM对内存的使用

运行时数据区大致分为:

  1. 线程共享区域
    1. 方法区
  2. 线程独享区域
    1. 程序计数器
    2. 本地方法栈

示意图大致如下:

线程共享空间就是所有线程共同使用的内存空间,而独享空间,则是创建线程才有的空间,且每一个线程都有自己的这么一块空间。

方法区

方法区主要存放以下内容:

存储内容 说明
类型信息 Class信息
方法信息 如Method,含方法名称,参数列表,方法返回值等
字段信息 如Field,含字段类型,名称
Code区 存储方法执行对应的字节码指令
方法表 例如调用A类的某个method时,是根据A类的方法表找到对应的方法
静态变量 JDK1.7之后,移到堆中
运行时常量池
(字符串常量池最为重要)
从class文件中的常量池加载,JDK1.7之后移到堆中。
参考精通JVM(一):class文件详解
JIT编译器编译之后的代码缓存 JIT编译器将字节码编译成系统能够识别的机器码,为了效率进行了缓存

永久代与元空间

方法区其实只是一个统称,或者说是一种抽象,一种规范。而永久代和元空间则是方法区的具体实现

在JDK1.8之前使用的是永久代,JDK1.8开始使用的是元空间

两者之间的区别如下:

  1. 存储位置不同:

    1. 永久代占用的是JVM进程所使用的内存空间,大小受JVM空间限制。
    2. 元空间使用的是物理内存区域,只受机器物理内存大小限制
  2. 存储内容不同:

    1. 永久代存储如上面的表格所示的内容。
    2. 元空间只存储类的元信息。静态变量和运行时常量池都已经挪到了堆中

永久代和元空间的历史变成过程大致如下:

方法区OOM异常

方法区能够导致OOM异常的情况主要两种:

  1. 加载太多的类,超出方法区空间大小。
    1. JDK1.8之前将抛出java.lang.OutOfMemoryError: PerGem space
    2. JDK1.8之后将抛出java.lang.OutOfMemoryError: Metaspace
  2. 字符串OOM异常:当我们创建太多的字符串时,将发生OOM。
    1. JDK1.6及之前版本,字符串常量还在永久代中,因此异常为java.lang.OutOfMemoryError: PerGem space
    2. JDK1.7开始,字符串常量池在堆中,因此异常为java.lang.OutOfMemoryError:Java heap space

堆内存

Java堆是Java应用中最大的一块内存,几乎所有的对象都在堆上,例如我们new出来的对象,字符串常量等等。

堆内存是由垃圾收集器分配和管理,因此也叫做GC堆。

堆内存的分配与垃圾收集器息息相关,并不是固定不变的,不同的垃圾收集器堆内存可能是不一样的。

最为经典的是分代设计:主要划分为年轻代(Young)和老年代(Old),二者之间默认大小比例为1:2。

其中年轻代又具体划分为Eden,Survivor From, Survivor To三个区域,三者之间默认大小比例为8:1:1。

具体示意图如下:

由于不同的垃圾收集器的堆内存设计是不一样的,因此想要深入理解堆内存,建议参考垃圾收集器部分

程序计数器

程序计数器是线程私有的,也就是说每一次线程都会有一个程序计数器,它的作用是用来记录当前线程下一个要执行的字节码的行号。

比如有这样一段代码:

int a = 1;
int b = 3;
int c = a + b;

字节码指令如下:(使用IDEA插件jclasslib bytecode viewer查看)

左侧红色数字就是字节码的行号。

正是由于程序计数器记录了行号,当线程发生切换时才知道应该从哪里继续执行。

if,while等控制流语句的完成也依赖程序计数器来完成

如果正在执行的方法是native方法,那么程序计数器记录的值为空。

虚拟机栈

Java中方法是虚拟机执行的最基本单元,栈帧则是对应了一个方法。

当某一个方法A要执行时,对应的栈帧将入栈,如果方法A中又调用了方法B,那么执行到方法B时,B对应的栈帧也将入栈,位于栈顶。

也就是说栈顶的那一个栈帧就是当前正在执行的方法,因此称之为“当前栈帧”

一个栈帧包含以下内容:

  1. 局部变量表
  2. 操作数栈
  3. 动态链接
  4. 方法返回地址

局部变量表

局部变量表用于存放当前方法的局部变量,如方法入参,方法内部定义的局部变量等,可以保存boolean,byte,char,short,int,reference,returnAddress类型的数据。

其中reference是对一个对象实例的引用,returnAddress比较少见,它指向了一条字节码指令。

局部变量表中变量顺序为:

  1. this引用
  2. 方法参数
  3. 方法内声明的局部变量

在局部变量表中,存储变量是以变量槽(slot)为单位,像boolean,int这种占用四个字节的变量都占用一个slot,而double/long这种8字节的变量需要两个slot

操作数栈

操作数栈的基本数据结构是一个先进先出、后进后出的栈结构,它是JVM用于计算用的。

比如有一个方法如下:

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

查看该方法的字节码如下:

其中iload指令就是将加载int类型的变量到操作数栈中,查看JVM规范文档)可以证实这一点:

将a和b变量iload入栈之后,然后iadd指令会将栈顶的两个变量出栈并相加,然后把结果入栈,ireturn从操作数栈的栈顶元素出栈用作返回结果。

这就是操作数栈的作用,不过操作数栈可不仅仅是计算基本数据类型,操作数栈中的元素可以是任意Java数据类型,一个32位的数据栈1个栈容量,64位的数据栈2个容量

动态链接

动态链接就是将符号引用转换为内存地址中的直接引用,这个过程有一部份是在类加载阶段或第一次使用的时候执行(参考类加载过程中的解析阶段)。比如静态方法和私有方法,这两者在编译期间就已经确定下来,因此适合在类加载阶段就进行连接。

而另一部分就是在运行期间执行,也就是这里说到的动态链接,比如public方法,protect方法,可能被子类继承重写,在类加载阶段无法确定到底要调用哪个版本的方法,因此适合在运行期间再进行情况进行动态链接

方法返回地址

一个方法执行结果无非就两种:

  1. 正常返回(return)
  2. 异常返回(throw new exception)

无法哪种方法返回,方法返回之后都需要回到方法调用的地方,程序才能继续往后执行,因此方法返回时需要在栈帧中保存一些信息,用于帮助调用者恢复状态继续向后执行

为什么这里用到了“可能”这个模糊的字眼呢?因为JVM仅仅是提供了一个规范,具体的实现还得依靠各个JVM厂商,不同的实现方法,不同的退出方式,可能都会有不一样的过程

本地方法栈

本地方法就是native关键字修饰的方法,它是由C/C++实现的。Java方法执行的时候需要栈,同样的C++方法执行的时候也是需要栈的。

本地方法栈就是native方法执行时所用的栈空间

文章作者: 周君
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 周君 !
评论