jvm笔记-运行时数据区初识

360 阅读5分钟
java虚拟机在执行java相关的程序时,会把对应的内存划分为几个数据区域,如下图:


                                     java虚拟机运行时数据区

其中蓝色的为线程共有的区域,红色为线程私有区域

1.程序计数器

表示当前线程所执行的字节码的指令行。在虚拟机的概念模型里,字节码解释器通过改变计数器所指向的指令行,来选取下一个要执行的指令。因为在java虚拟机中多个线程来回切换执行对应的任务,为了能够切换后能够定位到线程之前程序的执行位置,所以每个线程都需要一个计数器来表示自己程序的执行情况,因此程序计数器对于线程来说是私有的。在执行java方法时,计数器当中会存储正在执行的虚拟机字节码的地址,但如果是native方法,那么计数器的值就会为空。程序计数器是唯一一个java虚拟机规范没有规定任何OutOfMemoryError情况的区域。

2.java虚拟机栈

它描述的是java方法执行的内存模型。也是线程私有的,虚拟机栈的生命周期和线程是一样的。在每次执行方法同时,会在虚拟机栈中创建一个栈帧用于保存局部变量表、操作数栈、动态链接、方法出口等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在虚拟机栈中入栈到出栈。

举个例子:

public class Obj {
    public void m1() {
        m2();
    }

    public void m2() {
        System.out.println();
    }

    public static void main(string[] args) {
        Obj obj = new Obj();
        obj.m1();
    }
}

当main方法执行时,虚拟机栈将会发生如下操作:

准备执行m1,创建m1对应的栈帧,入栈;执行m1时,准备执行m2,创建m2对应的栈帧,入栈;m2执行完毕,m2对应的栈帧出栈;m1执行完毕,m1对应的栈帧出栈。如图:



其中存储的局部变量是在编译器可知的基本数据类型、对象引用(不等同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或者其他能与此对象相关的位置)和returnAddress类型(指向一个字节码指令的地址,这个类型我还有点不太明白,我初步的想法是returnAddress可能是一种情况,在某个方法A中需要调用另外一个方法B,那么B的地址即为该字节码指令地址,如果有哪位大神知道真正的含义,希望告诉我一声^_^)。局部变量表所需的内存空间在编译期间就完成分配了,因此当进入一个方法时,这个方法需要在栈中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变该大小。

在java虚拟机规范中,对这个区域规定两种异常情况:

  1. StackOverflowError异常:线程请求的栈深度大于虚拟机所允许的深度时。
  2. OutOfMemoryError异常:在虚拟机栈是可以动态扩展的情况下,当无法申请到足够的内存时。

3.本地方法栈

本地方法栈和虚拟机栈的作用是类似的,区别和他的名字一样,他是为本地方法(Native方法)服务的,在java虚拟机规范中,该区域也有两个异常:StackOverflowErrorOutOfMemoryError。

4.java堆

java堆是多个线程所共享的区域。它的唯一目的就是存放对象实例(但说它是唯一一个存储对象的区域也不是“绝对”的)。java堆也被成为GC堆,是垃圾收集器管理的主要区域。也因此会对其区域再次划分为新生代老年代等等,不过不管怎么划分,堆存储的都是对象实例。

java虚拟机规定,java堆可以处于物理空间不连续的内存空间中,只要逻辑上是连续的就可以了。它实现时,即可以实现成固定大小的,也可以实现成可扩展的(通过-Xmx和-Xms控制)。如果说堆当中没有内存完成实例分配,并且堆也无法再扩展时,就会抛出OutOfMemoryError异常。

5.方法区

方法区和堆一样是线程所共有的区域。主要用于存储已被虚拟机加载的类信息、静态变量、常量、即时编译器编译后的代码等数据

java虚拟机规范把方法区描述为堆的逻辑部分,也就是说方法区在某些方面和堆是一致的,比如说,它和堆一样不需要连续的内存,并且在实现时可以固定大小也可以选择可扩展。但它相对堆来说会更宽松一些,比如方法区可以选择不实现垃圾收集。但这并不代表该区域的数据会永久不回收,而是该区域的回收目标主要针对常量池的回收以及对类型的卸载。

根据java虚拟机规范规定,当方法区无法满足内存分配需求时,就会抛OutOfMemoryError异常。

6.运行时常量池

运行时常量池是方法区的一部分。它主要会加载两个时期的内容,一个是class文件中类的常量池数据(编译期生成的各种类的字面量和符号引用),会随着类加载后,进入到方法区的运行时常量池存放。另一个是在运行时期,也可能会有些新的常量被放入到运行时常量池中(比如开发人员利用String的intern()方法)。

运行时常量池做为方法去的一部分,自然受到方法区内存的限制,当常量池无法再申请到内存时会抛出OutOfMemoryError异常。